grid_details.rs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. //! The grid-details view lists several details views side-by-side.
  2. use std::io::{self, Write};
  3. use ansi_term::ANSIStrings;
  4. use term_grid as grid;
  5. use crate::fs::{Dir, File};
  6. use crate::fs::feature::git::GitCache;
  7. use crate::fs::filter::FileFilter;
  8. use crate::output::cell::TextCell;
  9. use crate::output::details::{Options as DetailsOptions, Row as DetailsRow, Render as DetailsRender};
  10. use crate::output::file_name::Options as FileStyle;
  11. use crate::output::grid::Options as GridOptions;
  12. use crate::output::table::{Table, Row as TableRow, Options as TableOptions};
  13. use crate::output::tree::{TreeParams, TreeDepth};
  14. use crate::theme::Theme;
  15. #[derive(PartialEq, Eq, Debug)]
  16. pub struct Options {
  17. pub grid: GridOptions,
  18. pub details: DetailsOptions,
  19. pub row_threshold: RowThreshold,
  20. }
  21. impl Options {
  22. pub fn to_details_options(&self) -> &DetailsOptions {
  23. &self.details
  24. }
  25. }
  26. /// The grid-details view can be configured to revert to just a details view
  27. /// (with one column) if it wouldn’t produce enough rows of output.
  28. ///
  29. /// Doing this makes the resulting output look a bit better: when listing a
  30. /// small directory of four files in four columns, the files just look spaced
  31. /// out and it’s harder to see what’s going on. So it can be enabled just for
  32. /// larger directory listings.
  33. #[derive(PartialEq, Eq, Debug, Copy, Clone)]
  34. pub enum RowThreshold {
  35. /// Only use grid-details view if it would result in at least this many
  36. /// rows of output.
  37. MinimumRows(usize),
  38. /// Use the grid-details view no matter what.
  39. AlwaysGrid,
  40. }
  41. pub struct Render<'a> {
  42. /// The directory that’s being rendered here.
  43. /// We need this to know which columns to put in the output.
  44. pub dir: Option<&'a Dir>,
  45. /// The files that have been read from the directory. They should all
  46. /// hold a reference to it.
  47. pub files: Vec<File<'a>>,
  48. /// How to colour various pieces of text.
  49. pub theme: &'a Theme,
  50. /// How to format filenames.
  51. pub file_style: &'a FileStyle,
  52. /// The grid part of the grid-details view.
  53. pub grid: &'a GridOptions,
  54. /// The details part of the grid-details view.
  55. pub details: &'a DetailsOptions,
  56. /// How to filter files after listing a directory. The files in this
  57. /// render will already have been filtered and sorted, but any directories
  58. /// that we recurse into will have to have this applied.
  59. pub filter: &'a FileFilter,
  60. /// The minimum number of rows that there need to be before grid-details
  61. /// mode is activated.
  62. pub row_threshold: RowThreshold,
  63. /// Whether we are skipping Git-ignored files.
  64. pub git_ignoring: bool,
  65. pub git: Option<&'a GitCache>,
  66. pub console_width: usize,
  67. }
  68. impl<'a> Render<'a> {
  69. /// Create a temporary Details render that gets used for the columns of
  70. /// the grid-details render that’s being generated.
  71. ///
  72. /// This includes an empty files vector because the files get added to
  73. /// the table in *this* file, not in details: we only want to insert every
  74. /// *n* files into each column’s table, not all of them.
  75. fn details_for_column(&self) -> DetailsRender<'a> {
  76. DetailsRender {
  77. dir: self.dir,
  78. files: Vec::new(),
  79. theme: self.theme,
  80. file_style: self.file_style,
  81. opts: self.details,
  82. recurse: None,
  83. filter: self.filter,
  84. git_ignoring: self.git_ignoring,
  85. git: self.git,
  86. }
  87. }
  88. /// Create a Details render for when this grid-details render doesn’t fit
  89. /// in the terminal (or something has gone wrong) and we have given up, or
  90. /// when the user asked for a grid-details view but the terminal width is
  91. /// not available, so we downgrade.
  92. pub fn give_up(self) -> DetailsRender<'a> {
  93. DetailsRender {
  94. dir: self.dir,
  95. files: self.files,
  96. theme: self.theme,
  97. file_style: self.file_style,
  98. opts: self.details,
  99. recurse: None,
  100. filter: self.filter,
  101. git_ignoring: self.git_ignoring,
  102. git: self.git,
  103. }
  104. }
  105. // This doesn’t take an IgnoreCache even though the details one does
  106. // because grid-details has no tree view.
  107. pub fn render<W: Write>(mut self, w: &mut W) -> io::Result<()> {
  108. if let Some((grid, width)) = self.find_fitting_grid() {
  109. write!(w, "{}", grid.fit_into_columns(width))
  110. }
  111. else {
  112. self.give_up().render(w)
  113. }
  114. }
  115. pub fn find_fitting_grid(&mut self) -> Option<(grid::Grid, grid::Width)> {
  116. let options = self.details.table.as_ref().expect("Details table options not given!");
  117. let drender = self.details_for_column();
  118. let (first_table, _) = self.make_table(options, &drender);
  119. let rows = self.files.iter()
  120. .map(|file| first_table.row_for_file(file, drender.show_xattr_hint(file)))
  121. .collect::<Vec<_>>();
  122. let file_names = self.files.iter()
  123. .map(|file| self.file_style.for_file(file, self.theme).paint().promote())
  124. .collect::<Vec<_>>();
  125. let mut last_working_grid = self.make_grid(1, options, &file_names, rows.clone(), &drender);
  126. if file_names.len() == 1 {
  127. return Some((last_working_grid, 1));
  128. }
  129. // If we can’t fit everything in a grid 100 columns wide, then
  130. // something has gone seriously awry
  131. for column_count in 2..100 {
  132. let grid = self.make_grid(column_count, options, &file_names, rows.clone(), &drender);
  133. let the_grid_fits = {
  134. let d = grid.fit_into_columns(column_count);
  135. d.width() <= self.console_width
  136. };
  137. if the_grid_fits {
  138. last_working_grid = grid;
  139. }
  140. if !the_grid_fits || column_count == file_names.len() {
  141. let last_column_count = if the_grid_fits { column_count } else { column_count - 1 };
  142. // If we’ve figured out how many columns can fit in the user’s terminal,
  143. // and it turns out there aren’t enough rows to make it worthwhile
  144. // (according to EXA_GRID_ROWS), then just resort to the lines view.
  145. if let RowThreshold::MinimumRows(thresh) = self.row_threshold {
  146. if last_working_grid.fit_into_columns(last_column_count).row_count() < thresh {
  147. return None;
  148. }
  149. }
  150. return Some((last_working_grid, last_column_count));
  151. }
  152. }
  153. None
  154. }
  155. fn make_table(&mut self, options: &'a TableOptions, drender: &DetailsRender<'_>) -> (Table<'a>, Vec<DetailsRow>) {
  156. match (self.git, self.dir) {
  157. (Some(g), Some(d)) => if ! g.has_anything_for(&d.path) { self.git = None },
  158. (Some(g), None) => if ! self.files.iter().any(|f| g.has_anything_for(&f.path)) { self.git = None },
  159. (None, _) => {/* Keep Git how it is */},
  160. }
  161. let mut table = Table::new(options, self.git, self.theme);
  162. let mut rows = Vec::new();
  163. if self.details.header {
  164. let row = table.header_row();
  165. table.add_widths(&row);
  166. rows.push(drender.render_header(row));
  167. }
  168. (table, rows)
  169. }
  170. fn make_grid(&mut self, column_count: usize, options: &'a TableOptions, file_names: &[TextCell], rows: Vec<TableRow>, drender: &DetailsRender<'_>) -> grid::Grid {
  171. let mut tables = Vec::new();
  172. for _ in 0 .. column_count {
  173. tables.push(self.make_table(options, drender));
  174. }
  175. let mut num_cells = rows.len();
  176. if self.details.header {
  177. num_cells += column_count;
  178. }
  179. let original_height = divide_rounding_up(rows.len(), column_count);
  180. let height = divide_rounding_up(num_cells, column_count);
  181. for (i, (file_name, row)) in file_names.iter().zip(rows.into_iter()).enumerate() {
  182. let index = if self.grid.across {
  183. i % column_count
  184. }
  185. else {
  186. i / original_height
  187. };
  188. let (ref mut table, ref mut rows) = tables[index];
  189. table.add_widths(&row);
  190. let details_row = drender.render_file(row, file_name.clone(), TreeParams::new(TreeDepth::root(), false));
  191. rows.push(details_row);
  192. }
  193. let columns = tables
  194. .into_iter()
  195. .map(|(table, details_rows)| {
  196. drender.iterate_with_table(table, details_rows)
  197. .collect::<Vec<_>>()
  198. })
  199. .collect::<Vec<_>>();
  200. let direction = if self.grid.across { grid::Direction::LeftToRight }
  201. else { grid::Direction::TopToBottom };
  202. let filling = grid::Filling::Spaces(4);
  203. let mut grid = grid::Grid::new(grid::GridOptions { direction, filling });
  204. if self.grid.across {
  205. for row in 0 .. height {
  206. for column in &columns {
  207. if row < column.len() {
  208. let cell = grid::Cell {
  209. contents: ANSIStrings(&column[row].contents).to_string(),
  210. width: *column[row].width,
  211. alignment: grid::Alignment::Left,
  212. };
  213. grid.add(cell);
  214. }
  215. }
  216. }
  217. }
  218. else {
  219. for column in &columns {
  220. for cell in column.iter() {
  221. let cell = grid::Cell {
  222. contents: ANSIStrings(&cell.contents).to_string(),
  223. width: *cell.width,
  224. alignment: grid::Alignment::Left,
  225. };
  226. grid.add(cell);
  227. }
  228. }
  229. }
  230. grid
  231. }
  232. }
  233. fn divide_rounding_up(a: usize, b: usize) -> usize {
  234. let mut result = a / b;
  235. if a % b != 0 {
  236. result += 1;
  237. }
  238. result
  239. }