table.rs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. use std::cmp::max;
  2. use std::fmt;
  3. use std::ops::Deref;
  4. use std::sync::{Mutex, MutexGuard};
  5. use datetime::TimeZone;
  6. use zoneinfo_compiled::{CompiledData, Result as TZResult};
  7. use locale;
  8. use users::UsersCache;
  9. use style::Colours;
  10. use output::cell::TextCell;
  11. use output::time::TimeFormat;
  12. use fs::{File, Dir, fields as f};
  13. use fs::feature::git::GitCache;
  14. /// Options for displaying a table.
  15. pub struct Options {
  16. pub env: Environment,
  17. pub size_format: SizeFormat,
  18. pub time_format: TimeFormat,
  19. pub extra_columns: Columns,
  20. }
  21. // I had to make other types derive Debug,
  22. // and Mutex<UsersCache> is not that!
  23. impl fmt::Debug for Options {
  24. fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
  25. write!(f, "<table options>")
  26. }
  27. }
  28. /// Extra columns to display in the table.
  29. #[derive(PartialEq, Debug)]
  30. pub struct Columns {
  31. /// At least one of these timestamps will be shown.
  32. pub time_types: TimeTypes,
  33. // The rest are just on/off
  34. pub inode: bool,
  35. pub links: bool,
  36. pub blocks: bool,
  37. pub group: bool,
  38. pub git: bool,
  39. }
  40. impl Columns {
  41. pub fn collect(&self, actually_enable_git: bool) -> Vec<Column> {
  42. let mut columns = Vec::with_capacity(4);
  43. if self.inode {
  44. columns.push(Column::Inode);
  45. }
  46. columns.push(Column::Permissions);
  47. if self.links {
  48. columns.push(Column::HardLinks);
  49. }
  50. columns.push(Column::FileSize);
  51. if self.blocks {
  52. columns.push(Column::Blocks);
  53. }
  54. columns.push(Column::User);
  55. if self.group {
  56. columns.push(Column::Group);
  57. }
  58. if self.time_types.modified {
  59. columns.push(Column::Timestamp(TimeType::Modified));
  60. }
  61. if self.time_types.created {
  62. columns.push(Column::Timestamp(TimeType::Created));
  63. }
  64. if self.time_types.accessed {
  65. columns.push(Column::Timestamp(TimeType::Accessed));
  66. }
  67. if cfg!(feature="git") && self.git && actually_enable_git {
  68. columns.push(Column::GitStatus);
  69. }
  70. columns
  71. }
  72. }
  73. /// A table contains these.
  74. #[derive(Debug)]
  75. pub enum Column {
  76. Permissions,
  77. FileSize,
  78. Timestamp(TimeType),
  79. Blocks,
  80. User,
  81. Group,
  82. HardLinks,
  83. Inode,
  84. GitStatus,
  85. }
  86. /// Each column can pick its own **Alignment**. Usually, numbers are
  87. /// right-aligned, and text is left-aligned.
  88. #[derive(Copy, Clone)]
  89. pub enum Alignment {
  90. Left, Right,
  91. }
  92. impl Column {
  93. /// Get the alignment this column should use.
  94. pub fn alignment(&self) -> Alignment {
  95. match *self {
  96. Column::FileSize
  97. | Column::HardLinks
  98. | Column::Inode
  99. | Column::Blocks
  100. | Column::GitStatus => Alignment::Right,
  101. _ => Alignment::Left,
  102. }
  103. }
  104. /// Get the text that should be printed at the top, when the user elects
  105. /// to have a header row printed.
  106. pub fn header(&self) -> &'static str {
  107. match *self {
  108. Column::Permissions => "Permissions",
  109. Column::FileSize => "Size",
  110. Column::Timestamp(t) => t.header(),
  111. Column::Blocks => "Blocks",
  112. Column::User => "User",
  113. Column::Group => "Group",
  114. Column::HardLinks => "Links",
  115. Column::Inode => "inode",
  116. Column::GitStatus => "Git",
  117. }
  118. }
  119. }
  120. /// Formatting options for file sizes.
  121. #[derive(PartialEq, Debug, Copy, Clone)]
  122. pub enum SizeFormat {
  123. /// Format the file size using **decimal** prefixes, such as “kilo”,
  124. /// “mega”, or “giga”.
  125. DecimalBytes,
  126. /// Format the file size using **binary** prefixes, such as “kibi”,
  127. /// “mebi”, or “gibi”.
  128. BinaryBytes,
  129. /// Do no formatting and just display the size as a number of bytes.
  130. JustBytes,
  131. }
  132. impl Default for SizeFormat {
  133. fn default() -> SizeFormat {
  134. SizeFormat::DecimalBytes
  135. }
  136. }
  137. /// The types of a file’s time fields. These three fields are standard
  138. /// across most (all?) operating systems.
  139. #[derive(PartialEq, Debug, Copy, Clone)]
  140. pub enum TimeType {
  141. /// The file’s accessed time (`st_atime`).
  142. Accessed,
  143. /// The file’s modified time (`st_mtime`).
  144. Modified,
  145. /// The file’s creation time (`st_ctime`).
  146. Created,
  147. }
  148. impl TimeType {
  149. /// Returns the text to use for a column’s heading in the columns output.
  150. pub fn header(&self) -> &'static str {
  151. match *self {
  152. TimeType::Accessed => "Date Accessed",
  153. TimeType::Modified => "Date Modified",
  154. TimeType::Created => "Date Created",
  155. }
  156. }
  157. }
  158. /// Fields for which of a file’s time fields should be displayed in the
  159. /// columns output.
  160. ///
  161. /// There should always be at least one of these--there's no way to disable
  162. /// the time columns entirely (yet).
  163. #[derive(PartialEq, Debug, Copy, Clone)]
  164. pub struct TimeTypes {
  165. pub accessed: bool,
  166. pub modified: bool,
  167. pub created: bool,
  168. }
  169. impl Default for TimeTypes {
  170. /// By default, display just the ‘modified’ time. This is the most
  171. /// common option, which is why it has this shorthand.
  172. fn default() -> TimeTypes {
  173. TimeTypes { accessed: false, modified: true, created: false }
  174. }
  175. }
  176. /// The **environment** struct contains any data that could change between
  177. /// running instances of exa, depending on the user's computer's configuration.
  178. ///
  179. /// Any environment field should be able to be mocked up for test runs.
  180. pub struct Environment {
  181. /// Localisation rules for formatting numbers.
  182. numeric: locale::Numeric,
  183. /// The computer's current time zone. This gets used to determine how to
  184. /// offset files' timestamps.
  185. tz: Option<TimeZone>,
  186. /// Mapping cache of user IDs to usernames.
  187. users: Mutex<UsersCache>,
  188. }
  189. impl Environment {
  190. pub fn lock_users(&self) -> MutexGuard<UsersCache> {
  191. self.users.lock().unwrap()
  192. }
  193. pub fn load_all() -> Self {
  194. let tz = match determine_time_zone() {
  195. Ok(t) => Some(t),
  196. Err(ref e) => {
  197. println!("Unable to determine time zone: {}", e);
  198. None
  199. }
  200. };
  201. let numeric = locale::Numeric::load_user_locale()
  202. .unwrap_or_else(|_| locale::Numeric::english());
  203. let users = Mutex::new(UsersCache::new());
  204. Environment { tz, numeric, users }
  205. }
  206. }
  207. fn determine_time_zone() -> TZResult<TimeZone> {
  208. TimeZone::from_file("/etc/localtime")
  209. }
  210. pub struct Table<'a> {
  211. columns: Vec<Column>,
  212. colours: &'a Colours,
  213. env: &'a Environment,
  214. widths: TableWidths,
  215. time_format: &'a TimeFormat,
  216. size_format: SizeFormat,
  217. git: Option<&'a GitCache>,
  218. }
  219. #[derive(Clone)]
  220. pub struct Row {
  221. cells: Vec<TextCell>,
  222. }
  223. impl<'a, 'f> Table<'a> {
  224. pub fn new(options: &'a Options, dir: Option<&'a Dir>, git: Option<&'a GitCache>, colours: &'a Colours) -> Table<'a> {
  225. let has_git = if let (Some(g), Some(d)) = (git, dir) { g.has_anything_for(&d.path) } else { false };
  226. let columns = options.extra_columns.collect(has_git);
  227. let widths = TableWidths::zero(columns.len());
  228. Table {
  229. colours, widths, columns, git,
  230. env: &options.env,
  231. time_format: &options.time_format,
  232. size_format: options.size_format,
  233. }
  234. }
  235. pub fn widths(&self) -> &TableWidths {
  236. &self.widths
  237. }
  238. pub fn header_row(&self) -> Row {
  239. let cells = self.columns.iter()
  240. .map(|c| TextCell::paint_str(self.colours.header, c.header()))
  241. .collect();
  242. Row { cells }
  243. }
  244. pub fn row_for_file(&self, file: &File, xattrs: bool) -> Row {
  245. let cells = self.columns.iter()
  246. .map(|c| self.display(file, c, xattrs))
  247. .collect();
  248. Row { cells }
  249. }
  250. pub fn add_widths(&mut self, row: &Row) {
  251. self.widths.add_widths(row)
  252. }
  253. fn permissions_plus(&self, file: &File, xattrs: bool) -> f::PermissionsPlus {
  254. f::PermissionsPlus {
  255. file_type: file.type_char(),
  256. permissions: file.permissions(),
  257. xattrs: xattrs,
  258. }
  259. }
  260. fn display(&self, file: &File, column: &Column, xattrs: bool) -> TextCell {
  261. use output::table::TimeType::*;
  262. match *column {
  263. Column::Permissions => self.permissions_plus(file, xattrs).render(self.colours),
  264. Column::FileSize => file.size().render(self.colours, self.size_format, &self.env.numeric),
  265. Column::HardLinks => file.links().render(self.colours, &self.env.numeric),
  266. Column::Inode => file.inode().render(self.colours.inode),
  267. Column::Blocks => file.blocks().render(self.colours),
  268. Column::User => file.user().render(self.colours, &*self.env.lock_users()),
  269. Column::Group => file.group().render(self.colours, &*self.env.lock_users()),
  270. Column::GitStatus => self.git_status(file).render(self.colours),
  271. Column::Timestamp(Modified) => file.modified_time().render(self.colours.date, &self.env.tz, &self.time_format),
  272. Column::Timestamp(Created) => file.created_time() .render(self.colours.date, &self.env.tz, &self.time_format),
  273. Column::Timestamp(Accessed) => file.accessed_time().render(self.colours.date, &self.env.tz, &self.time_format),
  274. }
  275. }
  276. fn git_status(&self, file: &File) -> f::Git {
  277. debug!("Getting Git status for file {:?}", file.path);
  278. self.git
  279. .map(|g| g.get(&file.path, file.is_directory()))
  280. .unwrap_or_default()
  281. }
  282. pub fn render(&self, row: Row) -> TextCell {
  283. let mut cell = TextCell::default();
  284. for (n, (this_cell, width)) in row.cells.into_iter().zip(self.widths.iter()).enumerate() {
  285. let padding = width - *this_cell.width;
  286. match self.columns[n].alignment() {
  287. Alignment::Left => { cell.append(this_cell); cell.add_spaces(padding); }
  288. Alignment::Right => { cell.add_spaces(padding); cell.append(this_cell); }
  289. }
  290. cell.add_spaces(1);
  291. }
  292. cell
  293. }
  294. }
  295. pub struct TableWidths(Vec<usize>);
  296. impl Deref for TableWidths {
  297. type Target = [usize];
  298. fn deref<'a>(&'a self) -> &'a Self::Target {
  299. &self.0
  300. }
  301. }
  302. impl TableWidths {
  303. pub fn zero(count: usize) -> TableWidths {
  304. TableWidths(vec![ 0; count ])
  305. }
  306. pub fn add_widths(&mut self, row: &Row) {
  307. for (old_width, cell) in self.0.iter_mut().zip(row.cells.iter()) {
  308. *old_width = max(*old_width, *cell.width);
  309. }
  310. }
  311. pub fn total(&self) -> usize {
  312. self.0.len() + self.0.iter().sum::<usize>()
  313. }
  314. }