use std::cmp::max; use std::env; use std::fmt; use std::ops::Deref; use std::sync::{Mutex, MutexGuard}; use datetime::TimeZone; use zoneinfo_compiled::{CompiledData, Result as TZResult}; use log::*; use users::UsersCache; use crate::fs::{File, fields as f}; use crate::fs::feature::git::GitCache; use crate::output::cell::TextCell; use crate::output::render::TimeRender; use crate::output::time::TimeFormat; use crate::style::Colours; /// Options for displaying a table. pub struct Options { pub env: Environment, pub size_format: SizeFormat, pub time_format: TimeFormat, pub columns: Columns, } // I had to make other types derive Debug, // and Mutex is not that! impl fmt::Debug for Options { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { write!(f, "Table({:#?})", self.columns) } } /// Extra columns to display in the table. #[derive(PartialEq, Debug, Copy, Clone)] pub struct Columns { /// At least one of these timestamps will be shown. pub time_types: TimeTypes, // The rest are just on/off pub inode: bool, pub links: bool, pub blocks: bool, pub group: bool, pub git: bool, pub octal: bool, // Defaults to true: pub permissions: bool, pub filesize: bool, pub user: bool, } impl Columns { pub fn collect(&self, actually_enable_git: bool) -> Vec { let mut columns = Vec::with_capacity(4); if self.inode { columns.push(Column::Inode); } if self.octal { columns.push(Column::Octal); } if self.permissions { columns.push(Column::Permissions); } if self.links { columns.push(Column::HardLinks); } if self.filesize { columns.push(Column::FileSize); } if self.blocks { columns.push(Column::Blocks); } if self.user { columns.push(Column::User); } if self.group { columns.push(Column::Group); } if self.time_types.modified { columns.push(Column::Timestamp(TimeType::Modified)); } if self.time_types.changed { columns.push(Column::Timestamp(TimeType::Changed)); } if self.time_types.created { columns.push(Column::Timestamp(TimeType::Created)); } if self.time_types.accessed { columns.push(Column::Timestamp(TimeType::Accessed)); } if cfg!(feature = "git") && self.git && actually_enable_git { columns.push(Column::GitStatus); } columns } } /// A table contains these. #[derive(Debug, Copy, Clone)] pub enum Column { Permissions, FileSize, Timestamp(TimeType), Blocks, User, Group, HardLinks, Inode, GitStatus, Octal, } /// Each column can pick its own **Alignment**. Usually, numbers are /// right-aligned, and text is left-aligned. #[derive(Copy, Clone)] pub enum Alignment { Left, Right, } impl Column { /// Get the alignment this column should use. pub fn alignment(self) -> Alignment { match self { Self::FileSize | Self::HardLinks | Self::Inode | Self::Blocks | Self::GitStatus => Alignment::Right, _ => Alignment::Left, } } /// Get the text that should be printed at the top, when the user elects /// to have a header row printed. pub fn header(self) -> &'static str { match self { Self::Permissions => "Permissions", Self::FileSize => "Size", Self::Timestamp(t) => t.header(), Self::Blocks => "Blocks", Self::User => "User", Self::Group => "Group", Self::HardLinks => "Links", Self::Inode => "inode", Self::GitStatus => "Git", Self::Octal => "Octal", } } } /// Formatting options for file sizes. #[derive(PartialEq, Debug, Copy, Clone)] pub enum SizeFormat { /// Format the file size using **decimal** prefixes, such as “kilo”, /// “mega”, or “giga”. DecimalBytes, /// Format the file size using **binary** prefixes, such as “kibi”, /// “mebi”, or “gibi”. BinaryBytes, /// Do no formatting and just display the size as a number of bytes. JustBytes, } impl Default for SizeFormat { fn default() -> Self { Self::DecimalBytes } } /// The types of a file’s time fields. These three fields are standard /// across most (all?) operating systems. #[derive(PartialEq, Debug, Copy, Clone)] pub enum TimeType { /// The file’s modified time (`st_mtime`). Modified, /// The file’s changed time (`st_ctime`) Changed, /// The file’s accessed time (`st_atime`). Accessed, /// The file’s creation time (`btime` or `birthtime`). Created, } impl TimeType { /// Returns the text to use for a column’s heading in the columns output. pub fn header(self) -> &'static str { match self { Self::Modified => "Date Modified", Self::Changed => "Date Changed", Self::Accessed => "Date Accessed", Self::Created => "Date Created", } } } /// Fields for which of a file’s time fields should be displayed in the /// columns output. /// /// There should always be at least one of these — there’s no way to disable /// the time columns entirely (yet). #[derive(PartialEq, Debug, Copy, Clone)] pub struct TimeTypes { pub modified: bool, pub changed: bool, pub accessed: bool, pub created: bool, } impl Default for TimeTypes { /// By default, display just the ‘modified’ time. This is the most /// common option, which is why it has this shorthand. fn default() -> Self { Self { modified: true, changed: false, accessed: false, created: false, } } } /// The **environment** struct contains any data that could change between /// 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 { /// Localisation rules for formatting numbers. numeric: locale::Numeric, /// The computer’s current time zone. This gets used to determine how to /// offset files’ timestamps. tz: Option, /// Mapping cache of user IDs to usernames. users: Mutex, } impl Environment { pub fn lock_users(&self) -> MutexGuard { self.users.lock().unwrap() } pub fn load_all() -> Self { let tz = match determine_time_zone() { Ok(t) => { Some(t) } Err(ref e) => { println!("Unable to determine time zone: {}", e); None } }; let numeric = locale::Numeric::load_user_locale() .unwrap_or_else(|_| locale::Numeric::english()); let users = Mutex::new(UsersCache::new()); Self { tz, numeric, users } } } fn determine_time_zone() -> TZResult { if let Ok(file) = env::var("TZ") { TimeZone::from_file(format!("/usr/share/zoneinfo/{}", file)) } else { TimeZone::from_file("/etc/localtime") } } pub struct Table<'a> { columns: Vec, colours: &'a Colours, env: &'a Environment, widths: TableWidths, time_format: &'a TimeFormat, size_format: SizeFormat, git: Option<&'a GitCache>, } #[derive(Clone)] pub struct Row { cells: Vec, } impl<'a, 'f> Table<'a> { pub fn new(options: &'a Options, git: Option<&'a GitCache>, colours: &'a Colours) -> Table<'a> { let columns = options.columns.collect(git.is_some()); let widths = TableWidths::zero(columns.len()); Table { colours, widths, columns, git, env: &options.env, time_format: &options.time_format, size_format: options.size_format, } } pub fn widths(&self) -> &TableWidths { &self.widths } pub fn header_row(&self) -> Row { let cells = self.columns.iter() .map(|c| TextCell::paint_str(self.colours.header, c.header())) .collect(); Row { cells } } pub fn row_for_file(&self, file: &File, xattrs: bool) -> Row { let cells = self.columns.iter() .map(|c| self.display(file, *c, xattrs)) .collect(); Row { cells } } pub fn add_widths(&mut self, row: &Row) { self.widths.add_widths(row) } fn permissions_plus(&self, file: &File, xattrs: bool) -> f::PermissionsPlus { f::PermissionsPlus { file_type: file.type_char(), permissions: file.permissions(), xattrs, } } fn octal_permissions(&self, file: &File) -> f::OctalPermissions { f::OctalPermissions { permissions: file.permissions(), } } fn display(&self, file: &File, column: Column, xattrs: bool) -> TextCell { match column { Column::Permissions => { self.permissions_plus(file, xattrs).render(self.colours) } Column::FileSize => { file.size().render(self.colours, self.size_format, &self.env.numeric) } Column::HardLinks => { file.links().render(self.colours, &self.env.numeric) } Column::Inode => { file.inode().render(self.colours.inode) } Column::Blocks => { file.blocks().render(self.colours) } Column::User => { file.user().render(self.colours, &*self.env.lock_users()) } Column::Group => { file.group().render(self.colours, &*self.env.lock_users()) } Column::GitStatus => { self.git_status(file).render(self.colours) } Column::Octal => { self.octal_permissions(file).render(self.colours.octal) } Column::Timestamp(TimeType::Modified) => { file.modified_time().render(self.colours.date, &self.env.tz, self.time_format) } Column::Timestamp(TimeType::Changed) => { file.changed_time().render(self.colours.date, &self.env.tz, self.time_format) } Column::Timestamp(TimeType::Created) => { file.created_time().render(self.colours.date, &self.env.tz, self.time_format) } Column::Timestamp(TimeType::Accessed) => { file.accessed_time().render(self.colours.date, &self.env.tz, self.time_format) } } } fn git_status(&self, file: &File) -> f::Git { debug!("Getting Git status for file {:?}", file.path); self.git .map(|g| g.get(&file.path, file.is_directory())) .unwrap_or_default() } pub fn render(&self, row: Row) -> TextCell { let mut cell = TextCell::default(); let iter = row.cells.into_iter() .zip(self.widths.iter()) .enumerate(); for (n, (this_cell, width)) in iter { let padding = width - *this_cell.width; match self.columns[n].alignment() { Alignment::Left => { cell.append(this_cell); cell.add_spaces(padding); } Alignment::Right => { cell.add_spaces(padding); cell.append(this_cell); } } cell.add_spaces(1); } cell } } pub struct TableWidths(Vec); impl Deref for TableWidths { type Target = [usize]; fn deref(&self) -> &Self::Target { &self.0 } } impl TableWidths { pub fn zero(count: usize) -> Self { Self(vec![0; count]) } pub fn add_widths(&mut self, row: &Row) { for (old_width, cell) in self.0.iter_mut().zip(row.cells.iter()) { *old_width = max(*old_width, *cell.width); } } pub fn total(&self) -> usize { self.0.len() + self.0.iter().sum::() } }