details.rs 22.2 KB
Newer Older
1
use colours::Colours;
N
nwin 已提交
2
use column::{Alignment, Column, Cell};
3
use dir::Dir;
B
Ben S 已提交
4
use feature::Attribute;
B
Ben S 已提交
5
use file::fields as f;
B
Ben S 已提交
6
use file::File;
B
Ben S 已提交
7
use options::{Columns, FileFilter, RecurseOptions, SizeFormat};
8

B
Ben S 已提交
9
use ansi_term::{ANSIString, ANSIStrings, Style};
10

B
Ben S 已提交
11 12 13 14
use datetime::local::{LocalDateTime, DatePiece};
use datetime::format::{DateFormat};
use datetime::zoned::{VariableOffset, TimeZone};

15
use locale;
16

B
Ben S 已提交
17 18
use number_prefix::{binary_prefix, decimal_prefix, Prefixed, Standalone, PrefixNames};

B
Ben S 已提交
19 20 21 22
use users::{OSUsers, Users};
use users::mock::MockUsers;

use super::filename;
B
Ben S 已提交
23

B
Ben S 已提交
24

B
Ben S 已提交
25 26 27 28 29 30 31 32 33 34 35
/// With the **Details** view, the output gets formatted into columns, with
/// each `Column` object showing some piece of information about the file,
/// such as its size, or its permissions.
///
/// To do this, the results have to be written to a table, instead of
/// displaying each file immediately. Then, the width of each column can be
/// calculated based on the individual results, and the fields are padded
/// during output.
///
/// Almost all the heavy lifting is done in a Table object, which handles the
/// columns for each row.
36
#[derive(PartialEq, Debug, Copy, Clone, Default)]
37
pub struct Details {
B
Ben S 已提交
38 39 40 41

    /// A Columns object that says which columns should be included in the
    /// output in the general case. Directories themselves can pick which
    /// columns are *added* to this list, such as the Git column.
42
    pub columns: Columns,
B
Ben S 已提交
43 44 45 46

    /// Whether to recurse through directories with a tree view, and if so,
    /// which options to use. This field is only relevant here if the `tree`
    /// field of the RecurseOptions is `true`.
47
    pub recurse: Option<(RecurseOptions, FileFilter)>,
B
Ben S 已提交
48 49 50 51 52

    /// Whether to show a header line or not.
    pub header: bool,

    /// Whether to show each file's extended attributes.
N
nwin 已提交
53
    pub xattr: bool,
54

B
Ben S 已提交
55 56
    /// The colours to use to display information in the table, including the
    /// colour of the tree view symbols.
57
    pub colours: Colours,
58 59 60 61
}

impl Details {
    pub fn view(&self, dir: Option<&Dir>, files: &[File]) {
B
Ben S 已提交
62 63
        // First, transform the Columns object into a vector of columns for
        // the current directory.
B
Ben S 已提交
64
        let mut table = Table::with_options(self.colours, self.columns.for_dir(dir));
B
Ben S 已提交
65 66
        if self.header { table.add_header() }

B
Ben S 已提交
67
        // Then add files to the table and print it out.
B
Ben S 已提交
68 69 70 71 72 73
        self.add_files_to_table(&mut table, files, 0);
        table.print_table(self.xattr, self.recurse.is_some());
    }

    /// Adds files to the table - recursively, if the `recurse` option
    /// is present.
74
    fn add_files_to_table<U: Users>(&self, table: &mut Table<U>, src: &[File], depth: usize) {
B
Ben S 已提交
75
        for (index, file) in src.iter().enumerate() {
76
            table.add_file(file, depth, index == src.len() - 1);
B
Ben S 已提交
77

B
Ben S 已提交
78 79 80 81
            // There are two types of recursion that exa supports: a tree
            // view, which is dealt with here, and multiple listings, which is
            // dealt with in the main module. So only actually recurse if we
            // are in tree mode - the other case will be dealt with elsewhere.
82
            if let Some((r, filter)) = self.recurse {
B
Ben S 已提交
83 84 85 86
                if r.tree == false || r.is_too_deep(depth) {
                    continue;
                }

B
Ben S 已提交
87 88 89
                // Use the filter to remove unwanted files *before* expanding
                // them, so we don't examine any directories that wouldn't
                // have their contents listed anyway.
B
Ben S 已提交
90 91
                if let Some(ref dir) = file.this {
                    let mut files = dir.files(true);
92
                    filter.transform_files(&mut files);
B
Ben S 已提交
93 94 95
                    self.add_files_to_table(table, &files, depth + 1);
                }
            }
96
        }
B
Ben S 已提交
97 98
    }
}
99

B
Ben S 已提交
100

B
Ben S 已提交
101
struct Row {
B
Ben S 已提交
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122

    /// Vector of cells to display.
    cells:    Vec<Cell>,

    /// This file's name, in coloured output. The name is treated separately
    /// from the other cells, as it never requires padding.
    name:     String,

    /// How many directories deep into the tree structure this is. Directories
    /// on top have depth 0.
    depth:    usize,

    /// Vector of this file's extended attributes, if that feature is active.
    attrs:    Vec<Attribute>,

    /// Whether this is the last entry in the directory. This flag is used
    /// when calculating the tree view.
    last:     bool,

    /// Whether this file is a directory and has any children. Also used when
    /// calculating the tree view.
123
    children: bool,
B
Ben S 已提交
124 125
}

B
Ben S 已提交
126

B
Ben S 已提交
127 128
/// A **Table** object gets built up by the view as it lists files and
/// directories.
129
pub struct Table<U> {
B
Ben S 已提交
130 131 132
    columns:  Vec<Column>,
    rows:     Vec<Row>,

B
Ben S 已提交
133 134
    time:         locale::Time,
    numeric:      locale::Numeric,
B
Ben S 已提交
135
    tz:           VariableOffset,
136
    users:        U,
B
Ben S 已提交
137 138
    colours:      Colours,
    current_year: i64,
B
Ben S 已提交
139 140
}

141 142 143 144 145 146 147
impl Default for Table<MockUsers> {
    fn default() -> Table<MockUsers> {
        Table {
            columns: Columns::default().for_dir(None),
            rows:    Vec::new(),
            time:    locale::Time::english(),
            numeric: locale::Numeric::english(),
B
Ben S 已提交
148
            tz:      VariableOffset::localtime().unwrap(),
149 150 151 152 153 154 155 156
            users:   MockUsers::with_current_uid(0),
            colours: Colours::default(),
            current_year: 1234,
        }
    }
}

impl Table<OSUsers> {
B
Ben S 已提交
157

B
Ben S 已提交
158 159
    /// Create a new, empty Table object, setting the caching fields to their
    /// empty states.
160
    fn with_options(colours: Colours, columns: Vec<Column>) -> Table<OSUsers> {
B
Ben S 已提交
161 162
        Table {
            columns: columns,
B
Ben S 已提交
163 164 165 166
            rows:    Vec::new(),

            time:         locale::Time::load_user_locale().unwrap_or_else(|_| locale::Time::english()),
            numeric:      locale::Numeric::load_user_locale().unwrap_or_else(|_| locale::Numeric::english()),
B
Ben S 已提交
167
            tz:           VariableOffset::localtime().unwrap(),
B
Ben S 已提交
168 169 170
            users:        OSUsers::empty_cache(),
            colours:      colours,
            current_year: LocalDateTime::now().year(),
B
Ben S 已提交
171 172
        }
    }
173 174 175
}

impl<U> Table<U> where U: Users {
176

B
Ben S 已提交
177 178 179
    /// Add a dummy "header" row to the table, which contains the names of all
    /// the columns, underlined. This has dummy data for the cases that aren't
    /// actually used, such as the depth or list of attributes.
B
Ben S 已提交
180 181
    fn add_header(&mut self) {
        let row = Row {
B
Ben S 已提交
182
            depth:    0,
183 184
            cells:    self.columns.iter().map(|c| Cell::paint(self.colours.header, c.header())).collect(),
            name:     self.colours.header.paint("Name").to_string(),
B
Ben S 已提交
185 186
            last:     false,
            attrs:    Vec::new(),
B
Ben S 已提交
187 188 189 190 191 192
            children: false,
        };

        self.rows.push(row);
    }

193 194 195 196 197 198 199 200 201 202 203
    /// Get the cells for the given file, and add the result to the table.
    fn add_file(&mut self, file: &File, depth: usize, last: bool) {
        let row = Row {
            depth:    depth,
            cells:    self.cells_for_file(file),
            name:     filename(file, &self.colours),
            last:     last,
            attrs:    file.xattrs.clone(),
            children: file.this.is_some(),
        };

B
Ben S 已提交
204
        self.rows.push(row);
205 206
    }

B
Ben S 已提交
207 208
    /// Use the list of columns to find which cells should be produced for
    /// this file, per-column.
B
Ben S 已提交
209 210
    fn cells_for_file(&mut self, file: &File) -> Vec<Cell> {
        self.columns.clone().iter()
B
Ben S 已提交
211
                    .map(|c| self.display(file, c))
B
Ben S 已提交
212 213 214
                    .collect()
    }

B
Ben S 已提交
215 216
    fn display(&mut self, file: &File, column: &Column) -> Cell {
        match *column {
B
Ben S 已提交
217 218 219 220 221 222 223 224 225
            Column::Permissions    => self.render_permissions(file.permissions()),
            Column::FileSize(fmt)  => self.render_size(file.size(), fmt),
            Column::Timestamp(t)   => self.render_time(file.timestamp(t)),
            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()),
B
Ben S 已提交
226 227 228
        }
    }

B
Ben S 已提交
229
    fn render_permissions(&self, permissions: f::Permissions) -> Cell {
230 231 232
        let c = self.colours.perms;
        let bit = |bit, chr: &'static str, style: Style| {
            if bit { style.paint(chr) } else { self.colours.punctuation.paint("-") }
B
Ben S 已提交
233 234
        };

235
        let file_type = match permissions.file_type {
B
Ben S 已提交
236 237 238 239 240
            f::Type::File       => self.colours.filetypes.normal.paint("."),
            f::Type::Directory  => self.colours.filetypes.directory.paint("d"),
            f::Type::Pipe       => self.colours.filetypes.special.paint("|"),
            f::Type::Link       => self.colours.filetypes.symlink.paint("l"),
            f::Type::Special    => self.colours.filetypes.special.paint("?"),
241 242
        };

B
Ben S 已提交
243
        let x_colour = if let f::Type::File = permissions.file_type { c.user_execute_file }
244
                                                               else { c.user_execute_other };
245

246
        let mut columns = vec![
247 248 249 250 251 252 253 254 255 256
            file_type,
            bit(permissions.user_read,     "r", c.user_read),
            bit(permissions.user_write,    "w", c.user_write),
            bit(permissions.user_execute,  "x", x_colour),
            bit(permissions.group_read,    "r", c.group_read),
            bit(permissions.group_write,   "w", c.group_write),
            bit(permissions.group_execute, "x", c.group_execute),
            bit(permissions.other_read,    "r", c.other_read),
            bit(permissions.other_write,   "w", c.other_write),
            bit(permissions.other_execute, "x", c.other_execute),
257 258 259 260 261
        ];

        if permissions.attribute {
            columns.push(c.attribute.paint("@"));
        }
262 263

        Cell {
264 265
            text: ANSIStrings(&columns).to_string(),
            length: columns.len(),
266 267 268
        }
    }

B
Ben S 已提交
269
    fn render_links(&self, links: f::Links) -> Cell {
270 271 272
        let style = if links.multiple { self.colours.links.multi_link_file }
                                 else { self.colours.links.normal };

B
Ben S 已提交
273
        Cell::paint(style, &self.numeric.format_int(links.count))
274 275
    }

B
Ben S 已提交
276
    fn render_blocks(&self, blocks: f::Blocks) -> Cell {
277
        match blocks {
B
Ben S 已提交
278 279
            f::Blocks::Some(blocks)  => Cell::paint(self.colours.blocks, &blocks.to_string()),
            f::Blocks::None          => Cell::paint(self.colours.punctuation, "-"),
280 281 282
        }
    }

B
Ben S 已提交
283
    fn render_inode(&self, inode: f::Inode) -> Cell {
284 285 286
        Cell::paint(self.colours.inode, &inode.0.to_string())
    }

B
Ben S 已提交
287 288
    fn render_size(&self, size: f::Size, size_format: SizeFormat) -> Cell {
        if let f::Size::Some(offset) = size {
B
Ben S 已提交
289
            let result = match size_format {
B
Ben S 已提交
290 291 292
                SizeFormat::DecimalBytes  => decimal_prefix(offset as f64),
                SizeFormat::BinaryBytes   => binary_prefix(offset as f64),
                SizeFormat::JustBytes     => return Cell::paint(self.colours.size.numbers, &self.numeric.format_int(offset)),
293 294 295
            };

            match result {
B
Ben S 已提交
296 297
                Standalone(bytes)    => Cell::paint(self.colours.size.numbers, &*bytes.to_string()),
                Prefixed(prefix, n)  => {
B
Ben S 已提交
298
                    let number = if n < 10f64 { self.numeric.format_float(n, 1) } else { self.numeric.format_int(n as isize) };
299 300 301 302 303 304 305 306 307 308 309 310 311 312
                    let symbol = prefix.symbol();

                    Cell {
                        text: ANSIStrings( &[ self.colours.size.numbers.paint(&number[..]), self.colours.size.unit.paint(symbol) ]).to_string(),
                        length: number.len() + symbol.len(),
                    }
                }
            }
        }
        else {
            Cell::paint(self.colours.punctuation, "-")
        }
    }

B
Ben S 已提交
313
    fn render_time(&self, timestamp: f::Time) -> Cell {
B
Ben S 已提交
314
        let date = self.tz.at(LocalDateTime::at(timestamp.0));
315

B
Ben S 已提交
316
        let format = if date.year() == self.current_year {
317 318 319 320 321 322
                DateFormat::parse("{2>:D} {:M} {2>:h}:{02>:m}").unwrap()
            }
            else {
                DateFormat::parse("{2>:D} {:M} {5>:Y}").unwrap()
            };

B
Ben S 已提交
323
        Cell::paint(self.colours.date, &format.format(&date, &self.time))
324 325
    }

B
Ben S 已提交
326
    fn render_git_status(&self, git: f::Git) -> Cell {
327
        Cell {
B
Ben S 已提交
328 329
            text: ANSIStrings(&[ self.render_git_char(git.staged),
                                 self.render_git_char(git.unstaged) ]).to_string(),
330 331 332 333
            length: 2,
        }
    }

B
Ben S 已提交
334 335 336 337 338 339 340 341 342 343 344
    fn render_git_char(&self, status: f::GitStatus) -> ANSIString {
        match status {
            f::GitStatus::NotModified  => self.colours.punctuation.paint("-"),
            f::GitStatus::New          => self.colours.git.new.paint("N"),
            f::GitStatus::Modified     => self.colours.git.modified.paint("M"),
            f::GitStatus::Deleted      => self.colours.git.deleted.paint("D"),
            f::GitStatus::Renamed      => self.colours.git.renamed.paint("R"),
            f::GitStatus::TypeChange   => self.colours.git.typechange.paint("T"),
        }
    }

B
Ben S 已提交
345
    fn render_user(&mut self, user: f::User) -> Cell {
B
Ben S 已提交
346
        let user_name = match self.users.get_user_by_uid(user.0) {
B
Ben S 已提交
347 348
            Some(user)  => user.name,
            None        => user.0.to_string(),
349 350
        };

B
Ben S 已提交
351
        let style = if self.users.get_current_uid() == user.0 { self.colours.users.user_you }
B
Ben S 已提交
352
                                                         else { self.colours.users.user_someone_else };
353 354 355
        Cell::paint(style, &*user_name)
    }

B
Ben S 已提交
356
    fn render_group(&mut self, group: f::Group) -> Cell {
357 358
        let mut style = self.colours.users.group_not_yours;

B
Ben S 已提交
359
        let group_name = match self.users.get_group_by_gid(group.0) {
360
            Some(group) => {
B
Ben S 已提交
361 362
                let current_uid = self.users.get_current_uid();
                if let Some(current_user) = self.users.get_user_by_uid(current_uid) {
363 364 365 366 367 368 369 370 371 372
                    if current_user.primary_group == group.gid || group.members.contains(&current_user.name) {
                        style = self.colours.users.group_yours;
                    }
                }
                group.name
            },
            None => group.0.to_string(),
        };

        Cell::paint(style, &*group_name)
B
Ben S 已提交
373 374
    }

B
Ben S 已提交
375
    /// Print the table to standard output, consuming it in the process.
B
Ben S 已提交
376
    fn print_table(self, xattr: bool, show_children: bool) {
377 378
        let mut stack = Vec::new();

B
Ben S 已提交
379 380 381
        // Work out the list of column widths by finding the longest cell for
        // each column, then formatting each cell in that column to be the
        // width of that one.
B
Ben S 已提交
382
        let column_widths: Vec<usize> = (0 .. self.columns.len())
B
Ben S 已提交
383 384 385
            .map(|n| self.rows.iter().map(|row| row.cells[n].length).max().unwrap_or(0))
            .collect();

B
Ben S 已提交
386
        for row in self.rows.into_iter() {
B
Ben S 已提交
387 388 389
            for (n, width) in column_widths.iter().enumerate() {
                let padding = width - row.cells[n].length;
                print!("{} ", self.columns[n].alignment().pad_string(&row.cells[n].text, padding));
390 391
            }

B
Ben S 已提交
392 393 394 395
            // A stack tracks which tree characters should be printed. It's
            // necessary to maintain information about the previously-printed
            // lines, as the output will change based on whether the
            // *previous* entry was the last in its directory.
B
Ben S 已提交
396 397 398
            if show_children {
                stack.resize(row.depth + 1, TreePart::Edge);
                stack[row.depth] = if row.last { TreePart::Corner } else { TreePart::Edge };
399 400

                for i in 1 .. row.depth + 1 {
B
Ben S 已提交
401
                    print!("{}", self.colours.punctuation.paint(stack[i].ascii_art()));
402 403 404
                }

                if row.children {
B
Ben S 已提交
405
                    stack[row.depth] = if row.last { TreePart::Blank } else { TreePart::Line };
406 407
                }

B
Ben S 已提交
408 409
                // If any tree characters have been printed, then add an extra
                // space, which makes the output look much better.
410 411 412 413 414
                if row.depth != 0 {
                    print!(" ");
                }
            }

B
Ben S 已提交
415
            // Print the name without worrying about padding.
416
            print!("{}\n", row.name);
417

B
Ben S 已提交
418
            if xattr {
N
nwin 已提交
419 420 421 422 423 424 425 426 427
                let width = row.attrs.iter().map(|a| a.name().len()).max().unwrap_or(0);
                for attr in row.attrs.iter() {
                    let name = attr.name();
                    println!("{}\t{}",
                        Alignment::Left.pad_string(name, width - name.len()),
                        attr.size()
                    )
                }
            }
428 429
        }
    }
B
Ben S 已提交
430
}
431

B
Ben S 已提交
432

B
Ben S 已提交
433 434
#[derive(PartialEq, Debug, Clone)]
enum TreePart {
B
Ben S 已提交
435 436

    /// Rightmost column, *not* the last in the directory.
B
Ben S 已提交
437
    Edge,
B
Ben S 已提交
438 439 440 441 442

    /// Not the rightmost column, and the directory has not finished yet.
    Line,

    /// Rightmost column, and the last in the directory.
B
Ben S 已提交
443
    Corner,
B
Ben S 已提交
444 445

    /// Not the rightmost column, and the directory *has* finished.
B
Ben S 已提交
446 447
    Blank,
}
448

B
Ben S 已提交
449 450 451
impl TreePart {
    fn ascii_art(&self) -> &'static str {
        match *self {
B
Ben S 已提交
452 453 454 455
            TreePart::Edge    => "├──",
            TreePart::Line    => "│  ",
            TreePart::Corner  => "└──",
            TreePart::Blank   => "   ",
456 457 458
        }
    }
}
459 460 461 462 463 464 465 466 467 468 469 470 471


#[cfg(test)]
pub mod test {
    pub use super::Table;
    pub use file::File;
    pub use file::fields as f;

    pub use column::{Cell, Column};

    pub use users::{User, Group, uid_t, gid_t};
    pub use users::mock::MockUsers;

B
Ben S 已提交
472
    pub use ansi_term::Style;
473 474 475 476 477 478 479 480 481 482 483 484 485 486
    pub use ansi_term::Colour::*;

    pub fn newser(uid: uid_t, name: &str, group: gid_t) -> User {
        User {
            uid: uid,
            name: name.to_string(),
            primary_group: group,
            home_dir: String::new(),
            shell: String::new(),
        }
    }

    // These tests create a new, default Table object, then fill in the
    // expected style in a certain way. This means we can check that the
B
Ben S 已提交
487
    // right style is being used, as otherwise, it would just be plain.
488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624
    //
    // Doing things with fields is way easier than having to fake the entire
    // Metadata struct, which is what I was doing before!

    mod users {
        use super::*;

        #[test]
        fn named() {
            let mut table = Table::default();
            table.colours.users.user_you = Red.bold();

            let mut users = MockUsers::with_current_uid(1000);
            users.add_user(newser(1000, "enoch", 100));
            table.users = users;

            let user = f::User(1000);
            let expected = Cell::paint(Red.bold(), "enoch");
            assert_eq!(expected, table.render_user(user))
        }

        #[test]
        fn unnamed() {
            let mut table = Table::default();
            table.colours.users.user_you = Cyan.bold();

            let users = MockUsers::with_current_uid(1000);
            table.users = users;

            let user = f::User(1000);
            let expected = Cell::paint(Cyan.bold(), "1000");
            assert_eq!(expected, table.render_user(user));
        }

        #[test]
        fn different_named() {
            let mut table = Table::default();
            table.colours.users.user_someone_else = Green.bold();
            table.users.add_user(newser(1000, "enoch", 100));

            let user = f::User(1000);
            let expected = Cell::paint(Green.bold(), "enoch");
            assert_eq!(expected, table.render_user(user));
        }

        #[test]
        fn different_unnamed() {
            let mut table = Table::default();
            table.colours.users.user_someone_else = Red.normal();

            let user = f::User(1000);
            let expected = Cell::paint(Red.normal(), "1000");
            assert_eq!(expected, table.render_user(user));
        }

        #[test]
        fn overflow() {
            let mut table = Table::default();
            table.colours.users.user_someone_else = Blue.underline();

            let user = f::User(2_147_483_648);
            let expected = Cell::paint(Blue.underline(), "2147483648");
            assert_eq!(expected, table.render_user(user));
        }
    }

    mod groups {
        use super::*;

        #[test]
        fn named() {
            let mut table = Table::default();
            table.colours.users.group_not_yours = Fixed(101).normal();

            let mut users = MockUsers::with_current_uid(1000);
            users.add_group(Group { gid: 100, name: "folk".to_string(), members: vec![] });
            table.users = users;

            let group = f::Group(100);
            let expected = Cell::paint(Fixed(101).normal(), "folk");
            assert_eq!(expected, table.render_group(group))
        }

        #[test]
        fn unnamed() {
            let mut table = Table::default();
            table.colours.users.group_not_yours = Fixed(87).normal();

            let users = MockUsers::with_current_uid(1000);
            table.users = users;

            let group = f::Group(100);
            let expected = Cell::paint(Fixed(87).normal(), "100");
            assert_eq!(expected, table.render_group(group));
        }

        #[test]
        fn primary() {
            let mut table = Table::default();
            table.colours.users.group_yours = Fixed(64).normal();

            let mut users = MockUsers::with_current_uid(2);
            users.add_user(newser(2, "eve", 100));
            users.add_group(Group { gid: 100, name: "folk".to_string(), members: vec![] });
            table.users = users;

            let group = f::Group(100);
            let expected = Cell::paint(Fixed(64).normal(), "folk");
            assert_eq!(expected, table.render_group(group))
        }

        #[test]
        fn secondary() {
            let mut table = Table::default();
            table.colours.users.group_yours = Fixed(31).normal();

            let mut users = MockUsers::with_current_uid(2);
            users.add_user(newser(2, "eve", 666));
            users.add_group(Group { gid: 100, name: "folk".to_string(), members: vec![ "eve".to_string() ] });
            table.users = users;

            let group = f::Group(100);
            let expected = Cell::paint(Fixed(31).normal(), "folk");
            assert_eq!(expected, table.render_group(group))
        }

        #[test]
        fn overflow() {
            let mut table = Table::default();
            table.colours.users.group_not_yours = Blue.underline();

            let group = f::Group(2_147_483_648);
            let expected = Cell::paint(Blue.underline(), "2147483648");
            assert_eq!(expected, table.render_group(group));
        }
    }
}