1 use std::cmp::max;
2 use std::env;
3 use std::ops::Deref;
4 use std::sync::{Mutex, MutexGuard};
5 
6 use datetime::TimeZone;
7 use zoneinfo_compiled::{CompiledData, Result as TZResult};
8 
9 use lazy_static::lazy_static;
10 use log::*;
11 use users::UsersCache;
12 
13 use crate::fs::{File, fields as f};
14 use crate::fs::feature::git::GitCache;
15 use crate::output::cell::TextCell;
16 use crate::output::render::TimeRender;
17 use crate::output::time::TimeFormat;
18 use crate::theme::Theme;
19 
20 
21 /// Options for displaying a table.
22 #[derive(PartialEq, Debug)]
23 pub struct Options {
24     pub size_format: SizeFormat,
25     pub time_format: TimeFormat,
26     pub user_format: UserFormat,
27     pub columns: Columns,
28 }
29 
30 /// Extra columns to display in the table.
31 #[derive(PartialEq, Debug, Copy, Clone)]
32 pub struct Columns {
33 
34     /// At least one of these timestamps will be shown.
35     pub time_types: TimeTypes,
36 
37     // The rest are just on/off
38     pub inode: bool,
39     pub links: bool,
40     pub blocks: bool,
41     pub group: bool,
42     pub git: bool,
43     pub octal: bool,
44 
45     // Defaults to true:
46     pub permissions: bool,
47     pub filesize: bool,
48     pub user: bool,
49 }
50 
51 impl Columns {
collect(&self, actually_enable_git: bool) -> Vec<Column>52     pub fn collect(&self, actually_enable_git: bool) -> Vec<Column> {
53         let mut columns = Vec::with_capacity(4);
54 
55         if self.inode {
56             columns.push(Column::Inode);
57         }
58 
59         if self.octal {
60             columns.push(Column::Octal);
61         }
62 
63         if self.permissions {
64             columns.push(Column::Permissions);
65         }
66 
67         if self.links {
68             columns.push(Column::HardLinks);
69         }
70 
71         if self.filesize {
72             columns.push(Column::FileSize);
73         }
74 
75         if self.blocks {
76             columns.push(Column::Blocks);
77         }
78 
79         if self.user {
80             columns.push(Column::User);
81         }
82 
83         if self.group {
84             columns.push(Column::Group);
85         }
86 
87         if self.time_types.modified {
88             columns.push(Column::Timestamp(TimeType::Modified));
89         }
90 
91         if self.time_types.changed {
92             columns.push(Column::Timestamp(TimeType::Changed));
93         }
94 
95         if self.time_types.created {
96             columns.push(Column::Timestamp(TimeType::Created));
97         }
98 
99         if self.time_types.accessed {
100             columns.push(Column::Timestamp(TimeType::Accessed));
101         }
102 
103         if self.git && actually_enable_git {
104             columns.push(Column::GitStatus);
105         }
106 
107         columns
108     }
109 }
110 
111 
112 /// A table contains these.
113 #[derive(Debug, Copy, Clone)]
114 pub enum Column {
115     Permissions,
116     FileSize,
117     Timestamp(TimeType),
118     Blocks,
119     User,
120     Group,
121     HardLinks,
122     Inode,
123     GitStatus,
124     Octal,
125 }
126 
127 /// Each column can pick its own **Alignment**. Usually, numbers are
128 /// right-aligned, and text is left-aligned.
129 #[derive(Copy, Clone)]
130 pub enum Alignment {
131     Left,
132     Right,
133 }
134 
135 impl Column {
136 
137     /// Get the alignment this column should use.
alignment(self) -> Alignment138     pub fn alignment(self) -> Alignment {
139         match self {
140             Self::FileSize   |
141             Self::HardLinks  |
142             Self::Inode      |
143             Self::Blocks     |
144             Self::GitStatus  => Alignment::Right,
145             _                => Alignment::Left,
146         }
147     }
148 
149     /// Get the text that should be printed at the top, when the user elects
150     /// to have a header row printed.
header(self) -> &'static str151     pub fn header(self) -> &'static str {
152         match self {
153             Self::Permissions   => "Permissions",
154             Self::FileSize      => "Size",
155             Self::Timestamp(t)  => t.header(),
156             Self::Blocks        => "Blocks",
157             Self::User          => "User",
158             Self::Group         => "Group",
159             Self::HardLinks     => "Links",
160             Self::Inode         => "inode",
161             Self::GitStatus     => "Git",
162             Self::Octal         => "Octal",
163         }
164     }
165 }
166 
167 
168 /// Formatting options for file sizes.
169 #[derive(PartialEq, Debug, Copy, Clone)]
170 pub enum SizeFormat {
171 
172     /// Format the file size using **decimal** prefixes, such as “kilo”,
173     /// “mega”, or “giga”.
174     DecimalBytes,
175 
176     /// Format the file size using **binary** prefixes, such as “kibi”,
177     /// “mebi”, or “gibi”.
178     BinaryBytes,
179 
180     /// Do no formatting and just display the size as a number of bytes.
181     JustBytes,
182 }
183 
184 /// Formatting options for user and group.
185 #[derive(PartialEq, Debug, Copy, Clone)]
186 pub enum UserFormat {
187     /// The UID / GID
188     Numeric,
189     /// Show the name
190     Name,
191 }
192 
193 impl Default for SizeFormat {
default() -> Self194     fn default() -> Self {
195         Self::DecimalBytes
196     }
197 }
198 
199 
200 /// The types of a file’s time fields. These three fields are standard
201 /// across most (all?) operating systems.
202 #[derive(PartialEq, Debug, Copy, Clone)]
203 pub enum TimeType {
204 
205     /// The file’s modified time (`st_mtime`).
206     Modified,
207 
208     /// The file’s changed time (`st_ctime`)
209     Changed,
210 
211     /// The file’s accessed time (`st_atime`).
212     Accessed,
213 
214     /// The file’s creation time (`btime` or `birthtime`).
215     Created,
216 }
217 
218 impl TimeType {
219 
220     /// Returns the text to use for a column’s heading in the columns output.
header(self) -> &'static str221     pub fn header(self) -> &'static str {
222         match self {
223             Self::Modified  => "Date Modified",
224             Self::Changed   => "Date Changed",
225             Self::Accessed  => "Date Accessed",
226             Self::Created   => "Date Created",
227         }
228     }
229 }
230 
231 
232 /// Fields for which of a file’s time fields should be displayed in the
233 /// columns output.
234 ///
235 /// There should always be at least one of these — there’s no way to disable
236 /// the time columns entirely (yet).
237 #[derive(PartialEq, Debug, Copy, Clone)]
238 #[allow(clippy::struct_excessive_bools)]
239 pub struct TimeTypes {
240     pub modified: bool,
241     pub changed:  bool,
242     pub accessed: bool,
243     pub created:  bool,
244 }
245 
246 impl Default for TimeTypes {
247 
248     /// By default, display just the ‘modified’ time. This is the most
249     /// common option, which is why it has this shorthand.
default() -> Self250     fn default() -> Self {
251         Self {
252             modified: true,
253             changed:  false,
254             accessed: false,
255             created:  false,
256         }
257     }
258 }
259 
260 
261 /// The **environment** struct contains any data that could change between
262 /// running instances of exa, depending on the user’s computer’s configuration.
263 ///
264 /// Any environment field should be able to be mocked up for test runs.
265 pub struct Environment {
266 
267     /// Localisation rules for formatting numbers.
268     numeric: locale::Numeric,
269 
270     /// The computer’s current time zone. This gets used to determine how to
271     /// offset files’ timestamps.
272     tz: Option<TimeZone>,
273 
274     /// Mapping cache of user IDs to usernames.
275     users: Mutex<UsersCache>,
276 }
277 
278 impl Environment {
lock_users(&self) -> MutexGuard<'_, UsersCache>279     pub fn lock_users(&self) -> MutexGuard<'_, UsersCache> {
280         self.users.lock().unwrap()
281     }
282 
load_all() -> Self283     fn load_all() -> Self {
284         let tz = match determine_time_zone() {
285             Ok(t) => {
286                 Some(t)
287             }
288             Err(ref e) => {
289                 println!("Unable to determine time zone: {}", e);
290                 None
291             }
292         };
293 
294         let numeric = locale::Numeric::load_user_locale()
295                              .unwrap_or_else(|_| locale::Numeric::english());
296 
297         let users = Mutex::new(UsersCache::new());
298 
299         Self { tz, numeric, users }
300     }
301 }
302 
determine_time_zone() -> TZResult<TimeZone>303 fn determine_time_zone() -> TZResult<TimeZone> {
304     if let Ok(file) = env::var("TZ") {
305         TimeZone::from_file({
306             if file.starts_with("/") {
307                 file
308             } else {
309                 format!("/usr/share/zoneinfo/{}", {
310                     if file.starts_with(":") {
311                         file.replacen(":", "", 1)
312                     } else {
313                         file
314                     }
315                 })
316             }
317         })
318     } else {
319         TimeZone::from_file("/etc/localtime")
320     }
321 }
322 
323 lazy_static! {
324     static ref ENVIRONMENT: Environment = Environment::load_all();
325 }
326 
327 
328 pub struct Table<'a> {
329     columns: Vec<Column>,
330     theme: &'a Theme,
331     env: &'a Environment,
332     widths: TableWidths,
333     time_format: TimeFormat,
334     size_format: SizeFormat,
335     user_format: UserFormat,
336     git: Option<&'a GitCache>,
337 }
338 
339 #[derive(Clone)]
340 pub struct Row {
341     cells: Vec<TextCell>,
342 }
343 
344 impl<'a, 'f> Table<'a> {
new(options: &'a Options, git: Option<&'a GitCache>, theme: &'a Theme) -> Table<'a>345     pub fn new(options: &'a Options, git: Option<&'a GitCache>, theme: &'a Theme) -> Table<'a> {
346         let columns = options.columns.collect(git.is_some());
347         let widths = TableWidths::zero(columns.len());
348         let env = &*ENVIRONMENT;
349 
350         Table {
351             theme,
352             widths,
353             columns,
354             git,
355             env,
356             time_format: options.time_format,
357             size_format: options.size_format,
358             user_format: options.user_format,
359         }
360     }
361 
widths(&self) -> &TableWidths362     pub fn widths(&self) -> &TableWidths {
363         &self.widths
364     }
365 
header_row(&self) -> Row366     pub fn header_row(&self) -> Row {
367         let cells = self.columns.iter()
368                         .map(|c| TextCell::paint_str(self.theme.ui.header, c.header()))
369                         .collect();
370 
371         Row { cells }
372     }
373 
row_for_file(&self, file: &File<'_>, xattrs: bool) -> Row374     pub fn row_for_file(&self, file: &File<'_>, xattrs: bool) -> Row {
375         let cells = self.columns.iter()
376                         .map(|c| self.display(file, *c, xattrs))
377                         .collect();
378 
379         Row { cells }
380     }
381 
add_widths(&mut self, row: &Row)382     pub fn add_widths(&mut self, row: &Row) {
383         self.widths.add_widths(row)
384     }
385 
permissions_plus(&self, file: &File<'_>, xattrs: bool) -> f::PermissionsPlus386     fn permissions_plus(&self, file: &File<'_>, xattrs: bool) -> f::PermissionsPlus {
387         f::PermissionsPlus {
388             file_type: file.type_char(),
389             permissions: file.permissions(),
390             xattrs,
391         }
392     }
393 
octal_permissions(&self, file: &File<'_>) -> f::OctalPermissions394     fn octal_permissions(&self, file: &File<'_>) -> f::OctalPermissions {
395         f::OctalPermissions {
396             permissions: file.permissions(),
397         }
398     }
399 
display(&self, file: &File<'_>, column: Column, xattrs: bool) -> TextCell400     fn display(&self, file: &File<'_>, column: Column, xattrs: bool) -> TextCell {
401         match column {
402             Column::Permissions => {
403                 self.permissions_plus(file, xattrs).render(self.theme)
404             }
405             Column::FileSize => {
406                 file.size().render(self.theme, self.size_format, &self.env.numeric)
407             }
408             Column::HardLinks => {
409                 file.links().render(self.theme, &self.env.numeric)
410             }
411             Column::Inode => {
412                 file.inode().render(self.theme.ui.inode)
413             }
414             Column::Blocks => {
415                 file.blocks().render(self.theme)
416             }
417             Column::User => {
418                 file.user().render(self.theme, &*self.env.lock_users(), self.user_format)
419             }
420             Column::Group => {
421                 file.group().render(self.theme, &*self.env.lock_users(), self.user_format)
422             }
423             Column::GitStatus => {
424                 self.git_status(file).render(self.theme)
425             }
426             Column::Octal => {
427                 self.octal_permissions(file).render(self.theme.ui.octal)
428             }
429 
430             Column::Timestamp(TimeType::Modified)  => {
431                 file.modified_time().render(self.theme.ui.date, &self.env.tz, self.time_format)
432             }
433             Column::Timestamp(TimeType::Changed)   => {
434                 file.changed_time().render(self.theme.ui.date, &self.env.tz, self.time_format)
435             }
436             Column::Timestamp(TimeType::Created)   => {
437                 file.created_time().render(self.theme.ui.date, &self.env.tz, self.time_format)
438             }
439             Column::Timestamp(TimeType::Accessed)  => {
440                 file.accessed_time().render(self.theme.ui.date, &self.env.tz, self.time_format)
441             }
442         }
443     }
444 
git_status(&self, file: &File<'_>) -> f::Git445     fn git_status(&self, file: &File<'_>) -> f::Git {
446         debug!("Getting Git status for file {:?}", file.path);
447 
448         self.git
449             .map(|g| g.get(&file.path, file.is_directory()))
450             .unwrap_or_default()
451     }
452 
render(&self, row: Row) -> TextCell453     pub fn render(&self, row: Row) -> TextCell {
454         let mut cell = TextCell::default();
455 
456         let iter = row.cells.into_iter()
457                       .zip(self.widths.iter())
458                       .enumerate();
459 
460         for (n, (this_cell, width)) in iter {
461             let padding = width - *this_cell.width;
462 
463             match self.columns[n].alignment() {
464                 Alignment::Left => {
465                     cell.append(this_cell);
466                     cell.add_spaces(padding);
467                 }
468                 Alignment::Right => {
469                     cell.add_spaces(padding);
470                     cell.append(this_cell);
471                 }
472             }
473 
474             cell.add_spaces(1);
475         }
476 
477         cell
478     }
479 }
480 
481 
482 pub struct TableWidths(Vec<usize>);
483 
484 impl Deref for TableWidths {
485     type Target = [usize];
486 
deref(&self) -> &Self::Target487     fn deref(&self) -> &Self::Target {
488         &self.0
489     }
490 }
491 
492 impl TableWidths {
zero(count: usize) -> Self493     pub fn zero(count: usize) -> Self {
494         Self(vec![0; count])
495     }
496 
add_widths(&mut self, row: &Row)497     pub fn add_widths(&mut self, row: &Row) {
498         for (old_width, cell) in self.0.iter_mut().zip(row.cells.iter()) {
499             *old_width = max(*old_width, *cell.width);
500         }
501     }
502 
total(&self) -> usize503     pub fn total(&self) -> usize {
504         self.0.len() + self.0.iter().sum::<usize>()
505     }
506 }
507