table.rs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. use std::cmp::max;
  2. use std::ops::Deref;
  3. #[cfg(unix)]
  4. use std::sync::{Mutex, MutexGuard};
  5. use chrono::prelude::*;
  6. use log::*;
  7. use once_cell::sync::Lazy;
  8. #[cfg(unix)]
  9. use uzers::UsersCache;
  10. use crate::fs::feature::git::GitCache;
  11. use crate::fs::feature::mercurial::MercurialCache;
  12. use crate::fs::{fields as f, File};
  13. use crate::options::vars::EZA_WINDOWS_ATTRIBUTES;
  14. use crate::options::Vars;
  15. use crate::output::cell::TextCell;
  16. use crate::output::color_scale::ColorScaleInformation;
  17. #[cfg(unix)]
  18. use crate::output::render::{GroupRender, OctalPermissionsRender, UserRender};
  19. use crate::output::render::{PermissionsPlusRender, TimeRender};
  20. use crate::output::time::TimeFormat;
  21. use crate::theme::Theme;
  22. use super::color_scale::ColorScaleMode;
  23. /// Options for displaying a table.
  24. #[derive(PartialEq, Eq, Debug)]
  25. pub struct Options {
  26. pub size_format: SizeFormat,
  27. pub time_format: TimeFormat,
  28. pub user_format: UserFormat,
  29. pub group_format: GroupFormat,
  30. pub flags_format: FlagsFormat,
  31. pub columns: Columns,
  32. }
  33. /// Extra columns to display in the table.
  34. #[allow(clippy::struct_excessive_bools)]
  35. #[derive(PartialEq, Eq, Debug, Copy, Clone)]
  36. pub struct Columns {
  37. /// At least one of these timestamps will be shown.
  38. pub time_types: TimeTypes,
  39. // The rest are just on/off
  40. pub inode: bool,
  41. pub links: bool,
  42. pub blocksize: bool,
  43. pub group: bool,
  44. pub git: bool,
  45. pub mercurial: bool,
  46. pub subdir_git_repos: bool,
  47. pub subdir_git_repos_no_stat: bool,
  48. pub octal: bool,
  49. pub security_context: bool,
  50. pub file_flags: bool,
  51. // Defaults to true:
  52. pub permissions: bool,
  53. pub filesize: bool,
  54. pub user: bool,
  55. }
  56. impl Columns {
  57. pub fn collect(
  58. &self,
  59. actually_enable_git: bool,
  60. git_repos: bool,
  61. actually_enable_mercurial: bool,
  62. ) -> Vec<Column> {
  63. let mut columns = Vec::with_capacity(4);
  64. if self.inode {
  65. #[cfg(unix)]
  66. columns.push(Column::Inode);
  67. }
  68. if self.octal {
  69. #[cfg(unix)]
  70. columns.push(Column::Octal);
  71. }
  72. if self.permissions {
  73. columns.push(Column::Permissions);
  74. }
  75. if self.links {
  76. #[cfg(unix)]
  77. columns.push(Column::HardLinks);
  78. }
  79. if self.filesize {
  80. columns.push(Column::FileSize);
  81. }
  82. if self.blocksize {
  83. #[cfg(unix)]
  84. columns.push(Column::Blocksize);
  85. }
  86. if self.user {
  87. #[cfg(unix)]
  88. columns.push(Column::User);
  89. }
  90. if self.group {
  91. #[cfg(unix)]
  92. columns.push(Column::Group);
  93. }
  94. if self.file_flags {
  95. columns.push(Column::FileFlags);
  96. }
  97. #[cfg(target_os = "linux")]
  98. if self.security_context {
  99. columns.push(Column::SecurityContext);
  100. }
  101. if self.time_types.modified {
  102. columns.push(Column::Timestamp(TimeType::Modified));
  103. }
  104. if self.time_types.changed {
  105. columns.push(Column::Timestamp(TimeType::Changed));
  106. }
  107. if self.time_types.created {
  108. columns.push(Column::Timestamp(TimeType::Created));
  109. }
  110. if self.time_types.accessed {
  111. columns.push(Column::Timestamp(TimeType::Accessed));
  112. }
  113. if self.git && actually_enable_git {
  114. columns.push(Column::GitStatus);
  115. }
  116. if self.subdir_git_repos && git_repos {
  117. columns.push(Column::SubdirGitRepo(true));
  118. }
  119. if self.subdir_git_repos_no_stat && git_repos {
  120. columns.push(Column::SubdirGitRepo(false));
  121. }
  122. if self.mercurial && actually_enable_mercurial {
  123. columns.push(Column::MercurialStatus);
  124. }
  125. columns
  126. }
  127. }
  128. /// A table contains these.
  129. #[derive(Debug, Copy, Clone)]
  130. pub enum Column {
  131. Permissions,
  132. FileSize,
  133. Timestamp(TimeType),
  134. #[cfg(unix)]
  135. Blocksize,
  136. #[cfg(unix)]
  137. User,
  138. #[cfg(unix)]
  139. Group,
  140. #[cfg(unix)]
  141. HardLinks,
  142. #[cfg(unix)]
  143. Inode,
  144. GitStatus,
  145. SubdirGitRepo(bool),
  146. MercurialStatus,
  147. #[cfg(unix)]
  148. Octal,
  149. #[cfg(unix)]
  150. SecurityContext,
  151. FileFlags,
  152. }
  153. /// Each column can pick its own **Alignment**. Usually, numbers are
  154. /// right-aligned, and text is left-aligned.
  155. #[derive(Copy, Clone)]
  156. pub enum Alignment {
  157. Left,
  158. Right,
  159. }
  160. impl Column {
  161. /// Get the alignment this column should use.
  162. #[cfg(unix)]
  163. pub fn alignment(self) -> Alignment {
  164. #[allow(clippy::wildcard_in_or_patterns)]
  165. match self {
  166. Self::FileSize | Self::HardLinks | Self::Inode | Self::Blocksize | Self::GitStatus => {
  167. Alignment::Right
  168. }
  169. Self::Timestamp(_) | _ => Alignment::Left,
  170. }
  171. }
  172. #[cfg(windows)]
  173. pub fn alignment(self) -> Alignment {
  174. match self {
  175. Self::FileSize | Self::GitStatus => Alignment::Right,
  176. _ => Alignment::Left,
  177. }
  178. }
  179. /// Get the text that should be printed at the top, when the user elects
  180. /// to have a header row printed.
  181. pub fn header(self) -> &'static str {
  182. match self {
  183. #[cfg(unix)]
  184. Self::Permissions => "Permissions",
  185. #[cfg(windows)]
  186. Self::Permissions => "Mode",
  187. Self::FileSize => "Size",
  188. Self::Timestamp(t) => t.header(),
  189. #[cfg(unix)]
  190. Self::Blocksize => "Blocksize",
  191. #[cfg(unix)]
  192. Self::User => "User",
  193. #[cfg(unix)]
  194. Self::Group => "Group",
  195. #[cfg(unix)]
  196. Self::HardLinks => "Links",
  197. #[cfg(unix)]
  198. Self::Inode => "inode",
  199. Self::GitStatus => "Git",
  200. Self::SubdirGitRepo(_) => "Repo",
  201. Self::MercurialStatus => "Mercurial",
  202. #[cfg(unix)]
  203. Self::Octal => "Octal",
  204. #[cfg(unix)]
  205. Self::SecurityContext => "Security Context",
  206. Self::FileFlags => "Flags",
  207. }
  208. }
  209. }
  210. /// Formatting options for file sizes.
  211. #[allow(clippy::enum_variant_names)]
  212. #[derive(PartialEq, Eq, Debug, Copy, Clone)]
  213. pub enum SizeFormat {
  214. /// Format the file size using **decimal** prefixes, such as “kilo”,
  215. /// “mega”, or “giga”.
  216. DecimalBytes,
  217. /// Format the file size using **binary** prefixes, such as “kibi”,
  218. /// “mebi”, or “gibi”.
  219. BinaryBytes,
  220. /// Do no formatting and just display the size as a number of bytes.
  221. JustBytes,
  222. }
  223. /// Formatting options for user and group.
  224. #[derive(PartialEq, Eq, Debug, Copy, Clone)]
  225. pub enum UserFormat {
  226. /// The UID / GID
  227. Numeric,
  228. /// Show the name
  229. Name,
  230. }
  231. /// Formatting options for group only.
  232. #[derive(PartialEq, Eq, Debug, Copy, Clone)]
  233. pub enum GroupFormat {
  234. /// Numeric or text value
  235. Regular,
  236. /// Show ":" if user-group value is the same
  237. Smart,
  238. }
  239. impl Default for SizeFormat {
  240. fn default() -> Self {
  241. Self::DecimalBytes
  242. }
  243. }
  244. /// The types of a file’s time fields. These three fields are standard
  245. /// across most (all?) operating systems.
  246. #[derive(PartialEq, Eq, Debug, Copy, Clone)]
  247. pub enum TimeType {
  248. /// The file’s modified time (`st_mtime`).
  249. Modified,
  250. /// The file’s changed time (`st_ctime`)
  251. Changed,
  252. /// The file’s accessed time (`st_atime`).
  253. Accessed,
  254. /// The file’s creation time (`btime` or `birthtime`).
  255. Created,
  256. }
  257. impl TimeType {
  258. /// Returns the text to use for a column’s heading in the columns output.
  259. pub fn header(self) -> &'static str {
  260. match self {
  261. Self::Modified => "Date Modified",
  262. Self::Changed => "Date Changed",
  263. Self::Accessed => "Date Accessed",
  264. Self::Created => "Date Created",
  265. }
  266. }
  267. /// Returns the corresponding time from [File]
  268. pub fn get_corresponding_time(self, file: &File<'_>) -> Option<NaiveDateTime> {
  269. match self {
  270. TimeType::Modified => file.modified_time(),
  271. TimeType::Changed => file.changed_time(),
  272. TimeType::Accessed => file.accessed_time(),
  273. TimeType::Created => file.created_time(),
  274. }
  275. }
  276. }
  277. /// How display file flags.
  278. #[derive(PartialEq, Eq, Debug, Copy, Clone)]
  279. pub enum FlagsFormat {
  280. /// Display flags as comma seperated descriptions
  281. Long,
  282. /// Display flags as single character abbreviations (Windows only)
  283. Short,
  284. }
  285. impl Default for FlagsFormat {
  286. fn default() -> Self {
  287. Self::Long
  288. }
  289. }
  290. impl FlagsFormat {
  291. pub(crate) fn deduce<V: Vars>(vars: &V) -> FlagsFormat {
  292. vars.get(EZA_WINDOWS_ATTRIBUTES)
  293. .and_then(|v| match v.to_ascii_lowercase().to_str() {
  294. Some("short") => Some(FlagsFormat::Short),
  295. Some("long") => Some(FlagsFormat::Long),
  296. _ => None,
  297. })
  298. .unwrap_or_default()
  299. }
  300. }
  301. /// Fields for which of a file’s time fields should be displayed in the
  302. /// columns output.
  303. ///
  304. /// There should always be at least one of these — there’s no way to disable
  305. /// the time columns entirely (yet).
  306. #[derive(PartialEq, Eq, Debug, Copy, Clone)]
  307. #[rustfmt::skip]
  308. #[allow(clippy::struct_excessive_bools)]
  309. pub struct TimeTypes {
  310. pub modified: bool,
  311. pub changed: bool,
  312. pub accessed: bool,
  313. pub created: bool,
  314. }
  315. impl Default for TimeTypes {
  316. /// By default, display just the ‘modified’ time. This is the most
  317. /// common option, which is why it has this shorthand.
  318. fn default() -> Self {
  319. Self {
  320. modified: true,
  321. changed: false,
  322. accessed: false,
  323. created: false,
  324. }
  325. }
  326. }
  327. /// The **environment** struct contains any data that could change between
  328. /// running instances of exa, depending on the user’s computer’s configuration.
  329. ///
  330. /// Any environment field should be able to be mocked up for test runs.
  331. pub struct Environment {
  332. /// The computer’s current time offset, determined from time zone.
  333. time_offset: FixedOffset,
  334. /// Localisation rules for formatting numbers.
  335. numeric: locale::Numeric,
  336. /// Mapping cache of user IDs to usernames.
  337. #[cfg(unix)]
  338. users: Mutex<UsersCache>,
  339. }
  340. impl Environment {
  341. #[cfg(unix)]
  342. pub fn lock_users(&self) -> MutexGuard<'_, UsersCache> {
  343. self.users.lock().unwrap()
  344. }
  345. fn load_all() -> Self {
  346. let time_offset = *Local::now().offset();
  347. let numeric =
  348. locale::Numeric::load_user_locale().unwrap_or_else(|_| locale::Numeric::english());
  349. #[cfg(unix)]
  350. let users = Mutex::new(UsersCache::new());
  351. Self {
  352. time_offset,
  353. numeric,
  354. #[cfg(unix)]
  355. users,
  356. }
  357. }
  358. }
  359. static ENVIRONMENT: Lazy<Environment> = Lazy::new(Environment::load_all);
  360. pub struct Table<'a> {
  361. columns: Vec<Column>,
  362. theme: &'a Theme,
  363. env: &'a Environment,
  364. widths: TableWidths,
  365. time_format: TimeFormat,
  366. size_format: SizeFormat,
  367. #[cfg(unix)]
  368. user_format: UserFormat,
  369. #[cfg(unix)]
  370. group_format: GroupFormat,
  371. flags_format: FlagsFormat,
  372. git: Option<&'a GitCache>,
  373. mercurial: Option<&'a MercurialCache>,
  374. }
  375. #[derive(Clone)]
  376. pub struct Row {
  377. cells: Vec<TextCell>,
  378. }
  379. impl<'a> Table<'a> {
  380. pub fn new(
  381. options: &'a Options,
  382. git: Option<&'a GitCache>,
  383. theme: &'a Theme,
  384. git_repos: bool,
  385. mercurial: Option<&'a MercurialCache>,
  386. ) -> Table<'a> {
  387. let columns = options
  388. .columns
  389. .collect(git.is_some(), git_repos, mercurial.is_some());
  390. let widths = TableWidths::zero(columns.len());
  391. let env = &*ENVIRONMENT;
  392. debug!("Creating table with columns: {:?}", columns);
  393. Table {
  394. theme,
  395. widths,
  396. columns,
  397. git,
  398. env,
  399. time_format: options.time_format.clone(),
  400. size_format: options.size_format,
  401. #[cfg(unix)]
  402. user_format: options.user_format,
  403. #[cfg(unix)]
  404. group_format: options.group_format,
  405. flags_format: options.flags_format,
  406. mercurial,
  407. }
  408. }
  409. pub fn widths(&self) -> &TableWidths {
  410. &self.widths
  411. }
  412. pub fn header_row(&self) -> Row {
  413. let cells = self
  414. .columns
  415. .iter()
  416. .map(|c| TextCell::paint_str(self.theme.ui.header, c.header()))
  417. .collect();
  418. Row { cells }
  419. }
  420. pub fn row_for_file(
  421. &self,
  422. file: &File<'_>,
  423. xattrs: bool,
  424. color_scale_info: Option<ColorScaleInformation>,
  425. ) -> Row {
  426. let cells = self
  427. .columns
  428. .iter()
  429. .map(|c| self.display(file, *c, xattrs, color_scale_info))
  430. .collect();
  431. Row { cells }
  432. }
  433. pub fn add_widths(&mut self, row: &Row) {
  434. self.widths.add_widths(row);
  435. }
  436. #[cfg(unix)]
  437. fn permissions_plus(&self, file: &File<'_>, xattrs: bool) -> Option<f::PermissionsPlus> {
  438. file.permissions().map(|p| f::PermissionsPlus {
  439. file_type: file.type_char(),
  440. permissions: p,
  441. xattrs,
  442. })
  443. }
  444. #[allow(clippy::unnecessary_wraps)] // Needs to match Unix function
  445. #[cfg(windows)]
  446. fn permissions_plus(&self, file: &File<'_>, xattrs: bool) -> Option<f::PermissionsPlus> {
  447. Some(f::PermissionsPlus {
  448. file_type: file.type_char(),
  449. #[cfg(windows)]
  450. attributes: file.attributes(),
  451. xattrs,
  452. })
  453. }
  454. #[cfg(unix)]
  455. fn octal_permissions(&self, file: &File<'_>) -> Option<f::OctalPermissions> {
  456. file.permissions()
  457. .map(|p| f::OctalPermissions { permissions: p })
  458. }
  459. fn display(
  460. &self,
  461. file: &File<'_>,
  462. column: Column,
  463. xattrs: bool,
  464. color_scale_info: Option<ColorScaleInformation>,
  465. ) -> TextCell {
  466. match column {
  467. Column::Permissions => self.permissions_plus(file, xattrs).render(self.theme),
  468. Column::FileSize => file.size().render(
  469. self.theme,
  470. self.size_format,
  471. &self.env.numeric,
  472. color_scale_info,
  473. ),
  474. #[cfg(unix)]
  475. Column::HardLinks => file.links().render(self.theme, &self.env.numeric),
  476. #[cfg(unix)]
  477. Column::Inode => file.inode().render(self.theme.ui.inode),
  478. #[cfg(unix)]
  479. Column::Blocksize => {
  480. file.blocksize()
  481. .render(self.theme, self.size_format, &self.env.numeric)
  482. }
  483. #[cfg(unix)]
  484. Column::User => {
  485. file.user()
  486. .render(self.theme, &*self.env.lock_users(), self.user_format)
  487. }
  488. #[cfg(unix)]
  489. Column::Group => file.group().render(
  490. self.theme,
  491. &*self.env.lock_users(),
  492. self.user_format,
  493. self.group_format,
  494. file.user(),
  495. ),
  496. #[cfg(unix)]
  497. Column::SecurityContext => file.security_context().render(self.theme),
  498. Column::FileFlags => file.flags().render(self.theme.ui.flags, self.flags_format),
  499. Column::GitStatus => self.git_status(file).render(self.theme),
  500. Column::SubdirGitRepo(status) => self.subdir_git_repo(file, status).render(self.theme),
  501. Column::MercurialStatus => self.mercurial_status(file).render(self.theme),
  502. #[cfg(unix)]
  503. Column::Octal => self.octal_permissions(file).render(self.theme.ui.octal),
  504. Column::Timestamp(time_type) => time_type.get_corresponding_time(file).render(
  505. if color_scale_info.is_some_and(|csi| csi.options.mode == ColorScaleMode::Gradient)
  506. {
  507. color_scale_info.unwrap().apply_time_gradient(
  508. self.theme.ui.date,
  509. file,
  510. time_type,
  511. )
  512. } else {
  513. self.theme.ui.date
  514. },
  515. self.env.time_offset,
  516. self.time_format.clone(),
  517. ),
  518. }
  519. }
  520. fn git_status(&self, file: &File<'_>) -> f::Git {
  521. debug!("Getting Git status for file {:?}", file.path);
  522. self.git
  523. .map(|g| g.get(&file.path, file.is_directory()))
  524. .unwrap_or_default()
  525. }
  526. fn mercurial_status(&self, file: &File<'_>) -> f::Mercurial {
  527. debug!("Getting Mercurial status for file {:?}", file.path);
  528. self.mercurial
  529. .map(|m| m.get(&file.path))
  530. .unwrap_or_default()
  531. }
  532. fn subdir_git_repo(&self, file: &File<'_>, status: bool) -> f::SubdirGitRepo {
  533. debug!("Getting subdir repo status for path {:?}", file.path);
  534. if file.is_directory() {
  535. return f::SubdirGitRepo::from_path(&file.path, status);
  536. }
  537. f::SubdirGitRepo::default()
  538. }
  539. pub fn render(&self, row: Row) -> TextCell {
  540. let mut cell = TextCell::default();
  541. let iter = row.cells.into_iter().zip(self.widths.iter()).enumerate();
  542. for (n, (this_cell, width)) in iter {
  543. let padding = width - *this_cell.width;
  544. match self.columns[n].alignment() {
  545. Alignment::Left => {
  546. cell.append(this_cell);
  547. cell.add_spaces(padding);
  548. }
  549. Alignment::Right => {
  550. cell.add_spaces(padding);
  551. cell.append(this_cell);
  552. }
  553. }
  554. cell.add_spaces(1);
  555. }
  556. cell
  557. }
  558. }
  559. pub struct TableWidths(Vec<usize>);
  560. impl Deref for TableWidths {
  561. type Target = [usize];
  562. fn deref(&self) -> &Self::Target {
  563. &self.0
  564. }
  565. }
  566. impl TableWidths {
  567. pub fn zero(count: usize) -> Self {
  568. Self(vec![0; count])
  569. }
  570. pub fn add_widths(&mut self, row: &Row) {
  571. for (old_width, cell) in self.0.iter_mut().zip(row.cells.iter()) {
  572. *old_width = max(*old_width, *cell.width);
  573. }
  574. }
  575. pub fn total(&self) -> usize {
  576. self.0.len() + self.0.iter().sum::<usize>()
  577. }
  578. }