Merge branch 'split-details'

This commit is contained in:
Benjamin Sago 2017-05-30 14:58:14 +01:00
commit bd860b8fab
14 changed files with 863 additions and 472 deletions

View File

@ -69,6 +69,15 @@ pub struct Permissions {
pub other_execute: bool, 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 files number of hard links on the filesystem. /// A files 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 /// This is what ls does as well. Without it, the devices will just have
/// file sizes of zero. /// file sizes of zero.
DeviceIDs(DeviceIDs),
}
/// The major and minor device IDs that gets displayed for device files.
/// ///
/// You can see what these device numbers mean: /// You can see what these device numbers mean:
/// - http://www.lanana.org/docs/device-list/ /// - http://www.lanana.org/docs/device-list/
/// - http://www.lanana.org/docs/device-list/devices-2.6+.txt /// - http://www.lanana.org/docs/device-list/devices-2.6+.txt
DeviceIDs { pub struct DeviceIDs {
major: u8, pub major: u8,
minor: u8, pub minor: u8,
}
} }

View File

@ -10,26 +10,6 @@ use fs::dir::Dir;
use fs::fields as f; use fs::fields as f;
#[allow(trivial_numeric_casts)]
mod modes {
use libc;
pub type Mode = u32;
// The `libc::mode_t` types 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 /// A **File** is a wrapper around one of Rust's Path objects, along with
/// associated data about the file. /// associated data about the file.
/// ///
@ -273,10 +253,10 @@ impl<'dir> File<'dir> {
} }
else if self.is_char_device() || self.is_block_device() { else if self.is_char_device() || self.is_block_device() {
let dev = self.metadata.rdev(); let dev = self.metadata.rdev();
f::Size::DeviceIDs { f::Size::DeviceIDs(f::DeviceIDs {
major: (dev / 256) as u8, major: (dev / 256) as u8,
minor: (dev % 256) as u8, minor: (dev % 256) as u8,
} })
} }
else { else {
f::Size::Some(self.metadata.len()) f::Size::Some(self.metadata.len())
@ -327,11 +307,7 @@ impl<'dir> File<'dir> {
} }
} }
/// This file's permissions, with flags for each bit. /// This files 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.)
pub fn permissions(&self) -> f::Permissions { pub fn permissions(&self) -> f::Permissions {
let bits = self.metadata.permissions().mode(); let bits = self.metadata.permissions().mode();
let has_bit = |bit| { bits & bit == bit }; 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` types 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)] #[cfg(test)]
mod test { mod test {
use super::ext; use super::ext;

View File

@ -80,17 +80,13 @@
use std::io::{Write, Error as IOError, Result as IOResult}; use std::io::{Write, Error as IOError, Result as IOResult};
use std::ops::Add; use std::ops::Add;
use std::path::PathBuf; use std::path::PathBuf;
use std::string::ToString; use std::sync::{Arc, Mutex, MutexGuard};
use std::sync::{Arc, Mutex};
use ansi_term::Style;
use datetime::fmt::DateFormat; use datetime::fmt::DateFormat;
use datetime::{LocalDateTime, DatePiece}; use datetime::{LocalDateTime, DatePiece};
use datetime::TimeZone; use datetime::TimeZone;
use zoneinfo_compiled::{CompiledData, Result as TZResult}; use zoneinfo_compiled::{CompiledData, Result as TZResult};
use unicode_width::UnicodeWidthStr;
use locale; use locale;
use users::{Users, Groups, UsersCache}; use users::{Users, Groups, UsersCache};
@ -99,8 +95,8 @@ use fs::{Dir, File, fields as f};
use fs::feature::xattr::{Attribute, FileAttributes}; use fs::feature::xattr::{Attribute, FileAttributes};
use options::{FileFilter, RecurseOptions}; use options::{FileFilter, RecurseOptions};
use output::colours::Colours; use output::colours::Colours;
use output::column::{Alignment, Column, Columns, SizeFormat}; use output::column::{Alignment, Column, Columns};
use output::cell::{TextCell, TextCellContents, DisplayWidth}; use output::cell::{TextCell, TextCellContents};
use output::tree::TreeTrunk; use output::tree::TreeTrunk;
use output::file_name::{FileName, LinkStyle, Classify}; 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. /// 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. /// 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 /// The year of the current time. This gets used to determine which date
/// format to use. /// format to use.
@ -176,10 +172,17 @@ pub struct Environment<U: Users+Groups> {
users: Mutex<U>, users: Mutex<U>,
} }
impl<U> Environment<U> {
pub fn lock_users(&self) -> MutexGuard<U> {
self.users.lock().unwrap()
}
}
impl Default for Environment<UsersCache> { impl Default for Environment<UsersCache> {
fn default() -> Self { fn default() -> Self {
let tz = determine_time_zone(); use unicode_width::UnicodeWidthStr;
let tz = determine_time_zone();
if let Err(ref e) = tz { if let Err(ref e) = tz {
println!("Unable to determine time zone: {}", e); 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); // Some locales use a three-character wide month name (Jan to Dec);
// others vary between three and four (1月 to 12月). We assume that // 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 // 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 december_width = UnicodeWidthStr::width(&*time.short_month_name(11));
let date_and_time = match december_width { let date_and_time = match december_width {
4 => DateFormat::parse("{2>:D} {4>:M} {2>:h}:{02>:m}").unwrap(), 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 /// A **Table** object gets built up by the view as it lists files and
/// directories. /// directories.
pub struct Table<'a, U: Users+Groups+'a> { pub struct Table<'a, U: 'a> { // where U: Users+Groups
pub rows: Vec<Row>, pub rows: Vec<Row>,
pub columns: &'a [Column], pub columns: &'a [Column],
@ -488,234 +492,32 @@ impl<'a, U: Users+Groups+'a> Table<'a, U> {
.collect() .collect()
} }
fn permissions_plus(&self, file: &File, xattrs: bool) -> f::PermissionsPlus {
f::PermissionsPlus {
file_type: file.type_char(),
permissions: file.permissions(),
xattrs: xattrs,
}
}
fn display(&self, file: &File, column: &Column, xattrs: bool) -> TextCell { fn display(&self, file: &File, column: &Column, xattrs: bool) -> TextCell {
use output::column::TimeType::*; use output::column::TimeType::*;
match *column { match *column {
Column::Permissions => self.render_permissions(file.type_char(), file.permissions(), xattrs), Column::Permissions => self.permissions_plus(file, xattrs).render(&self.opts.colours),
Column::FileSize(fmt) => self.render_size(file.size(), fmt), Column::FileSize(fmt) => file.size().render(&self.opts.colours, fmt, &self.env.numeric),
Column::Timestamp(Modified) => self.render_time(file.modified_time()), 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) => self.render_time(file.created_time()), 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) => self.render_time(file.accessed_time()), 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 => self.render_links(file.links()), Column::HardLinks => file.links().render(&self.opts.colours, &self.env.numeric),
Column::Inode => self.render_inode(file.inode()), Column::Inode => file.inode().render(&self.opts.colours),
Column::Blocks => self.render_blocks(file.blocks()), Column::Blocks => file.blocks().render(&self.opts.colours),
Column::User => self.render_user(file.user()), Column::User => file.user().render(&self.opts.colours, &*self.env.lock_users()),
Column::Group => self.render_group(file.group()), Column::Group => file.group().render(&self.opts.colours, &*self.env.lock_users()),
Column::GitStatus => self.render_git_status(file.git_status()), Column::GitStatus => file.git_status().render(&self.opts.colours),
} }
} }
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("@"));
}
// As these are all ASCII characters, we can guarantee that theyre
// all going to be one character wide, and dont need to compute the
// cells 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()),
};
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;
}
}
TextCell::paint(style, group.name().to_owned())
}
/// Render the table as a vector of Cells, to be displayed on standard output. /// Render the table as a vector of Cells, to be displayed on standard output.
pub fn print_table(self) -> Vec<TextCell> { pub fn print_table(self) -> Vec<TextCell> {
let mut tree_trunk = TreeTrunk::default(); let mut tree_trunk = TreeTrunk::default();
@ -771,209 +573,3 @@ impl<'a, U: Users+Groups+'a> Table<'a, U> {
cells 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));
}
}
}

View File

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

View File

@ -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
src/output/render/git.rs Normal file
View File

@ -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
src/output/render/groups.rs Normal file
View File

@ -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)));
}
}

View File

@ -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());
}
}

View File

@ -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
src/output/render/mod.rs Normal file
View File

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

View File

@ -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 theyre
// all going to be one character wide, and dont need to compute the
// cells 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
src/output/render/size.rs Normal file
View File

@ -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()))
}
}

View File

@ -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)
}
}
}

View File

@ -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)));
}
}