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 62 63 use std::io::{self, Write}; 64 use std::mem::MaybeUninit; 65 use std::path::PathBuf; 66 use std::vec::IntoIter as VecIntoIter; 67 68 use ansi_term::Style; 69 use scoped_threadpool::Pool; 70 71 use crate::fs::{Dir, File}; 72 use crate::fs::dir_action::RecurseOptions; 73 use crate::fs::feature::git::GitCache; 74 use crate::fs::feature::xattr::{Attribute, FileAttributes}; 75 use crate::fs::filter::FileFilter; 76 use crate::output::cell::TextCell; 77 use crate::output::file_name::Options as FileStyle; 78 use crate::output::table::{Table, Options as TableOptions, Row as TableRow}; 79 use crate::output::tree::{TreeTrunk, TreeParams, TreeDepth}; 80 use crate::theme::Theme; 81 82 83 /// With the **Details** view, the output gets formatted into columns, with 84 /// each `Column` object showing some piece of information about the file, 85 /// such as its size, or its permissions. 86 /// 87 /// To do this, the results have to be written to a table, instead of 88 /// displaying each file immediately. Then, the width of each column can be 89 /// calculated based on the individual results, and the fields are padded 90 /// during output. 91 /// 92 /// Almost all the heavy lifting is done in a Table object, which handles the 93 /// columns for each row. 94 #[derive(PartialEq, Debug)] 95 pub struct Options { 96 97 /// Options specific to drawing a table. 98 /// 99 /// Directories themselves can pick which columns are *added* to this 100 /// list, such as the Git column. 101 pub table: Option<TableOptions>, 102 103 /// Whether to show a header line or not. 104 pub header: bool, 105 106 /// Whether to show each file’s extended attributes. 107 pub xattr: bool, 108 } 109 110 111 pub struct Render<'a> { 112 pub dir: Option<&'a Dir>, 113 pub files: Vec<File<'a>>, 114 pub theme: &'a Theme, 115 pub file_style: &'a FileStyle, 116 pub opts: &'a Options, 117 118 /// Whether to recurse through directories with a tree view, and if so, 119 /// which options to use. This field is only relevant here if the `tree` 120 /// field of the RecurseOptions is `true`. 121 pub recurse: Option<RecurseOptions>, 122 123 /// How to sort and filter the files after getting their details. 124 pub filter: &'a FileFilter, 125 126 /// Whether we are skipping Git-ignored files. 127 pub git_ignoring: bool, 128 129 pub git: Option<&'a GitCache>, 130 } 131 132 133 struct Egg<'a> { 134 table_row: Option<TableRow>, 135 xattrs: Vec<Attribute>, 136 errors: Vec<(io::Error, Option<PathBuf>)>, 137 dir: Option<Dir>, 138 file: &'a File<'a>, 139 } 140 141 impl<'a> AsRef<File<'a>> for Egg<'a> { as_ref(&self) -> &File<'a>142 fn as_ref(&self) -> &File<'a> { 143 self.file 144 } 145 } 146 147 148 impl<'a> Render<'a> { render<W: Write>(mut self, w: &mut W) -> io::Result<()>149 pub fn render<W: Write>(mut self, w: &mut W) -> io::Result<()> { 150 let mut pool = Pool::new(num_cpus::get() as u32); 151 let mut rows = Vec::new(); 152 153 if let Some(ref table) = self.opts.table { 154 match (self.git, self.dir) { 155 (Some(g), Some(d)) => if ! g.has_anything_for(&d.path) { self.git = None }, 156 (Some(g), None) => if ! self.files.iter().any(|f| g.has_anything_for(&f.path)) { self.git = None }, 157 (None, _) => {/* Keep Git how it is */}, 158 } 159 160 let mut table = Table::new(table, self.git, &self.theme); 161 162 if self.opts.header { 163 let header = table.header_row(); 164 table.add_widths(&header); 165 rows.push(self.render_header(header)); 166 } 167 168 // This is weird, but I can’t find a way around it: 169 // https://internals.rust-lang.org/t/should-option-mut-t-implement-copy/3715/6 170 let mut table = Some(table); 171 self.add_files_to_table(&mut pool, &mut table, &mut rows, &self.files, TreeDepth::root()); 172 173 for row in self.iterate_with_table(table.unwrap(), rows) { 174 writeln!(w, "{}", row.strings())? 175 } 176 } 177 else { 178 self.add_files_to_table(&mut pool, &mut None, &mut rows, &self.files, TreeDepth::root()); 179 180 for row in self.iterate(rows) { 181 writeln!(w, "{}", row.strings())? 182 } 183 } 184 185 Ok(()) 186 } 187 188 /// Adds files to the table, possibly recursively. This is easily 189 /// parallelisable, and uses a pool of threads. add_files_to_table<'dir>(&self, pool: &mut Pool, table: &mut Option<Table<'a>>, rows: &mut Vec<Row>, src: &[File<'dir>], depth: TreeDepth)190 fn add_files_to_table<'dir>(&self, pool: &mut Pool, table: &mut Option<Table<'a>>, rows: &mut Vec<Row>, src: &[File<'dir>], depth: TreeDepth) { 191 use std::sync::{Arc, Mutex}; 192 use log::*; 193 use crate::fs::feature::xattr; 194 195 let mut file_eggs = (0..src.len()).map(|_| MaybeUninit::uninit()).collect::<Vec<_>>(); 196 197 pool.scoped(|scoped| { 198 let file_eggs = Arc::new(Mutex::new(&mut file_eggs)); 199 let table = table.as_ref(); 200 201 for (idx, file) in src.iter().enumerate() { 202 let file_eggs = Arc::clone(&file_eggs); 203 204 scoped.execute(move || { 205 let mut errors = Vec::new(); 206 let mut xattrs = Vec::new(); 207 208 // There are three “levels” of extended attribute support: 209 // 210 // 1. If we’re compiling without that feature, then 211 // exa pretends all files have no attributes. 212 // 2. If the feature is enabled and the --extended flag 213 // has been specified, then display an @ in the 214 // permissions column for files with attributes, the 215 // names of all attributes and their lengths, and any 216 // errors encountered when getting them. 217 // 3. If the --extended flag *hasn’t* been specified, then 218 // display the @, but don’t display anything else. 219 // 220 // For a while, exa took a stricter approach to (3): 221 // if an error occurred while checking a file’s xattrs to 222 // see if it should display the @, exa would display that 223 // error even though the attributes weren’t actually being 224 // shown! This was confusing, as users were being shown 225 // errors for something they didn’t explicitly ask for, 226 // and just cluttered up the output. So now errors aren’t 227 // printed unless the user passes --extended to signify 228 // that they want to see them. 229 230 if xattr::ENABLED { 231 match file.path.attributes() { 232 Ok(xs) => { 233 xattrs.extend(xs); 234 } 235 Err(e) => { 236 if self.opts.xattr { 237 errors.push((e, None)); 238 } 239 else { 240 error!("Error looking up xattr for {:?}: {:#?}", file.path, e); 241 } 242 } 243 } 244 } 245 246 let table_row = table.as_ref() 247 .map(|t| t.row_for_file(file, ! xattrs.is_empty())); 248 249 if ! self.opts.xattr { 250 xattrs.clear(); 251 } 252 253 let mut dir = None; 254 if let Some(r) = self.recurse { 255 if file.is_directory() && r.tree && ! r.is_too_deep(depth.0) { 256 match file.to_dir() { 257 Ok(d) => { 258 dir = Some(d); 259 } 260 Err(e) => { 261 errors.push((e, None)); 262 } 263 } 264 } 265 }; 266 267 let egg = Egg { table_row, xattrs, errors, dir, file }; 268 unsafe { std::ptr::write(file_eggs.lock().unwrap()[idx].as_mut_ptr(), egg) } 269 }); 270 } 271 }); 272 273 // this is safe because all entries have been initialized above 274 let mut file_eggs = unsafe { std::mem::transmute::<_, Vec<Egg<'_>>>(file_eggs) }; 275 self.filter.sort_files(&mut file_eggs); 276 277 for (tree_params, egg) in depth.iterate_over(file_eggs.into_iter()) { 278 let mut files = Vec::new(); 279 let mut errors = egg.errors; 280 281 if let (Some(ref mut t), Some(row)) = (table.as_mut(), egg.table_row.as_ref()) { 282 t.add_widths(row); 283 } 284 285 let file_name = self.file_style.for_file(egg.file, self.theme) 286 .with_link_paths() 287 .paint() 288 .promote(); 289 290 let row = Row { 291 tree: tree_params, 292 cells: egg.table_row, 293 name: file_name, 294 }; 295 296 rows.push(row); 297 298 if let Some(ref dir) = egg.dir { 299 for file_to_add in dir.files(self.filter.dot_filter, self.git, self.git_ignoring) { 300 match file_to_add { 301 Ok(f) => { 302 files.push(f); 303 } 304 Err((path, e)) => { 305 errors.push((e, Some(path))); 306 } 307 } 308 } 309 310 self.filter.filter_child_files(&mut files); 311 312 if ! files.is_empty() { 313 for xattr in egg.xattrs { 314 rows.push(self.render_xattr(&xattr, TreeParams::new(depth.deeper(), false))); 315 } 316 317 for (error, path) in errors { 318 rows.push(self.render_error(&error, TreeParams::new(depth.deeper(), false), path)); 319 } 320 321 self.add_files_to_table(pool, table, rows, &files, depth.deeper()); 322 continue; 323 } 324 } 325 326 let count = egg.xattrs.len(); 327 for (index, xattr) in egg.xattrs.into_iter().enumerate() { 328 let params = TreeParams::new(depth.deeper(), errors.is_empty() && index == count - 1); 329 let r = self.render_xattr(&xattr, params); 330 rows.push(r); 331 } 332 333 let count = errors.len(); 334 for (index, (error, path)) in errors.into_iter().enumerate() { 335 let params = TreeParams::new(depth.deeper(), index == count - 1); 336 let r = self.render_error(&error, params, path); 337 rows.push(r); 338 } 339 } 340 } 341 render_header(&self, header: TableRow) -> Row342 pub fn render_header(&self, header: TableRow) -> Row { 343 Row { 344 tree: TreeParams::new(TreeDepth::root(), false), 345 cells: Some(header), 346 name: TextCell::paint_str(self.theme.ui.header, "Name"), 347 } 348 } 349 render_error(&self, error: &io::Error, tree: TreeParams, path: Option<PathBuf>) -> Row350 fn render_error(&self, error: &io::Error, tree: TreeParams, path: Option<PathBuf>) -> Row { 351 use crate::output::file_name::Colours; 352 353 let error_message = match path { 354 Some(path) => format!("<{}: {}>", path.display(), error), 355 None => format!("<{}>", error), 356 }; 357 358 // TODO: broken_symlink() doesn’t quite seem like the right name for 359 // the style that’s being used here. Maybe split it in two? 360 let name = TextCell::paint(self.theme.broken_symlink(), error_message); 361 Row { cells: None, name, tree } 362 } 363 render_xattr(&self, xattr: &Attribute, tree: TreeParams) -> Row364 fn render_xattr(&self, xattr: &Attribute, tree: TreeParams) -> Row { 365 let name = TextCell::paint(self.theme.ui.perms.attribute, format!("{} (len {})", xattr.name, xattr.size)); 366 Row { cells: None, name, tree } 367 } 368 render_file(&self, cells: TableRow, name: TextCell, tree: TreeParams) -> Row369 pub fn render_file(&self, cells: TableRow, name: TextCell, tree: TreeParams) -> Row { 370 Row { cells: Some(cells), name, tree } 371 } 372 iterate_with_table(&'a self, table: Table<'a>, rows: Vec<Row>) -> TableIter<'a>373 pub fn iterate_with_table(&'a self, table: Table<'a>, rows: Vec<Row>) -> TableIter<'a> { 374 TableIter { 375 tree_trunk: TreeTrunk::default(), 376 total_width: table.widths().total(), 377 table, 378 inner: rows.into_iter(), 379 tree_style: self.theme.ui.punctuation, 380 } 381 } 382 iterate(&'a self, rows: Vec<Row>) -> Iter383 pub fn iterate(&'a self, rows: Vec<Row>) -> Iter { 384 Iter { 385 tree_trunk: TreeTrunk::default(), 386 inner: rows.into_iter(), 387 tree_style: self.theme.ui.punctuation, 388 } 389 } 390 } 391 392 393 pub struct Row { 394 395 /// Vector of cells to display. 396 /// 397 /// Most of the rows will be used to display files’ metadata, so this will 398 /// almost always be `Some`, containing a vector of cells. It will only be 399 /// `None` for a row displaying an attribute or error, neither of which 400 /// have cells. 401 pub cells: Option<TableRow>, 402 403 /// This file’s name, in coloured output. The name is treated separately 404 /// from the other cells, as it never requires padding. 405 pub name: TextCell, 406 407 /// Information used to determine which symbols to display in a tree. 408 pub tree: TreeParams, 409 } 410 411 412 pub struct TableIter<'a> { 413 inner: VecIntoIter<Row>, 414 table: Table<'a>, 415 416 total_width: usize, 417 tree_style: Style, 418 tree_trunk: TreeTrunk, 419 } 420 421 impl<'a> Iterator for TableIter<'a> { 422 type Item = TextCell; 423 next(&mut self) -> Option<Self::Item>424 fn next(&mut self) -> Option<Self::Item> { 425 self.inner.next().map(|row| { 426 let mut cell = 427 if let Some(cells) = row.cells { 428 self.table.render(cells) 429 } 430 else { 431 let mut cell = TextCell::default(); 432 cell.add_spaces(self.total_width); 433 cell 434 }; 435 436 for tree_part in self.tree_trunk.new_row(row.tree) { 437 cell.push(self.tree_style.paint(tree_part.ascii_art()), 4); 438 } 439 440 // If any tree characters have been printed, then add an extra 441 // space, which makes the output look much better. 442 if ! row.tree.is_at_root() { 443 cell.add_spaces(1); 444 } 445 446 cell.append(row.name); 447 cell 448 }) 449 } 450 } 451 452 453 pub struct Iter { 454 tree_trunk: TreeTrunk, 455 tree_style: Style, 456 inner: VecIntoIter<Row>, 457 } 458 459 impl Iterator for Iter { 460 type Item = TextCell; 461 next(&mut self) -> Option<Self::Item>462 fn next(&mut self) -> Option<Self::Item> { 463 self.inner.next().map(|row| { 464 let mut cell = TextCell::default(); 465 466 for tree_part in self.tree_trunk.new_row(row.tree) { 467 cell.push(self.tree_style.paint(tree_part.ascii_art()), 4); 468 } 469 470 // If any tree characters have been printed, then add an extra 471 // space, which makes the output look much better. 472 if ! row.tree.is_at_root() { 473 cell.add_spaces(1); 474 } 475 476 cell.append(row.name); 477 cell 478 }) 479 } 480 } 481