table.rs 16 KB

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