浏览代码

Merge branch 'split-details'

Benjamin Sago 8 年之前
父节点
当前提交
bd860b8fab

+ 20 - 8
src/fs/fields.rs

@@ -69,6 +69,15 @@ pub struct Permissions {
     pub other_execute:  bool,
 }
 
+/// The three pieces of information that are displayed as a single column in
+/// the details view. These values are fused together to make the output a
+/// little more compressed.
+pub struct PermissionsPlus {
+    pub file_type:   Type,
+    pub permissions: Permissions,
+    pub xattrs:      bool,
+}
+
 
 /// A file’s number of hard links on the filesystem.
 ///
@@ -135,14 +144,17 @@ pub enum Size {
     ///
     /// This is what ls does as well. Without it, the devices will just have
     /// file sizes of zero.
-    ///
-    /// You can see what these device numbers mean:
-    /// - http://www.lanana.org/docs/device-list/
-    /// - http://www.lanana.org/docs/device-list/devices-2.6+.txt
-    DeviceIDs {
-        major: u8,
-        minor: u8,
-    }
+    DeviceIDs(DeviceIDs),
+}
+
+/// The major and minor device IDs that gets displayed for device files.
+///
+/// You can see what these device numbers mean:
+/// - http://www.lanana.org/docs/device-list/
+/// - http://www.lanana.org/docs/device-list/devices-2.6+.txt
+pub struct DeviceIDs {
+    pub major: u8,
+    pub minor: u8,
 }
 
 

+ 24 - 27
src/fs/file.rs

@@ -10,26 +10,6 @@ use fs::dir::Dir;
 use fs::fields as f;
 
 
-#[allow(trivial_numeric_casts)]
-mod modes {
-    use libc;
-
-    pub type Mode = u32;
-    // The `libc::mode_t` type’s actual type varies, but the value returned
-    // from `metadata.permissions().mode()` is always `u32`.
-
-    pub const USER_READ: Mode     = libc::S_IRUSR as Mode;
-    pub const USER_WRITE: Mode    = libc::S_IWUSR as Mode;
-    pub const USER_EXECUTE: Mode  = libc::S_IXUSR as Mode;
-    pub const GROUP_READ: Mode    = libc::S_IRGRP as Mode;
-    pub const GROUP_WRITE: Mode   = libc::S_IWGRP as Mode;
-    pub const GROUP_EXECUTE: Mode = libc::S_IXGRP as Mode;
-    pub const OTHER_READ: Mode    = libc::S_IROTH as Mode;
-    pub const OTHER_WRITE: Mode   = libc::S_IWOTH as Mode;
-    pub const OTHER_EXECUTE: Mode = libc::S_IXOTH as Mode;
-}
-
-
 /// A **File** is a wrapper around one of Rust's Path objects, along with
 /// associated data about the file.
 ///
@@ -273,10 +253,10 @@ impl<'dir> File<'dir> {
         }
         else if self.is_char_device() || self.is_block_device() {
             let dev = self.metadata.rdev();
-            f::Size::DeviceIDs {
+            f::Size::DeviceIDs(f::DeviceIDs {
                 major: (dev / 256) as u8,
                 minor: (dev % 256) as u8,
-            }
+            })
         }
         else {
             f::Size::Some(self.metadata.len())
@@ -327,11 +307,7 @@ impl<'dir> File<'dir> {
         }
     }
 
-    /// This file's permissions, with flags for each bit.
-    ///
-    /// The extended-attribute '@' character that you see in here is in fact
-    /// added in later, to avoid querying the extended attributes more than
-    /// once. (Yes, it's a little hacky.)
+    /// This file’s permissions, with flags for each bit.
     pub fn permissions(&self) -> f::Permissions {
         let bits = self.metadata.permissions().mode();
         let has_bit = |bit| { bits & bit == bit };
@@ -449,6 +425,27 @@ impl<'dir> FileTarget<'dir> {
 }
 
 
+/// More readable aliases for the permission bits exposed by libc.
+#[allow(trivial_numeric_casts)]
+mod modes {
+    use libc;
+
+    pub type Mode = u32;
+    // The `libc::mode_t` type’s actual type varies, but the value returned
+    // from `metadata.permissions().mode()` is always `u32`.
+
+    pub const USER_READ: Mode     = libc::S_IRUSR as Mode;
+    pub const USER_WRITE: Mode    = libc::S_IWUSR as Mode;
+    pub const USER_EXECUTE: Mode  = libc::S_IXUSR as Mode;
+    pub const GROUP_READ: Mode    = libc::S_IRGRP as Mode;
+    pub const GROUP_WRITE: Mode   = libc::S_IWGRP as Mode;
+    pub const GROUP_EXECUTE: Mode = libc::S_IXGRP as Mode;
+    pub const OTHER_READ: Mode    = libc::S_IROTH as Mode;
+    pub const OTHER_WRITE: Mode   = libc::S_IWOTH as Mode;
+    pub const OTHER_EXECUTE: Mode = libc::S_IXOTH as Mode;
+}
+
+
 #[cfg(test)]
 mod test {
     use super::ext;

+ 33 - 437
src/output/details.rs

@@ -80,17 +80,13 @@
 use std::io::{Write, Error as IOError, Result as IOResult};
 use std::ops::Add;
 use std::path::PathBuf;
-use std::string::ToString;
-use std::sync::{Arc, Mutex};
-
-use ansi_term::Style;
+use std::sync::{Arc, Mutex, MutexGuard};
 
 use datetime::fmt::DateFormat;
 use datetime::{LocalDateTime, DatePiece};
 use datetime::TimeZone;
 use zoneinfo_compiled::{CompiledData, Result as TZResult};
 
-use unicode_width::UnicodeWidthStr;
 use locale;
 
 use users::{Users, Groups, UsersCache};
@@ -99,8 +95,8 @@ use fs::{Dir, File, fields as f};
 use fs::feature::xattr::{Attribute, FileAttributes};
 use options::{FileFilter, RecurseOptions};
 use output::colours::Colours;
-use output::column::{Alignment, Column, Columns, SizeFormat};
-use output::cell::{TextCell, TextCellContents, DisplayWidth};
+use output::column::{Alignment, Column, Columns};
+use output::cell::{TextCell, TextCellContents};
 use output::tree::TreeTrunk;
 use output::file_name::{FileName, LinkStyle, Classify};
 
@@ -150,7 +146,7 @@ pub struct Details {
 /// running instances of exa, depending on the user's computer's configuration.
 ///
 /// Any environment field should be able to be mocked up for test runs.
-pub struct Environment<U: Users+Groups> {
+pub struct Environment<U> {  // where U: Users+Groups
 
     /// The year of the current time. This gets used to determine which date
     /// format to use.
@@ -176,10 +172,17 @@ pub struct Environment<U: Users+Groups> {
     users: Mutex<U>,
 }
 
+impl<U> Environment<U> {
+    pub fn lock_users(&self) -> MutexGuard<U> {
+        self.users.lock().unwrap()
+    }
+}
+
 impl Default for Environment<UsersCache> {
     fn default() -> Self {
-        let tz = determine_time_zone();
+        use unicode_width::UnicodeWidthStr;
 
+        let tz = determine_time_zone();
         if let Err(ref e) = tz {
             println!("Unable to determine time zone: {}", e);
         }
@@ -193,6 +196,7 @@ impl Default for Environment<UsersCache> {
         // Some locales use a three-character wide month name (Jan to Dec);
         // others vary between three and four (1月 to 12月). We assume that
         // December is the month with the maximum width, and use the width of
+        // that to determine how to pad the other months.
         let december_width = UnicodeWidthStr::width(&*time.short_month_name(11));
         let date_and_time = match december_width {
             4  => DateFormat::parse("{2>:D} {4>:M} {2>:h}:{02>:m}").unwrap(),
@@ -414,7 +418,7 @@ impl Row {
 
 /// A **Table** object gets built up by the view as it lists files and
 /// directories.
-pub struct Table<'a, U: Users+Groups+'a> {
+pub struct Table<'a, U: 'a> { // where U: Users+Groups
     pub rows: Vec<Row>,
 
     pub columns: &'a [Column],
@@ -488,232 +492,30 @@ impl<'a, U: Users+Groups+'a> Table<'a, U> {
                     .collect()
     }
 
-    fn display(&self, file: &File, column: &Column, xattrs: bool) -> TextCell {
-        use output::column::TimeType::*;
-
-        match *column {
-            Column::Permissions          => self.render_permissions(file.type_char(), file.permissions(), xattrs),
-            Column::FileSize(fmt)        => self.render_size(file.size(), fmt),
-            Column::Timestamp(Modified)  => self.render_time(file.modified_time()),
-            Column::Timestamp(Created)   => self.render_time(file.created_time()),
-            Column::Timestamp(Accessed)  => self.render_time(file.accessed_time()),
-            Column::HardLinks            => self.render_links(file.links()),
-            Column::Inode                => self.render_inode(file.inode()),
-            Column::Blocks               => self.render_blocks(file.blocks()),
-            Column::User                 => self.render_user(file.user()),
-            Column::Group                => self.render_group(file.group()),
-            Column::GitStatus            => self.render_git_status(file.git_status()),
-        }
-    }
-
-    fn render_permissions(&self, file_type: f::Type, permissions: f::Permissions, xattrs: bool) -> TextCell {
-        let perms = self.opts.colours.perms;
-        let types = self.opts.colours.filetypes;
-
-        let bit = |bit, chr: &'static str, style: Style| {
-            if bit { style.paint(chr) } else { self.opts.colours.punctuation.paint("-") }
-        };
-
-        let type_char = match file_type {
-            f::Type::File       => types.normal.paint("."),
-            f::Type::Directory  => types.directory.paint("d"),
-            f::Type::Pipe       => types.pipe.paint("|"),
-            f::Type::Link       => types.symlink.paint("l"),
-            f::Type::CharDevice => types.device.paint("c"),
-            f::Type::BlockDevice => types.device.paint("b"),
-            f::Type::Socket     => types.socket.paint("s"),
-            f::Type::Special    => types.special.paint("?"),
-        };
-
-        let x_colour = if file_type.is_regular_file() { perms.user_execute_file }
-                                                 else { perms.user_execute_other };
-
-        let mut chars = vec![
-            type_char,
-            bit(permissions.user_read,     "r", perms.user_read),
-            bit(permissions.user_write,    "w", perms.user_write),
-            bit(permissions.user_execute,  "x", x_colour),
-            bit(permissions.group_read,    "r", perms.group_read),
-            bit(permissions.group_write,   "w", perms.group_write),
-            bit(permissions.group_execute, "x", perms.group_execute),
-            bit(permissions.other_read,    "r", perms.other_read),
-            bit(permissions.other_write,   "w", perms.other_write),
-            bit(permissions.other_execute, "x", perms.other_execute),
-        ];
-
-        if xattrs {
-            chars.push(perms.attribute.paint("@"));
+    fn permissions_plus(&self, file: &File, xattrs: bool) -> f::PermissionsPlus {
+        f::PermissionsPlus {
+            file_type: file.type_char(),
+            permissions: file.permissions(),
+            xattrs: xattrs,
         }
-
-        // As these are all ASCII characters, we can guarantee that they’re
-        // all going to be one character wide, and don’t need to compute the
-        // cell’s display width.
-        let width = DisplayWidth::from(chars.len());
-
-        TextCell {
-            contents: chars.into(),
-            width:    width,
-        }
-    }
-
-    fn render_links(&self, links: f::Links) -> TextCell {
-        let style = if links.multiple { self.opts.colours.links.multi_link_file }
-                                 else { self.opts.colours.links.normal };
-
-        TextCell::paint(style, self.env.numeric.format_int(links.count))
-    }
-
-    fn render_blocks(&self, blocks: f::Blocks) -> TextCell {
-        match blocks {
-            f::Blocks::Some(blk)  => TextCell::paint(self.opts.colours.blocks, blk.to_string()),
-            f::Blocks::None       => TextCell::blank(self.opts.colours.punctuation),
-        }
-    }
-
-    fn render_inode(&self, inode: f::Inode) -> TextCell {
-        TextCell::paint(self.opts.colours.inode, inode.0.to_string())
-    }
-
-    fn render_size(&self, size: f::Size, size_format: SizeFormat) -> TextCell {
-        use number_prefix::{binary_prefix, decimal_prefix};
-        use number_prefix::{Prefixed, Standalone, PrefixNames};
-
-        let size = match size {
-            f::Size::Some(s)                     => s,
-            f::Size::None                        => return TextCell::blank(self.opts.colours.punctuation),
-            f::Size::DeviceIDs { major, minor }  => return self.render_device_ids(major, minor),
-        };
-
-        let result = match size_format {
-            SizeFormat::DecimalBytes  => decimal_prefix(size as f64),
-            SizeFormat::BinaryBytes   => binary_prefix(size as f64),
-            SizeFormat::JustBytes     => {
-                let string = self.env.numeric.format_int(size);
-                return TextCell::paint(self.opts.colours.file_size(size), string);
-            },
-        };
-
-        let (prefix, n) = match result {
-            Standalone(b)  => return TextCell::paint(self.opts.colours.file_size(b as u64), b.to_string()),
-            Prefixed(p, n) => (p, n)
-        };
-
-        let symbol = prefix.symbol();
-        let number = if n < 10f64 { self.env.numeric.format_float(n, 1) }
-                             else { self.env.numeric.format_int(n as isize) };
-
-        // The numbers and symbols are guaranteed to be written in ASCII, so
-        // we can skip the display width calculation.
-        let width = DisplayWidth::from(number.len() + symbol.len());
-
-        TextCell {
-            width:    width,
-            contents: vec![
-                self.opts.colours.file_size(size).paint(number),
-                self.opts.colours.size.unit.paint(symbol),
-            ].into(),
-        }
-    }
-
-    fn render_device_ids(&self, major: u8, minor: u8) -> TextCell {
-        let major = major.to_string();
-        let minor = minor.to_string();
-
-        TextCell {
-            width: DisplayWidth::from(major.len() + 1 + minor.len()),
-            contents: vec![
-                self.opts.colours.size.major.paint(major),
-                self.opts.colours.punctuation.paint(","),
-                self.opts.colours.size.minor.paint(minor),
-            ].into(),
-        }
-    }
-
-    #[allow(trivial_numeric_casts)]
-    fn render_time(&self, timestamp: f::Time) -> TextCell {
-        // TODO(ogham): This method needs some serious de-duping!
-        // zoned and local times have different types at the moment,
-        // so it's tricky.
-
-        if let Some(ref tz) = self.env.tz {
-            let date = tz.to_zoned(LocalDateTime::at(timestamp.0 as i64));
-
-            let datestamp = if date.year() == self.env.current_year {
-                self.env.date_and_time.format(&date, &self.env.time)
-            }
-            else {
-                self.env.date_and_year.format(&date, &self.env.time)
-            };
-
-            TextCell::paint(self.opts.colours.date, datestamp)
-        }
-        else {
-            let date = LocalDateTime::at(timestamp.0 as i64);
-
-            let datestamp = if date.year() == self.env.current_year {
-                self.env.date_and_time.format(&date, &self.env.time)
-            }
-            else {
-                self.env.date_and_year.format(&date, &self.env.time)
-            };
-
-            TextCell::paint(self.opts.colours.date, datestamp)
-        }
-    }
-
-    fn render_git_status(&self, git: f::Git) -> TextCell {
-        let git_char = |status| match status {
-            f::GitStatus::NotModified  => self.opts.colours.punctuation.paint("-"),
-            f::GitStatus::New          => self.opts.colours.git.new.paint("N"),
-            f::GitStatus::Modified     => self.opts.colours.git.modified.paint("M"),
-            f::GitStatus::Deleted      => self.opts.colours.git.deleted.paint("D"),
-            f::GitStatus::Renamed      => self.opts.colours.git.renamed.paint("R"),
-            f::GitStatus::TypeChange   => self.opts.colours.git.typechange.paint("T"),
-        };
-
-        TextCell {
-            width: DisplayWidth::from(2),
-            contents: vec![
-                git_char(git.staged),
-                git_char(git.unstaged)
-            ].into(),
-        }
-    }
-
-    fn render_user(&self, user: f::User) -> TextCell {
-        let users = self.env.users.lock().unwrap();
-
-
-        let user_name = match users.get_user_by_uid(user.0) {
-            Some(user)  => user.name().to_owned(),
-            None        => user.0.to_string(),
-        };
-
-        let style = if users.get_current_uid() == user.0 { self.opts.colours.users.user_you }
-                                                    else { self.opts.colours.users.user_someone_else };
-        TextCell::paint(style, user_name)
     }
 
-    fn render_group(&self, group: f::Group) -> TextCell {
-        use users::os::unix::GroupExt;
-
-        let mut style = self.opts.colours.users.group_not_yours;
-
-        let users = self.env.users.lock().unwrap();
-        let group = match users.get_group_by_gid(group.0) {
-            Some(g) => (*g).clone(),
-            None    => return TextCell::paint(style, group.0.to_string()),
-        };
+    fn display(&self, file: &File, column: &Column, xattrs: bool) -> TextCell {
+        use output::column::TimeType::*;
 
-        let current_uid = users.get_current_uid();
-        if let Some(current_user) = users.get_user_by_uid(current_uid) {
-            if current_user.primary_group_id() == group.gid()
-            || group.members().contains(&current_user.name().to_owned()) {
-                style = self.opts.colours.users.group_yours;
-            }
+        match *column {
+            Column::Permissions          => self.permissions_plus(file, xattrs).render(&self.opts.colours),
+            Column::FileSize(fmt)        => file.size().render(&self.opts.colours, fmt, &self.env.numeric),
+            Column::Timestamp(Modified)  => file.modified_time().render(&self.opts.colours, &self.env.tz, &self.env.date_and_time, &self.env.date_and_year, &self.env.time, self.env.current_year),
+            Column::Timestamp(Created)   => file.created_time().render( &self.opts.colours, &self.env.tz, &self.env.date_and_time, &self.env.date_and_year, &self.env.time, self.env.current_year),
+            Column::Timestamp(Accessed)  => file.accessed_time().render(&self.opts.colours, &self.env.tz, &self.env.date_and_time, &self.env.date_and_year, &self.env.time, self.env.current_year),
+            Column::HardLinks            => file.links().render(&self.opts.colours, &self.env.numeric),
+            Column::Inode                => file.inode().render(&self.opts.colours),
+            Column::Blocks               => file.blocks().render(&self.opts.colours),
+            Column::User                 => file.user().render(&self.opts.colours, &*self.env.lock_users()),
+            Column::Group                => file.group().render(&self.opts.colours, &*self.env.lock_users()),
+            Column::GitStatus            => file.git_status().render(&self.opts.colours),
         }
-
-        TextCell::paint(style, group.name().to_owned())
     }
 
     /// Render the table as a vector of Cells, to be displayed on standard output.
@@ -771,209 +573,3 @@ impl<'a, U: Users+Groups+'a> Table<'a, U> {
         cells
     }
 }
-
-
-#[cfg(test)]
-pub mod test {
-    pub use super::{Table, Environment, Details};
-    pub use std::sync::Mutex;
-
-    pub use fs::{File, fields as f};
-    pub use output::column::{Column, Columns};
-    pub use output::cell::TextCell;
-
-    pub use users::{User, Group, uid_t, gid_t};
-    pub use users::mock::MockUsers;
-    pub use users::os::unix::{UserExt, GroupExt};
-    pub use datetime::fmt::DateFormat;
-    pub use ansi_term::Style;
-    pub use ansi_term::Colour::*;
-
-    impl Default for Environment<MockUsers> {
-        fn default() -> Self {
-            use locale;
-            use users::mock::MockUsers;
-            use std::sync::Mutex;
-
-            Environment {
-                current_year:  1234,
-                numeric:       locale::Numeric::english(),
-                time:          locale::Time::english(),
-                date_and_time: DateFormat::parse("{2>:D} {4>:M} {2>:h}:{02>:m}").unwrap(),
-                date_and_year: DateFormat::parse("{2>:D} {:M} {5>:Y}").unwrap(),
-                tz:            None,
-                users:         Mutex::new(MockUsers::with_current_uid(0)),
-            }
-        }
-    }
-
-    pub fn new_table<'a>(columns: &'a [Column], details: &'a Details, users: MockUsers) -> Table<'a, MockUsers> {
-        use std::sync::Arc;
-
-        Table {
-            columns: columns,
-            opts: details,
-            env: Arc::new(Environment { users: Mutex::new(users), ..Environment::default() }),
-            rows: Vec::new(),
-        }
-    }
-
-    mod users {
-        #![allow(unused_results)]
-        use super::*;
-
-        #[test]
-        fn named() {
-            let columns = Columns::default().for_dir(None);
-            let mut details = Details::default();
-            details.colours.users.user_you = Red.bold();
-
-            let mut users = MockUsers::with_current_uid(1000);
-            users.add_user(User::new(1000, "enoch", 100));
-
-            let table = new_table(&columns, &details, users);
-
-            let user = f::User(1000);
-            let expected = TextCell::paint_str(Red.bold(), "enoch");
-            assert_eq!(expected, table.render_user(user))
-        }
-
-        #[test]
-        fn unnamed() {
-            let columns = Columns::default().for_dir(None);
-            let mut details = Details::default();
-            details.colours.users.user_you = Cyan.bold();
-
-            let users = MockUsers::with_current_uid(1000);
-
-            let table = new_table(&columns, &details, users);
-
-            let user = f::User(1000);
-            let expected = TextCell::paint_str(Cyan.bold(), "1000");
-            assert_eq!(expected, table.render_user(user));
-        }
-
-        #[test]
-        fn different_named() {
-            let columns = Columns::default().for_dir(None);
-            let mut details = Details::default();
-            details.colours.users.user_someone_else = Green.bold();
-
-            let table = new_table(&columns, &details, MockUsers::with_current_uid(0));
-            table.env.users.lock().unwrap().add_user(User::new(1000, "enoch", 100));
-
-            let user = f::User(1000);
-            let expected = TextCell::paint_str(Green.bold(), "enoch");
-            assert_eq!(expected, table.render_user(user));
-        }
-
-        #[test]
-        fn different_unnamed() {
-            let columns = Columns::default().for_dir(None);
-            let mut details = Details::default();
-            details.colours.users.user_someone_else = Red.normal();
-
-            let table = new_table(&columns, &details, MockUsers::with_current_uid(0));
-
-            let user = f::User(1000);
-            let expected = TextCell::paint_str(Red.normal(), "1000");
-            assert_eq!(expected, table.render_user(user));
-        }
-
-        #[test]
-        fn overflow() {
-            let columns = Columns::default().for_dir(None);
-            let mut details = Details::default();
-            details.colours.users.user_someone_else = Blue.underline();
-
-            let table = new_table(&columns, &details, MockUsers::with_current_uid(0));
-
-            let user = f::User(2_147_483_648);
-            let expected = TextCell::paint_str(Blue.underline(), "2147483648");
-            assert_eq!(expected, table.render_user(user));
-        }
-    }
-
-    mod groups {
-        #![allow(unused_results)]
-        use super::*;
-
-        #[test]
-        fn named() {
-            let columns = Columns::default().for_dir(None);
-            let mut details = Details::default();
-            details.colours.users.group_not_yours = Fixed(101).normal();
-
-            let mut users = MockUsers::with_current_uid(1000);
-            users.add_group(Group::new(100, "folk"));
-            let table = new_table(&columns, &details, users);
-
-            let group = f::Group(100);
-            let expected = TextCell::paint_str(Fixed(101).normal(), "folk");
-            assert_eq!(expected, table.render_group(group))
-        }
-
-        #[test]
-        fn unnamed() {
-            let columns = Columns::default().for_dir(None);
-            let mut details = Details::default();
-            details.colours.users.group_not_yours = Fixed(87).normal();
-
-            let users = MockUsers::with_current_uid(1000);
-            let table = new_table(&columns, &details, users);
-
-            let group = f::Group(100);
-            let expected = TextCell::paint_str(Fixed(87).normal(), "100");
-            assert_eq!(expected, table.render_group(group));
-        }
-
-        #[test]
-        fn primary() {
-            let columns = Columns::default().for_dir(None);
-            let mut details = Details::default();
-            details.colours.users.group_yours = Fixed(64).normal();
-
-            let mut users = MockUsers::with_current_uid(2);
-            users.add_user(User::new(2, "eve", 100));
-            users.add_group(Group::new(100, "folk"));
-
-            let table = new_table(&columns, &details, users);
-
-            let group = f::Group(100);
-            let expected = TextCell::paint_str(Fixed(64).normal(), "folk");
-            assert_eq!(expected, table.render_group(group))
-        }
-
-        #[test]
-        fn secondary() {
-            let columns = Columns::default().for_dir(None);
-            let mut details = Details::default();
-            details.colours.users.group_yours = Fixed(31).normal();
-
-            let mut users = MockUsers::with_current_uid(2);
-            users.add_user(User::new(2, "eve", 666));
-
-            let test_group = Group::new(100, "folk").add_member("eve");
-            users.add_group(test_group);
-
-            let table = new_table(&columns, &details, users);
-
-            let group = f::Group(100);
-            let expected = TextCell::paint_str(Fixed(31).normal(), "folk");
-            assert_eq!(expected, table.render_group(group))
-        }
-
-        #[test]
-        fn overflow() {
-            let columns = Columns::default().for_dir(None);
-            let mut details = Details::default();
-            details.colours.users.group_not_yours = Blue.underline();
-
-            let table = new_table(&columns, &details, MockUsers::with_current_uid(0));
-
-            let group = f::Group(2_147_483_648);
-            let expected = TextCell::paint_str(Blue.underline(), "2147483648");
-            assert_eq!(expected, table.render_group(group));
-        }
-    }
-}

+ 1 - 0
src/output/mod.rs

@@ -16,3 +16,4 @@ mod colours;
 mod tree;
 pub mod file_name;
 mod escape;
+mod render;

+ 44 - 0
src/output/render/blocks.rs

@@ -0,0 +1,44 @@
+use output::cell::TextCell;
+use output::colours::Colours;
+use fs::fields as f;
+
+
+impl f::Blocks {
+    pub fn render(&self, colours: &Colours) -> TextCell {
+        match *self {
+            f::Blocks::Some(ref blk)  => TextCell::paint(colours.blocks, blk.to_string()),
+            f::Blocks::None           => TextCell::blank(colours.punctuation),
+        }
+    }
+}
+
+
+#[cfg(test)]
+pub mod test {
+    use output::details::Details;
+    use output::cell::TextCell;
+    use fs::fields as f;
+
+    use ansi_term::Colour::*;
+
+
+    #[test]
+    fn blocklessness() {
+        let mut details = Details::default();
+        details.colours.punctuation = Green.italic();
+
+        let blox = f::Blocks::None;
+        let expected = TextCell::blank(Green.italic());
+        assert_eq!(expected, blox.render(&details.colours).into());
+    }
+
+    #[test]
+    fn blockfulity() {
+        let mut details = Details::default();
+        details.colours.blocks = Red.blink();
+
+        let blox = f::Blocks::Some(3005);
+        let expected = TextCell::paint_str(Red.blink(), "3005");
+        assert_eq!(expected, blox.render(&details.colours).into());
+    }
+}

+ 86 - 0
src/output/render/git.rs

@@ -0,0 +1,86 @@
+use ansi_term::ANSIString;
+
+use output::cell::{TextCell, DisplayWidth};
+use output::colours::Colours;
+use fs::fields as f;
+
+
+impl f::Git {
+    pub fn render(&self, colours: &Colours) -> TextCell {
+        TextCell {
+            width: DisplayWidth::from(2),
+            contents: vec![
+                self.staged.render(colours),
+                self.unstaged.render(colours),
+            ].into(),
+        }
+    }
+}
+
+impl f::GitStatus {
+    fn render(&self, colours: &Colours) -> ANSIString<'static> {
+        match *self {
+            f::GitStatus::NotModified  => colours.punctuation.paint("-"),
+            f::GitStatus::New          => colours.git.new.paint("N"),
+            f::GitStatus::Modified     => colours.git.modified.paint("M"),
+            f::GitStatus::Deleted      => colours.git.deleted.paint("D"),
+            f::GitStatus::Renamed      => colours.git.renamed.paint("R"),
+            f::GitStatus::TypeChange   => colours.git.typechange.paint("T"),
+        }
+    }
+}
+
+
+#[cfg(test)]
+pub mod test {
+    use output::details::Details;
+    use output::cell::{TextCell, DisplayWidth};
+    use fs::fields as f;
+
+    use ansi_term::Colour::*;
+
+
+    #[test]
+    fn git_blank() {
+        let mut details = Details::default();
+        details.colours.punctuation = Fixed(44).normal();
+
+        let stati = f::Git {
+            staged:   f::GitStatus::NotModified,
+            unstaged: f::GitStatus::NotModified,
+        };
+
+        let expected = TextCell {
+            width: DisplayWidth::from(2),
+            contents: vec![
+                Fixed(44).paint("-"),
+                Fixed(44).paint("-"),
+            ].into(),
+        };
+
+        assert_eq!(expected, stati.render(&details.colours).into())
+    }
+
+
+    #[test]
+    fn git_new_changed() {
+        let mut details = Details::default();
+        details.colours.git.new = Red.normal();
+        details.colours.git.modified = Purple.normal();
+
+        let stati = f::Git {
+            staged:   f::GitStatus::New,
+            unstaged: f::GitStatus::Modified,
+        };
+
+        let expected = TextCell {
+            width: DisplayWidth::from(2),
+            contents: vec![
+                Red.paint("N"),
+                Purple.paint("M"),
+            ].into(),
+        };
+
+        assert_eq!(expected, stati.render(&details.colours).into())
+    }
+}

+ 110 - 0
src/output/render/groups.rs

@@ -0,0 +1,110 @@
+use users::{Users, Groups};
+
+use fs::fields as f;
+use output::colours::Colours;
+use output::cell::TextCell;
+
+
+impl f::Group {
+    pub fn render<U: Users+Groups>(&self, colours: &Colours, users: &U) -> TextCell {
+        use users::os::unix::GroupExt;
+
+        let mut style = colours.users.group_not_yours;
+
+        let group = match users.get_group_by_gid(self.0) {
+            Some(g) => (*g).clone(),
+            None    => return TextCell::paint(style, self.0.to_string()),
+        };
+
+        let current_uid = users.get_current_uid();
+        if let Some(current_user) = users.get_user_by_uid(current_uid) {
+            if current_user.primary_group_id() == group.gid()
+            || group.members().contains(&current_user.name().to_owned()) {
+                style = colours.users.group_yours;
+            }
+        }
+
+        TextCell::paint(style, group.name().to_owned())
+    }
+}
+
+
+#[cfg(test)]
+#[allow(unused_results)]
+pub mod test {
+    use output::details::Details;
+
+    use fs::fields as f;
+    use output::cell::TextCell;
+
+    use users::{User, Group};
+    use users::mock::MockUsers;
+    use users::os::unix::GroupExt;
+    use ansi_term::Colour::*;
+
+
+    #[test]
+    fn named() {
+        let mut details = Details::default();
+        details.colours.users.group_not_yours = Fixed(101).normal();
+
+        let mut users = MockUsers::with_current_uid(1000);
+        users.add_group(Group::new(100, "folk"));
+
+        let group = f::Group(100);
+        let expected = TextCell::paint_str(Fixed(101).normal(), "folk");
+        assert_eq!(expected, group.render(&details.colours, &users))
+    }
+
+    #[test]
+    fn unnamed() {
+        let mut details = Details::default();
+        details.colours.users.group_not_yours = Fixed(87).normal();
+
+        let users = MockUsers::with_current_uid(1000);
+
+        let group = f::Group(100);
+        let expected = TextCell::paint_str(Fixed(87).normal(), "100");
+        assert_eq!(expected, group.render(&details.colours, &users));
+    }
+
+    #[test]
+    fn primary() {
+        let mut details = Details::default();
+        details.colours.users.group_yours = Fixed(64).normal();
+
+        let mut users = MockUsers::with_current_uid(2);
+        users.add_user(User::new(2, "eve", 100));
+        users.add_group(Group::new(100, "folk"));
+
+        let group = f::Group(100);
+        let expected = TextCell::paint_str(Fixed(64).normal(), "folk");
+        assert_eq!(expected, group.render(&details.colours, &users))
+    }
+
+    #[test]
+    fn secondary() {
+        let mut details = Details::default();
+        details.colours.users.group_yours = Fixed(31).normal();
+
+        let mut users = MockUsers::with_current_uid(2);
+        users.add_user(User::new(2, "eve", 666));
+
+        let test_group = Group::new(100, "folk").add_member("eve");
+        users.add_group(test_group);
+
+        let group = f::Group(100);
+        let expected = TextCell::paint_str(Fixed(31).normal(), "folk");
+        assert_eq!(expected, group.render(&details.colours, &users))
+    }
+
+    #[test]
+    fn overflow() {
+        let mut details = Details::default();
+        details.colours.users.group_not_yours = Blue.underline();
+
+        let group = f::Group(2_147_483_648);
+        let expected = TextCell::paint_str(Blue.underline(), "2147483648");
+        assert_eq!(expected, group.render(&details.colours, &MockUsers::with_current_uid(0)));
+    }
+}

+ 31 - 0
src/output/render/inode.rs

@@ -0,0 +1,31 @@
+use output::cell::TextCell;
+use output::colours::Colours;
+use fs::fields as f;
+
+
+impl f::Inode {
+    pub fn render(&self, colours: &Colours) -> TextCell {
+        TextCell::paint(colours.inode, self.0.to_string())
+    }
+}
+
+
+#[cfg(test)]
+pub mod test {
+    use output::details::Details;
+    use output::cell::TextCell;
+    use fs::fields as f;
+
+    use ansi_term::Colour::*;
+
+
+    #[test]
+    fn blocklessness() {
+        let mut details = Details::default();
+        details.colours.inode = Cyan.underline();
+
+        let io = f::Inode(1414213);
+        let expected = TextCell::paint_str(Cyan.underline(), "1414213");
+        assert_eq!(expected, io.render(&details.colours).into());
+    }
+}

+ 81 - 0
src/output/render/links.rs

@@ -0,0 +1,81 @@
+use output::cell::TextCell;
+use output::colours::Colours;
+use fs::fields as f;
+
+use locale;
+
+
+impl f::Links {
+    pub fn render(&self, colours: &Colours, numeric: &locale::Numeric) -> TextCell {
+        let style = if self.multiple { colours.links.multi_link_file }
+                                else { colours.links.normal };
+
+        TextCell::paint(style, numeric.format_int(self.count))
+    }
+}
+
+
+#[cfg(test)]
+pub mod test {
+    use output::details::Details;
+    use output::cell::{TextCell, DisplayWidth};
+    use fs::fields as f;
+
+    use ansi_term::Colour::*;
+    use locale;
+
+
+    #[test]
+    fn regular_file() {
+        let mut details = Details::default();
+        details.colours.links.normal = Blue.normal();
+
+        let stati = f::Links {
+            count:    1,
+            multiple: false,
+        };
+
+        let expected = TextCell {
+            width: DisplayWidth::from(1),
+            contents: vec![ Blue.paint("1") ].into(),
+        };
+
+        assert_eq!(expected, stati.render(&details.colours, &locale::Numeric::english()).into());
+    }
+
+    #[test]
+    fn regular_directory() {
+        let mut details = Details::default();
+        details.colours.links.normal = Blue.normal();
+
+        let stati = f::Links {
+            count:    3005,
+            multiple: false,
+        };
+
+        let expected = TextCell {
+            width: DisplayWidth::from(5),
+            contents: vec![ Blue.paint("3,005") ].into(),
+        };
+
+        assert_eq!(expected, stati.render(&details.colours, &locale::Numeric::english()).into());
+    }
+
+    #[test]
+    fn popular_file() {
+        let mut details = Details::default();
+        details.colours.links.multi_link_file = Blue.on(Red);
+
+        let stati = f::Links {
+            count:    3005,
+            multiple: true,
+        };
+
+        let expected = TextCell {
+            width: DisplayWidth::from(5),
+            contents: vec![ Blue.on(Red).paint("3,005") ].into(),
+        };
+
+        assert_eq!(expected, stati.render(&details.colours, &locale::Numeric::english()).into());
+    }
+}

+ 9 - 0
src/output/render/mod.rs

@@ -0,0 +1,9 @@
+mod blocks;
+mod git;
+mod groups;
+mod inode;
+mod links;
+mod permissions;
+mod size;
+mod times;
+mod users;

+ 125 - 0
src/output/render/permissions.rs

@@ -0,0 +1,125 @@
+use fs::fields as f;
+use output::colours::Colours;
+use output::cell::{TextCell, DisplayWidth};
+use ansi_term::{ANSIString, Style};
+
+
+impl f::PermissionsPlus {
+    pub fn render(&self, colours: &Colours) -> TextCell {
+        let x_colour = if self.file_type.is_regular_file() { colours.perms.user_execute_file }
+                                                      else { colours.perms.user_execute_other };
+
+        let mut chars = vec![ self.file_type.render(colours) ];
+        chars.extend(self.permissions.render(colours, x_colour));
+
+        if self.xattrs {
+           chars.push(colours.perms.attribute.paint("@"));
+        }
+
+        // As these are all ASCII characters, we can guarantee that they’re
+        // all going to be one character wide, and don’t need to compute the
+        // cell’s display width.
+        TextCell {
+            width:    DisplayWidth::from(chars.len()),
+            contents: chars.into(),
+        }
+    }
+}
+
+impl f::Permissions {
+    pub fn render(&self, colours: &Colours, x_colour: Style) -> Vec<ANSIString<'static>> {
+        let bit = |bit, chr: &'static str, style: Style| {
+            if bit { style.paint(chr) } else { colours.punctuation.paint("-") }
+        };
+
+        vec![
+            bit(self.user_read,     "r", colours.perms.user_read),
+            bit(self.user_write,    "w", colours.perms.user_write),
+            bit(self.user_execute,  "x", x_colour),
+            bit(self.group_read,    "r", colours.perms.group_read),
+            bit(self.group_write,   "w", colours.perms.group_write),
+            bit(self.group_execute, "x", colours.perms.group_execute),
+            bit(self.other_read,    "r", colours.perms.other_read),
+            bit(self.other_write,   "w", colours.perms.other_write),
+            bit(self.other_execute, "x", colours.perms.other_execute),
+        ]
+    }
+}
+
+impl f::Type {
+    pub fn render(&self, colours: &Colours) -> ANSIString<'static> {
+        match *self {
+            f::Type::File        => colours.filetypes.normal.paint("."),
+            f::Type::Directory   => colours.filetypes.directory.paint("d"),
+            f::Type::Pipe        => colours.filetypes.pipe.paint("|"),
+            f::Type::Link        => colours.filetypes.symlink.paint("l"),
+            f::Type::CharDevice  => colours.filetypes.device.paint("c"),
+            f::Type::BlockDevice => colours.filetypes.device.paint("b"),
+            f::Type::Socket      => colours.filetypes.socket.paint("s"),
+            f::Type::Special     => colours.filetypes.special.paint("?"),
+        }
+    }
+}
+
+
+
+#[cfg(test)]
+#[allow(unused_results)]
+pub mod test {
+    use output::details::Details;
+    use output::cell::TextCellContents;
+    use fs::fields as f;
+
+    use ansi_term::Colour::*;
+
+
+    #[test]
+    fn negate() {
+        let mut details = Details::default();
+        details.colours.punctuation = Fixed(44).normal();
+
+        let bits = f::Permissions {
+            user_read:  false,  user_write:  false,  user_execute:  false,
+            group_read: false,  group_write: false,  group_execute: false,
+            other_read: false,  other_write: false,  other_execute: false,
+        };
+
+        let expected = TextCellContents::from(vec![
+            Fixed(44).paint("-"),  Fixed(44).paint("-"),  Fixed(44).paint("-"),
+            Fixed(44).paint("-"),  Fixed(44).paint("-"),  Fixed(44).paint("-"),
+            Fixed(44).paint("-"),  Fixed(44).paint("-"),  Fixed(44).paint("-"),
+        ]);
+
+        assert_eq!(expected, bits.render(&details.colours, Fixed(66).normal()).into())
+    }
+
+
+    #[test]
+    fn affirm() {
+        let mut details = Details::default();
+        details.colours.perms.user_read    = Fixed(101).normal();
+        details.colours.perms.user_write   = Fixed(102).normal();
+
+        details.colours.perms.group_read    = Fixed(104).normal();
+        details.colours.perms.group_write   = Fixed(105).normal();
+        details.colours.perms.group_execute = Fixed(106).normal();
+
+        details.colours.perms.other_read    = Fixed(107).normal();
+        details.colours.perms.other_write   = Fixed(108).normal();
+        details.colours.perms.other_execute = Fixed(109).normal();
+
+        let bits = f::Permissions {
+            user_read:  true,  user_write:  true,  user_execute:  true,
+            group_read: true,  group_write: true,  group_execute: true,
+            other_read: true,  other_write: true,  other_execute: true,
+        };
+
+        let expected = TextCellContents::from(vec![
+            Fixed(101).paint("r"),  Fixed(102).paint("w"),  Fixed(103).paint("x"),
+            Fixed(104).paint("r"),  Fixed(105).paint("w"),  Fixed(106).paint("x"),
+            Fixed(107).paint("r"),  Fixed(108).paint("w"),  Fixed(109).paint("x"),
+        ]);
+
+        assert_eq!(expected, bits.render(&details.colours, Fixed(103).normal()).into())
+    }
+}

+ 164 - 0
src/output/render/size.rs

@@ -0,0 +1,164 @@
+use fs::fields as f;
+use output::column::SizeFormat;
+use output::cell::{TextCell, DisplayWidth};
+use output::colours::Colours;
+use locale;
+
+
+impl f::Size {
+    pub fn render(&self, colours: &Colours, size_format: SizeFormat, numerics: &locale::Numeric) -> TextCell {
+        use number_prefix::{binary_prefix, decimal_prefix};
+        use number_prefix::{Prefixed, Standalone, PrefixNames};
+
+        let size = match *self {
+            f::Size::Some(s)             => s,
+            f::Size::None                => return TextCell::blank(colours.punctuation),
+            f::Size::DeviceIDs(ref ids)  => return ids.render(colours),
+        };
+
+        let result = match size_format {
+            SizeFormat::DecimalBytes  => decimal_prefix(size as f64),
+            SizeFormat::BinaryBytes   => binary_prefix(size as f64),
+            SizeFormat::JustBytes     => {
+                let string = numerics.format_int(size);
+                return TextCell::paint(colours.file_size(size), string);
+            },
+        };
+
+        let (prefix, n) = match result {
+            Standalone(b)  => return TextCell::paint(colours.file_size(b as u64), b.to_string()),
+            Prefixed(p, n) => (p, n)
+        };
+
+        let symbol = prefix.symbol();
+        let number = if n < 10f64 { numerics.format_float(n, 1) }
+                             else { numerics.format_int(n as isize) };
+
+        // The numbers and symbols are guaranteed to be written in ASCII, so
+        // we can skip the display width calculation.
+        let width = DisplayWidth::from(number.len() + symbol.len());
+
+        TextCell {
+            width:    width,
+            contents: vec![
+                colours.file_size(size).paint(number),
+                colours.size.unit.paint(symbol),
+            ].into(),
+        }
+    }
+}
+
+impl f::DeviceIDs {
+    fn render(&self, colours: &Colours) -> TextCell {
+        let major = self.major.to_string();
+        let minor = self.minor.to_string();
+
+        TextCell {
+            width: DisplayWidth::from(major.len() + 1 + minor.len()),
+            contents: vec![
+                colours.size.major.paint(major),
+                colours.punctuation.paint(","),
+                colours.size.minor.paint(minor),
+            ].into(),
+        }
+    }
+}
+
+
+#[cfg(test)]
+pub mod test {
+    use output::details::Details;
+    use output::column::SizeFormat;
+    use output::cell::{TextCell, DisplayWidth};
+    use fs::fields as f;
+
+    use locale;
+    use ansi_term::Colour::*;
+
+
+    #[test]
+    fn directory() {
+        let mut details = Details::default();
+        details.colours.punctuation = Green.italic();
+
+        let directory = f::Size::None;
+        let expected = TextCell::blank(Green.italic());
+        assert_eq!(expected, directory.render(&details.colours, SizeFormat::JustBytes, &locale::Numeric::english()))
+    }
+
+
+    #[test]
+    fn file_decimal() {
+        let mut details = Details::default();
+        details.colours.size.numbers = Blue.on(Red);
+        details.colours.size.unit    = Yellow.bold();
+
+        let directory = f::Size::Some(2_100_000);
+        let expected = TextCell {
+            width: DisplayWidth::from(4),
+            contents: vec![
+                Blue.on(Red).paint("2.1"),
+                Yellow.bold().paint("M"),
+            ].into(),
+        };
+
+        assert_eq!(expected, directory.render(&details.colours, SizeFormat::DecimalBytes, &locale::Numeric::english()))
+    }
+
+
+    #[test]
+    fn file_binary() {
+        let mut details = Details::default();
+        details.colours.size.numbers = Blue.on(Red);
+        details.colours.size.unit    = Yellow.bold();
+
+        let directory = f::Size::Some(1_048_576);
+        let expected = TextCell {
+            width: DisplayWidth::from(5),
+            contents: vec![
+                Blue.on(Red).paint("1.0"),
+                Yellow.bold().paint("Mi"),
+            ].into(),
+        };
+
+        assert_eq!(expected, directory.render(&details.colours, SizeFormat::BinaryBytes, &locale::Numeric::english()))
+    }
+
+
+    #[test]
+    fn file_bytes() {
+        let mut details = Details::default();
+        details.colours.size.numbers = Blue.on(Red);
+
+        let directory = f::Size::Some(1048576);
+        let expected = TextCell {
+            width: DisplayWidth::from(9),
+            contents: vec![
+                Blue.on(Red).paint("1,048,576"),
+            ].into(),
+        };
+
+        assert_eq!(expected, directory.render(&details.colours, SizeFormat::JustBytes, &locale::Numeric::english()))
+    }
+
+
+    #[test]
+    fn device_ids() {
+        let mut details = Details::default();
+        details.colours.size.major = Blue.on(Red);
+        details.colours.punctuation = Green.italic();
+        details.colours.size.minor = Cyan.on(Yellow);
+
+        let directory = f::Size::DeviceIDs(f::DeviceIDs { major: 10, minor: 80 });
+        let expected = TextCell {
+            width: DisplayWidth::from(5),
+            contents: vec![
+                Blue.on(Red).paint("10"),
+                Green.italic().paint(","),
+                Cyan.on(Yellow).paint("80"),
+            ].into(),
+        };
+
+        assert_eq!(expected, directory.render(&details.colours, SizeFormat::JustBytes, &locale::Numeric::english()))
+    }
+}

+ 45 - 0
src/output/render/times.rs

@@ -0,0 +1,45 @@
+use output::cell::TextCell;
+use output::colours::Colours;
+use fs::fields as f;
+
+use datetime::{LocalDateTime, TimeZone, DatePiece};
+use datetime::fmt::DateFormat;
+use locale;
+
+
+#[allow(trivial_numeric_casts)]
+impl f::Time {
+    pub fn render(&self, colours: &Colours, tz: &Option<TimeZone>,
+                          date_and_time: &DateFormat<'static>, date_and_year: &DateFormat<'static>,
+                          time: &locale::Time, current_year: i64) -> TextCell {
+
+        // TODO(ogham): This method needs some serious de-duping!
+        // zoned and local times have different types at the moment,
+        // so it's tricky.
+
+        if let Some(ref tz) = *tz {
+            let date = tz.to_zoned(LocalDateTime::at(self.0 as i64));
+
+            let datestamp = if date.year() == current_year {
+                date_and_time.format(&date, time)
+            }
+            else {
+                date_and_year.format(&date, time)
+            };
+
+            TextCell::paint(colours.date, datestamp)
+        }
+        else {
+            let date = LocalDateTime::at(self.0 as i64);
+
+            let datestamp = if date.year() == current_year {
+                date_and_time.format(&date, time)
+            }
+            else {
+                date_and_year.format(&date, time)
+            };
+
+            TextCell::paint(colours.date, datestamp)
+        }
+    }
+}

+ 90 - 0
src/output/render/users.rs

@@ -0,0 +1,90 @@
+use users::Users;
+
+use fs::fields as f;
+use output::colours::Colours;
+use output::cell::TextCell;
+
+
+impl f::User {
+    pub fn render(&self, colours: &Colours, users: &Users) -> TextCell {
+        let user_name = match users.get_user_by_uid(self.0) {
+            Some(user)  => user.name().to_owned(),
+            None        => self.0.to_string(),
+        };
+
+        let style = if users.get_current_uid() == self.0 { colours.users.user_you }
+                                                    else { colours.users.user_someone_else };
+        TextCell::paint(style, user_name)
+    }
+}
+
+#[cfg(test)]
+#[allow(unused_results)]
+pub mod test {
+    use output::details::Details;
+
+    use fs::fields as f;
+    use output::cell::TextCell;
+
+    use users::User;
+    use users::mock::MockUsers;
+    use ansi_term::Colour::*;
+
+    #[test]
+    fn named() {
+        let mut details = Details::default();
+        details.colours.users.user_you = Red.bold();
+
+        let mut users = MockUsers::with_current_uid(1000);
+        users.add_user(User::new(1000, "enoch", 100));
+
+        let user = f::User(1000);
+        let expected = TextCell::paint_str(Red.bold(), "enoch");
+        assert_eq!(expected, user.render(&details.colours, &users))
+    }
+
+    #[test]
+    fn unnamed() {
+        let mut details = Details::default();
+        details.colours.users.user_you = Cyan.bold();
+
+        let users = MockUsers::with_current_uid(1000);
+
+        let user = f::User(1000);
+        let expected = TextCell::paint_str(Cyan.bold(), "1000");
+        assert_eq!(expected, user.render(&details.colours, &users));
+    }
+
+    #[test]
+    fn different_named() {
+        let mut details = Details::default();
+        details.colours.users.user_someone_else = Green.bold();
+
+        let mut users = MockUsers::with_current_uid(0);
+        users.add_user(User::new(1000, "enoch", 100));
+
+        let user = f::User(1000);
+        let expected = TextCell::paint_str(Green.bold(), "enoch");
+        assert_eq!(expected, user.render(&details.colours, &users));
+    }
+
+    #[test]
+    fn different_unnamed() {
+        let mut details = Details::default();
+        details.colours.users.user_someone_else = Red.normal();
+
+        let user = f::User(1000);
+        let expected = TextCell::paint_str(Red.normal(), "1000");
+        assert_eq!(expected, user.render(&details.colours, &MockUsers::with_current_uid(0)));
+    }
+
+    #[test]
+    fn overflow() {
+        let mut details = Details::default();
+        details.colours.users.user_someone_else = Blue.underline();
+
+        let user = f::User(2_147_483_648);
+        let expected = TextCell::paint_str(Blue.underline(), "2147483648");
+        assert_eq!(expected, user.render(&details.colours, &MockUsers::with_current_uid(0)));
+    }
+}