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