details.rs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. //! The **Details** output view displays each file as a row in a table.
  2. //!
  3. //! Itโ€™s used in the following situations:
  4. //!
  5. //! - Most commonly, when using the `--long` command-line argument to display the
  6. //! details of each file, which requires using a table view to hold all the data;
  7. //! - When using the `--tree` argument, which uses the same table view to display
  8. //! each file on its own line, with the table providing the tree characters;
  9. //! - When using both the `--long` and `--grid` arguments, which constructs a
  10. //! series of tables to fit all the data on the screen.
  11. //!
  12. //! You will probably recognise it from the `ls --long` command. It looks like
  13. //! this:
  14. //!
  15. //! ```text
  16. //! .rw-r--r-- 9.6k ben 29 Jun 16:16 Cargo.lock
  17. //! .rw-r--r-- 547 ben 23 Jun 10:54 Cargo.toml
  18. //! .rw-r--r-- 1.1k ben 23 Nov 2014 LICENCE
  19. //! .rw-r--r-- 2.5k ben 21 May 14:38 README.md
  20. //! .rw-r--r-- 382k ben 8 Jun 21:00 screenshot.png
  21. //! drwxr-xr-x - ben 29 Jun 14:50 src
  22. //! drwxr-xr-x - ben 28 Jun 19:53 target
  23. //! ```
  24. //!
  25. //! The table is constructed by creating a `Table` value, which produces a `Row`
  26. //! value for each file. These rows can contain a vector of `Cell`s, or they can
  27. //! contain depth information for the tree view, or both. These are described
  28. //! below.
  29. //!
  30. //!
  31. //! ## Constructing Detail Views
  32. //!
  33. //! When using the `--long` command-line argument, the details of each file are
  34. //! displayed next to its name.
  35. //!
  36. //! The table holds a vector of all the column types. For each file and column, a
  37. //! `Cell` value containing the ANSI-coloured text and Unicode width of each cell
  38. //! is generated, with the row and column determined by indexing into both arrays.
  39. //!
  40. //! The column types vector does not actually include the filename. This is
  41. //! because the filename is always the rightmost field, and as such, it does not
  42. //! need to have its width queried or be padded with spaces.
  43. //!
  44. //! To illustrate the above:
  45. //!
  46. //! ```text
  47. //! โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  48. //! โ”‚ columns: [ Permissions, Size, User, Date(Modified) ] โ”‚
  49. //! โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
  50. //! โ”‚ rows: cells: filename: โ”‚
  51. //! โ”‚ row 1: [ ".rw-r--r--", "9.6k", "ben", "29 Jun 16:16" ] Cargo.lock โ”‚
  52. //! โ”‚ row 2: [ ".rw-r--r--", "547", "ben", "23 Jun 10:54" ] Cargo.toml โ”‚
  53. //! โ”‚ row 3: [ "drwxr-xr-x", "-", "ben", "29 Jun 14:50" ] src โ”‚
  54. //! โ”‚ row 4: [ "drwxr-xr-x", "-", "ben", "28 Jun 19:53" ] target โ”‚
  55. //! โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
  56. //! ```
  57. //!
  58. //! Each column in the table needs to be resized to fit its widest argument. This
  59. //! means that we must wait until every row has been added to the table before it
  60. //! can be displayed, in order to make sure that every column is wide enough.
  61. use std::io::{self, Write};
  62. use std::mem::MaybeUninit;
  63. use std::path::PathBuf;
  64. use std::vec::IntoIter as VecIntoIter;
  65. use ansiterm::Style;
  66. use scoped_threadpool::Pool;
  67. use log::*;
  68. use crate::fs::dir_action::RecurseOptions;
  69. use crate::fs::feature::git::GitCache;
  70. use crate::fs::feature::xattr::Attribute;
  71. use crate::fs::fields::SecurityContextType;
  72. use crate::fs::filter::FileFilter;
  73. use crate::fs::{Dir, File};
  74. use crate::output::cell::TextCell;
  75. use crate::output::file_name::Options as FileStyle;
  76. use crate::output::table::{Options as TableOptions, Row as TableRow, Table};
  77. use crate::output::tree::{TreeDepth, TreeParams, TreeTrunk};
  78. use crate::theme::Theme;
  79. /// With the **Details** view, the output gets formatted into columns, with
  80. /// each `Column` object showing some piece of information about the file,
  81. /// such as its size, or its permissions.
  82. ///
  83. /// To do this, the results have to be written to a table, instead of
  84. /// displaying each file immediately. Then, the width of each column can be
  85. /// calculated based on the individual results, and the fields are padded
  86. /// during output.
  87. ///
  88. /// Almost all the heavy lifting is done in a Table object, which handles the
  89. /// columns for each row.
  90. #[allow(clippy::struct_excessive_bools)]
  91. /// This clearly isn't a state machine
  92. #[derive(PartialEq, Eq, Debug)]
  93. pub struct Options {
  94. /// Options specific to drawing a table.
  95. ///
  96. /// Directories themselves can pick which columns are *added* to this
  97. /// list, such as the Git column.
  98. pub table: Option<TableOptions>,
  99. /// Whether to show a header line or not.
  100. pub header: bool,
  101. /// Whether to show each fileโ€™s extended attributes.
  102. pub xattr: bool,
  103. /// Whether to show each file's security attribute.
  104. pub secattr: bool,
  105. /// Whether to show a directory's mounted filesystem details
  106. pub mounts: bool,
  107. }
  108. pub struct Render<'a> {
  109. pub dir: Option<&'a Dir>,
  110. pub files: Vec<File<'a>>,
  111. pub theme: &'a Theme,
  112. pub file_style: &'a FileStyle,
  113. pub opts: &'a Options,
  114. /// Whether to recurse through directories with a tree view, and if so,
  115. /// which options to use. This field is only relevant here if the `tree`
  116. /// field of the RecurseOptions is `true`.
  117. pub recurse: Option<RecurseOptions>,
  118. /// How to sort and filter the files after getting their details.
  119. pub filter: &'a FileFilter,
  120. /// Whether we are skipping Git-ignored files.
  121. pub git_ignoring: bool,
  122. pub git: Option<&'a GitCache>,
  123. }
  124. #[rustfmt::skip]
  125. struct Egg<'a> {
  126. table_row: Option<TableRow>,
  127. xattrs: &'a [Attribute],
  128. errors: Vec<(io::Error, Option<PathBuf>)>,
  129. dir: Option<Dir>,
  130. file: &'a File<'a>,
  131. }
  132. impl<'a> AsRef<File<'a>> for Egg<'a> {
  133. fn as_ref(&self) -> &File<'a> {
  134. self.file
  135. }
  136. }
  137. impl<'a> Render<'a> {
  138. pub fn render<W: Write>(mut self, w: &mut W) -> io::Result<()> {
  139. let n_cpus = match num_cpus::get() as u32 {
  140. 0 => 1,
  141. n => n,
  142. };
  143. let mut pool = Pool::new(n_cpus);
  144. let mut rows = Vec::new();
  145. if let Some(ref table) = self.opts.table {
  146. match (self.git, self.dir) {
  147. (Some(g), Some(d)) => {
  148. if !g.has_anything_for(&d.path) {
  149. self.git = None;
  150. }
  151. }
  152. (Some(g), None) => {
  153. if !self.files.iter().any(|f| g.has_anything_for(&f.path)) {
  154. self.git = None;
  155. }
  156. }
  157. (None, _) => { /* Keep Git how it is */ }
  158. }
  159. let mut table = Table::new(table, self.git, self.theme);
  160. if self.opts.header {
  161. let header = table.header_row();
  162. table.add_widths(&header);
  163. rows.push(self.render_header(header));
  164. }
  165. // This is weird, but I canโ€™t find a way around it:
  166. // https://internals.rust-lang.org/t/should-option-mut-t-implement-copy/3715/6
  167. let mut table = Some(table);
  168. self.add_files_to_table(
  169. &mut pool,
  170. &mut table,
  171. &mut rows,
  172. &self.files,
  173. TreeDepth::root(),
  174. );
  175. for row in self.iterate_with_table(table.unwrap(), rows) {
  176. writeln!(w, "{}", row.strings())?;
  177. }
  178. } else {
  179. self.add_files_to_table(
  180. &mut pool,
  181. &mut None,
  182. &mut rows,
  183. &self.files,
  184. TreeDepth::root(),
  185. );
  186. for row in self.iterate(rows) {
  187. writeln!(w, "{}", row.strings())?;
  188. }
  189. }
  190. Ok(())
  191. }
  192. /// Whether to show the extended attribute hint
  193. pub fn show_xattr_hint(&self, file: &File<'_>) -> bool {
  194. // Do not show the hint '@' if the only extended attribute is the security
  195. // attribute and the security attribute column is active.
  196. let xattr_count = file.extended_attributes().len();
  197. let selinux_ctx_shown = self.opts.secattr
  198. && match file.security_context().context {
  199. SecurityContextType::SELinux(_) => true,
  200. SecurityContextType::None => false,
  201. };
  202. xattr_count > 1 || (xattr_count == 1 && !selinux_ctx_shown)
  203. }
  204. /// Adds files to the table, possibly recursively. This is easily
  205. /// parallelisable, and uses a pool of threads.
  206. fn add_files_to_table<'dir>(
  207. &self,
  208. pool: &mut Pool,
  209. table: &mut Option<Table<'a>>,
  210. rows: &mut Vec<Row>,
  211. src: &[File<'dir>],
  212. depth: TreeDepth,
  213. ) {
  214. use crate::fs::feature::xattr;
  215. use std::sync::{Arc, Mutex};
  216. let mut file_eggs = (0..src.len())
  217. .map(|_| MaybeUninit::uninit())
  218. .collect::<Vec<_>>();
  219. pool.scoped(|scoped| {
  220. let file_eggs = Arc::new(Mutex::new(&mut file_eggs));
  221. let table = table.as_ref();
  222. for (idx, file) in src.iter().enumerate() {
  223. let file_eggs = Arc::clone(&file_eggs);
  224. scoped.execute(move || {
  225. let mut errors = Vec::new();
  226. // There are three โ€œlevelsโ€ of extended attribute support:
  227. //
  228. // 1. If weโ€™re compiling without that feature, then
  229. // exa pretends all files have no attributes.
  230. // 2. If the feature is enabled and the --extended flag
  231. // has been specified, then display an @ in the
  232. // permissions column for files with attributes, the
  233. // names of all attributes and their values, and any
  234. // errors encountered when getting them.
  235. // 3. If the --extended flag *hasnโ€™t* been specified, then
  236. // display the @, but donโ€™t display anything else.
  237. //
  238. // For a while, exa took a stricter approach to (3):
  239. // if an error occurred while checking a fileโ€™s xattrs to
  240. // see if it should display the @, exa would display that
  241. // error even though the attributes werenโ€™t actually being
  242. // shown! This was confusing, as users were being shown
  243. // errors for something they didnโ€™t explicitly ask for,
  244. // and just cluttered up the output. So now errors arenโ€™t
  245. // printed unless the user passes --extended to signify
  246. // that they want to see them.
  247. let xattrs: &[Attribute] = if xattr::ENABLED && self.opts.xattr {
  248. file.extended_attributes()
  249. } else {
  250. &[]
  251. };
  252. let table_row = table
  253. .as_ref()
  254. .map(|t| t.row_for_file(file, self.show_xattr_hint(file)));
  255. let mut dir = None;
  256. if let Some(r) = self.recurse {
  257. if file.is_directory() && r.tree && !r.is_too_deep(depth.0) {
  258. trace!("matching on to_dir");
  259. match file.to_dir() {
  260. Ok(d) => {
  261. dir = Some(d);
  262. }
  263. Err(e) => {
  264. errors.push((e, None));
  265. }
  266. }
  267. }
  268. };
  269. let egg = Egg {
  270. table_row,
  271. xattrs,
  272. errors,
  273. dir,
  274. file,
  275. };
  276. unsafe { std::ptr::write(file_eggs.lock().unwrap()[idx].as_mut_ptr(), egg) }
  277. });
  278. }
  279. });
  280. // this is safe because all entries have been initialized above
  281. let mut file_eggs = unsafe { std::mem::transmute::<_, Vec<Egg<'_>>>(file_eggs) };
  282. self.filter.sort_files(&mut file_eggs);
  283. for (tree_params, egg) in depth.iterate_over(file_eggs.into_iter()) {
  284. let mut files = Vec::new();
  285. let mut errors = egg.errors;
  286. if let (Some(ref mut t), Some(row)) = (table.as_mut(), egg.table_row.as_ref()) {
  287. t.add_widths(row);
  288. }
  289. let file_name = self
  290. .file_style
  291. .for_file(egg.file, self.theme)
  292. .with_link_paths()
  293. .with_mount_details(self.opts.mounts)
  294. .paint()
  295. .promote();
  296. let row = Row {
  297. tree: tree_params,
  298. cells: egg.table_row,
  299. name: file_name,
  300. };
  301. rows.push(row);
  302. if let Some(ref dir) = egg.dir {
  303. for file_to_add in dir.files(
  304. self.filter.dot_filter,
  305. self.git,
  306. self.git_ignoring,
  307. egg.file.deref_links,
  308. ) {
  309. match file_to_add {
  310. Ok(f) => {
  311. files.push(f);
  312. }
  313. Err((path, e)) => {
  314. errors.push((e, Some(path)));
  315. }
  316. }
  317. }
  318. self.filter.filter_child_files(&mut files);
  319. if !files.is_empty() {
  320. for xattr in egg.xattrs {
  321. rows.push(self.render_xattr(xattr, TreeParams::new(depth.deeper(), false)));
  322. }
  323. for (error, path) in errors {
  324. rows.push(self.render_error(
  325. &error,
  326. TreeParams::new(depth.deeper(), false),
  327. path,
  328. ));
  329. }
  330. self.add_files_to_table(pool, table, rows, &files, depth.deeper());
  331. continue;
  332. }
  333. }
  334. let count = egg.xattrs.len();
  335. for (index, xattr) in egg.xattrs.iter().enumerate() {
  336. let params =
  337. TreeParams::new(depth.deeper(), errors.is_empty() && index == count - 1);
  338. let r = self.render_xattr(xattr, params);
  339. rows.push(r);
  340. }
  341. let count = errors.len();
  342. for (index, (error, path)) in errors.into_iter().enumerate() {
  343. let params = TreeParams::new(depth.deeper(), index == count - 1);
  344. let r = self.render_error(&error, params, path);
  345. rows.push(r);
  346. }
  347. }
  348. }
  349. pub fn render_header(&self, header: TableRow) -> Row {
  350. Row {
  351. tree: TreeParams::new(TreeDepth::root(), false),
  352. cells: Some(header),
  353. name: TextCell::paint_str(self.theme.ui.header, "Name"),
  354. }
  355. }
  356. fn render_error(&self, error: &io::Error, tree: TreeParams, path: Option<PathBuf>) -> Row {
  357. use crate::output::file_name::Colours;
  358. let error_message = if let Some(path) = path {
  359. format!("<{}: {}>", path.display(), error)
  360. } else {
  361. format!("<{error}>")
  362. };
  363. // TODO: broken_symlink() doesnโ€™t quite seem like the right name for
  364. // the style thatโ€™s being used here. Maybe split it in two?
  365. let name = TextCell::paint(self.theme.broken_symlink(), error_message);
  366. Row {
  367. cells: None,
  368. name,
  369. tree,
  370. }
  371. }
  372. fn render_xattr(&self, xattr: &Attribute, tree: TreeParams) -> Row {
  373. let name = TextCell::paint(
  374. self.theme.ui.perms.attribute,
  375. format!("{}=\"{}\"", xattr.name, xattr.value),
  376. );
  377. Row {
  378. cells: None,
  379. name,
  380. tree,
  381. }
  382. }
  383. pub fn render_file(&self, cells: TableRow, name: TextCell, tree: TreeParams) -> Row {
  384. Row {
  385. cells: Some(cells),
  386. name,
  387. tree,
  388. }
  389. }
  390. pub fn iterate_with_table(&'a self, table: Table<'a>, rows: Vec<Row>) -> TableIter<'a> {
  391. TableIter {
  392. tree_trunk: TreeTrunk::default(),
  393. total_width: table.widths().total(),
  394. table,
  395. inner: rows.into_iter(),
  396. tree_style: self.theme.ui.punctuation,
  397. }
  398. }
  399. pub fn iterate(&'a self, rows: Vec<Row>) -> Iter {
  400. Iter {
  401. tree_trunk: TreeTrunk::default(),
  402. inner: rows.into_iter(),
  403. tree_style: self.theme.ui.punctuation,
  404. }
  405. }
  406. }
  407. pub struct Row {
  408. /// Vector of cells to display.
  409. ///
  410. /// Most of the rows will be used to display filesโ€™ metadata, so this will
  411. /// almost always be `Some`, containing a vector of cells. It will only be
  412. /// `None` for a row displaying an attribute or error, neither of which
  413. /// have cells.
  414. pub cells: Option<TableRow>,
  415. /// This fileโ€™s name, in coloured output. The name is treated separately
  416. /// from the other cells, as it never requires padding.
  417. pub name: TextCell,
  418. /// Information used to determine which symbols to display in a tree.
  419. pub tree: TreeParams,
  420. }
  421. #[rustfmt::skip]
  422. pub struct TableIter<'a> {
  423. inner: VecIntoIter<Row>,
  424. table: Table<'a>,
  425. total_width: usize,
  426. tree_style: Style,
  427. tree_trunk: TreeTrunk,
  428. }
  429. impl<'a> Iterator for TableIter<'a> {
  430. type Item = TextCell;
  431. fn next(&mut self) -> Option<Self::Item> {
  432. self.inner.next().map(|row| {
  433. let mut cell = if let Some(cells) = row.cells {
  434. self.table.render(cells)
  435. } else {
  436. let mut cell = TextCell::default();
  437. cell.add_spaces(self.total_width);
  438. cell
  439. };
  440. for tree_part in self.tree_trunk.new_row(row.tree) {
  441. cell.push(self.tree_style.paint(tree_part.ascii_art()), 4);
  442. }
  443. // If any tree characters have been printed, then add an extra
  444. // space, which makes the output look much better.
  445. if !row.tree.is_at_root() {
  446. cell.add_spaces(1);
  447. }
  448. cell.append(row.name);
  449. cell
  450. })
  451. }
  452. }
  453. pub struct Iter {
  454. tree_trunk: TreeTrunk,
  455. tree_style: Style,
  456. inner: VecIntoIter<Row>,
  457. }
  458. impl Iterator for Iter {
  459. type Item = TextCell;
  460. fn next(&mut self) -> Option<Self::Item> {
  461. self.inner.next().map(|row| {
  462. let mut cell = TextCell::default();
  463. for tree_part in self.tree_trunk.new_row(row.tree) {
  464. cell.push(self.tree_style.paint(tree_part.ascii_art()), 4);
  465. }
  466. // If any tree characters have been printed, then add an extra
  467. // space, which makes the output look much better.
  468. if !row.tree.is_at_root() {
  469. cell.add_spaces(1);
  470. }
  471. cell.append(row.name);
  472. cell
  473. })
  474. }
  475. }