view.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. use std::env::var_os;
  2. use output::Colours;
  3. use output::{View, Mode, grid, details};
  4. use output::table::{TimeTypes, Environment, SizeFormat, Options as TableOptions};
  5. use output::file_name::{Classify, FileStyle};
  6. use output::time::TimeFormat;
  7. use options::{flags, Misfire};
  8. use options::parser::Matches;
  9. use fs::feature::xattr;
  10. use info::filetype::FileExtensions;
  11. impl View {
  12. /// Determine which view to use and all of that view’s arguments.
  13. pub fn deduce(matches: &Matches) -> Result<View, Misfire> {
  14. let mode = Mode::deduce(matches)?;
  15. let colours = Colours::deduce(matches)?;
  16. let style = FileStyle::deduce(matches);
  17. Ok(View { mode, colours, style })
  18. }
  19. }
  20. impl Mode {
  21. /// Determine the mode from the command-line arguments.
  22. pub fn deduce(matches: &Matches) -> Result<Mode, Misfire> {
  23. use options::misfire::Misfire::*;
  24. let long = || {
  25. if matches.has(&flags::ACROSS) && !matches.has(&flags::GRID) {
  26. Err(Useless(&flags::ACROSS, true, &flags::LONG))
  27. }
  28. else if matches.has(&flags::ONE_LINE) {
  29. Err(Useless(&flags::ONE_LINE, true, &flags::LONG))
  30. }
  31. else {
  32. Ok(details::Options {
  33. table: Some(TableOptions::deduce(matches)?),
  34. header: matches.has(&flags::HEADER),
  35. xattr: xattr::ENABLED && matches.has(&flags::EXTENDED),
  36. })
  37. }
  38. };
  39. let long_options_scan = || {
  40. for option in &[ &flags::BINARY, &flags::BYTES, &flags::INODE, &flags::LINKS,
  41. &flags::HEADER, &flags::BLOCKS, &flags::TIME, &flags::GROUP ] {
  42. if matches.has(option) {
  43. return Err(Useless(*option, false, &flags::LONG));
  44. }
  45. }
  46. if cfg!(feature="git") && matches.has(&flags::GIT) {
  47. Err(Useless(&flags::GIT, false, &flags::LONG))
  48. }
  49. else if matches.has(&flags::LEVEL) && !matches.has(&flags::RECURSE) && !matches.has(&flags::TREE) {
  50. Err(Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE))
  51. }
  52. else if xattr::ENABLED && matches.has(&flags::EXTENDED) {
  53. Err(Useless(&flags::EXTENDED, false, &flags::LONG))
  54. }
  55. else {
  56. Ok(())
  57. }
  58. };
  59. let other_options_scan = || {
  60. if let Some(width) = TerminalWidth::deduce()?.width() {
  61. if matches.has(&flags::ONE_LINE) {
  62. if matches.has(&flags::ACROSS) {
  63. Err(Useless(&flags::ACROSS, true, &flags::ONE_LINE))
  64. }
  65. else {
  66. Ok(Mode::Lines)
  67. }
  68. }
  69. else if matches.has(&flags::TREE) {
  70. let details = details::Options {
  71. table: None,
  72. header: false,
  73. xattr: false,
  74. };
  75. Ok(Mode::Details(details))
  76. }
  77. else {
  78. let grid = grid::Options {
  79. across: matches.has(&flags::ACROSS),
  80. console_width: width,
  81. };
  82. Ok(Mode::Grid(grid))
  83. }
  84. }
  85. else {
  86. // If the terminal width couldn’t be matched for some reason, such
  87. // as the program’s stdout being connected to a file, then
  88. // fallback to the lines view.
  89. if matches.has(&flags::TREE) {
  90. let details = details::Options {
  91. table: None,
  92. header: false,
  93. xattr: false,
  94. };
  95. Ok(Mode::Details(details))
  96. }
  97. else {
  98. Ok(Mode::Lines)
  99. }
  100. }
  101. };
  102. if matches.has(&flags::LONG) {
  103. let details = long()?;
  104. if matches.has(&flags::GRID) {
  105. match other_options_scan()? {
  106. Mode::Grid(grid) => return Ok(Mode::GridDetails(grid, details)),
  107. others => return Ok(others),
  108. };
  109. }
  110. else {
  111. return Ok(Mode::Details(details));
  112. }
  113. }
  114. long_options_scan()?;
  115. other_options_scan()
  116. }
  117. }
  118. /// The width of the terminal requested by the user.
  119. #[derive(PartialEq, Debug)]
  120. enum TerminalWidth {
  121. /// The user requested this specific number of columns.
  122. Set(usize),
  123. /// The terminal was found to have this number of columns.
  124. Terminal(usize),
  125. /// The user didn’t request any particular terminal width.
  126. Unset,
  127. }
  128. impl TerminalWidth {
  129. /// Determine a requested terminal width from the command-line arguments.
  130. ///
  131. /// Returns an error if a requested width doesn’t parse to an integer.
  132. fn deduce() -> Result<TerminalWidth, Misfire> {
  133. if let Some(columns) = var_os("COLUMNS").and_then(|s| s.into_string().ok()) {
  134. match columns.parse() {
  135. Ok(width) => Ok(TerminalWidth::Set(width)),
  136. Err(e) => Err(Misfire::FailedParse(e)),
  137. }
  138. }
  139. else if let Some(width) = *TERM_WIDTH {
  140. Ok(TerminalWidth::Terminal(width))
  141. }
  142. else {
  143. Ok(TerminalWidth::Unset)
  144. }
  145. }
  146. fn width(&self) -> Option<usize> {
  147. match *self {
  148. TerminalWidth::Set(width) |
  149. TerminalWidth::Terminal(width) => Some(width),
  150. TerminalWidth::Unset => None,
  151. }
  152. }
  153. }
  154. impl TableOptions {
  155. fn deduce(matches: &Matches) -> Result<Self, Misfire> {
  156. Ok(TableOptions {
  157. env: Environment::load_all(),
  158. time_format: TimeFormat::deduce(matches)?,
  159. size_format: SizeFormat::deduce(matches)?,
  160. time_types: TimeTypes::deduce(matches)?,
  161. inode: matches.has(&flags::INODE),
  162. links: matches.has(&flags::LINKS),
  163. blocks: matches.has(&flags::BLOCKS),
  164. group: matches.has(&flags::GROUP),
  165. git: cfg!(feature="git") && matches.has(&flags::GIT),
  166. })
  167. }
  168. }
  169. impl SizeFormat {
  170. /// Determine which file size to use in the file size column based on
  171. /// the user’s options.
  172. ///
  173. /// The default mode is to use the decimal prefixes, as they are the
  174. /// most commonly-understood, and don’t involve trying to parse large
  175. /// strings of digits in your head. Changing the format to anything else
  176. /// involves the `--binary` or `--bytes` flags, and these conflict with
  177. /// each other.
  178. fn deduce(matches: &Matches) -> Result<SizeFormat, Misfire> {
  179. let binary = matches.has(&flags::BINARY);
  180. let bytes = matches.has(&flags::BYTES);
  181. match (binary, bytes) {
  182. (true, true ) => Err(Misfire::Conflict(&flags::BINARY, &flags::BYTES)),
  183. (true, false) => Ok(SizeFormat::BinaryBytes),
  184. (false, true ) => Ok(SizeFormat::JustBytes),
  185. (false, false) => Ok(SizeFormat::DecimalBytes),
  186. }
  187. }
  188. }
  189. impl TimeFormat {
  190. /// Determine how time should be formatted in timestamp columns.
  191. fn deduce(matches: &Matches) -> Result<TimeFormat, Misfire> {
  192. pub use output::time::{DefaultFormat, ISOFormat};
  193. const STYLES: &[&str] = &["default", "long-iso", "full-iso", "iso"];
  194. let word = match matches.get(&flags::TIME_STYLE) {
  195. Some(w) => w,
  196. None => return Ok(TimeFormat::DefaultFormat(DefaultFormat::new())),
  197. };
  198. if word == "default" {
  199. Ok(TimeFormat::DefaultFormat(DefaultFormat::new()))
  200. }
  201. else if word == "iso" {
  202. Ok(TimeFormat::ISOFormat(ISOFormat::new()))
  203. }
  204. else if word == "long-iso" {
  205. Ok(TimeFormat::LongISO)
  206. }
  207. else if word == "full-iso" {
  208. Ok(TimeFormat::FullISO)
  209. }
  210. else {
  211. Err(Misfire::bad_argument(&flags::TIME_STYLE, word, STYLES))
  212. }
  213. }
  214. }
  215. impl TimeTypes {
  216. /// Determine which of a file’s time fields should be displayed for it
  217. /// based on the user’s options.
  218. ///
  219. /// There are two separate ways to pick which fields to show: with a
  220. /// flag (such as `--modified`) or with a parameter (such as
  221. /// `--time=modified`). An error is signaled if both ways are used.
  222. ///
  223. /// It’s valid to show more than one column by passing in more than one
  224. /// option, but passing *no* options means that the user just wants to
  225. /// see the default set.
  226. fn deduce(matches: &Matches) -> Result<TimeTypes, Misfire> {
  227. let possible_word = matches.get(&flags::TIME);
  228. let modified = matches.has(&flags::MODIFIED);
  229. let created = matches.has(&flags::CREATED);
  230. let accessed = matches.has(&flags::ACCESSED);
  231. if let Some(word) = possible_word {
  232. if modified {
  233. return Err(Misfire::Useless(&flags::MODIFIED, true, &flags::TIME));
  234. }
  235. else if created {
  236. return Err(Misfire::Useless(&flags::CREATED, true, &flags::TIME));
  237. }
  238. else if accessed {
  239. return Err(Misfire::Useless(&flags::ACCESSED, true, &flags::TIME));
  240. }
  241. static TIMES: &[&str] = &["modified", "accessed", "created"];
  242. if word == "mod" || word == "modified" {
  243. Ok(TimeTypes { accessed: false, modified: true, created: false })
  244. }
  245. else if word == "acc" || word == "accessed" {
  246. Ok(TimeTypes { accessed: true, modified: false, created: false })
  247. }
  248. else if word == "cr" || word == "created" {
  249. Ok(TimeTypes { accessed: false, modified: false, created: true })
  250. }
  251. else {
  252. Err(Misfire::bad_argument(&flags::TIME, word, TIMES))
  253. }
  254. }
  255. else if modified || created || accessed {
  256. Ok(TimeTypes { accessed, modified, created })
  257. }
  258. else {
  259. Ok(TimeTypes::default())
  260. }
  261. }
  262. }
  263. /// Under what circumstances we should display coloured, rather than plain,
  264. /// output to the terminal.
  265. ///
  266. /// By default, we want to display the colours when stdout can display them.
  267. /// Turning them on when output is going to, say, a pipe, would make programs
  268. /// such as `grep` or `more` not work properly. So the `Automatic` mode does
  269. /// this check and only displays colours when they can be truly appreciated.
  270. #[derive(PartialEq, Debug)]
  271. enum TerminalColours {
  272. /// Display them even when output isn’t going to a terminal.
  273. Always,
  274. /// Display them when output is going to a terminal, but not otherwise.
  275. Automatic,
  276. /// Never display them, even when output is going to a terminal.
  277. Never,
  278. }
  279. impl Default for TerminalColours {
  280. fn default() -> TerminalColours {
  281. TerminalColours::Automatic
  282. }
  283. }
  284. impl TerminalColours {
  285. /// Determine which terminal colour conditions to use.
  286. fn deduce(matches: &Matches) -> Result<TerminalColours, Misfire> {
  287. const COLOURS: &[&str] = &["always", "auto", "never"];
  288. let word = match matches.get(&flags::COLOR).or_else(|| matches.get(&flags::COLOUR)) {
  289. Some(w) => w,
  290. None => return Ok(TerminalColours::default()),
  291. };
  292. if word == "always" {
  293. Ok(TerminalColours::Always)
  294. }
  295. else if word == "auto" || word == "automatic" {
  296. Ok(TerminalColours::Automatic)
  297. }
  298. else if word == "never" {
  299. Ok(TerminalColours::Never)
  300. }
  301. else {
  302. Err(Misfire::bad_argument(&flags::COLOR, word, COLOURS))
  303. }
  304. }
  305. }
  306. impl Colours {
  307. fn deduce(matches: &Matches) -> Result<Colours, Misfire> {
  308. use self::TerminalColours::*;
  309. let tc = TerminalColours::deduce(matches)?;
  310. if tc == Always || (tc == Automatic && TERM_WIDTH.is_some()) {
  311. let scale = matches.has(&flags::COLOR_SCALE) || matches.has(&flags::COLOUR_SCALE);
  312. Ok(Colours::colourful(scale))
  313. }
  314. else {
  315. Ok(Colours::plain())
  316. }
  317. }
  318. }
  319. impl FileStyle {
  320. fn deduce(matches: &Matches) -> FileStyle {
  321. let classify = Classify::deduce(matches);
  322. let exts = FileExtensions;
  323. FileStyle { classify, exts }
  324. }
  325. }
  326. impl Classify {
  327. fn deduce(matches: &Matches) -> Classify {
  328. if matches.has(&flags::CLASSIFY) { Classify::AddFileIndicators }
  329. else { Classify::JustFilenames }
  330. }
  331. }
  332. // Gets, then caches, the width of the terminal that exa is running in.
  333. // This gets used multiple times above, with no real guarantee of order,
  334. // so it’s easier to just cache it the first time it runs.
  335. lazy_static! {
  336. static ref TERM_WIDTH: Option<usize> = {
  337. use term::dimensions;
  338. dimensions().map(|t| t.0)
  339. };
  340. }