table.rs 16 KB

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