diff --git a/src/exa.rs b/src/exa.rs index 9079ea7..a3f4583 100644 --- a/src/exa.rs +++ b/src/exa.rs @@ -221,8 +221,8 @@ impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> { let View { ref mode, ref colours, ref style } = self.options.view; match *mode { - Mode::Lines => { - let r = lines::Render { files, colours, style }; + Mode::Lines(ref opts) => { + let r = lines::Render { files, colours, style, opts }; r.render(self.writer) } diff --git a/src/info/filetype.rs b/src/info/filetype.rs index f9157b7..a0ebfa1 100644 --- a/src/info/filetype.rs +++ b/src/info/filetype.rs @@ -8,6 +8,7 @@ use ansi_term::Style; use fs::File; use output::file_name::FileColours; +use output::icons::FileIcon; #[derive(Debug, Default, PartialEq)] @@ -115,3 +116,16 @@ impl FileColours for FileExtensions { }) } } + +impl FileIcon for FileExtensions { + fn icon_file(&self, file: &File) -> Option { + use output::icons::Icons; + + Some(match file { + f if self.is_music(f) || self.is_lossless(f) => Icons::Audio.value(), + f if self.is_image(f) => Icons::Image.value(), + f if self.is_video(f) => Icons::Video.value(), + _ => return None, + }) + } +} diff --git a/src/options/flags.rs b/src/options/flags.rs index 113e5af..a89c63e 100644 --- a/src/options/flags.rs +++ b/src/options/flags.rs @@ -40,6 +40,7 @@ pub static BINARY: Arg = Arg { short: Some(b'b'), long: "binary", takes_ pub static BYTES: Arg = Arg { short: Some(b'B'), long: "bytes", takes_value: TakesValue::Forbidden }; pub static GROUP: Arg = Arg { short: Some(b'g'), long: "group", takes_value: TakesValue::Forbidden }; pub static HEADER: Arg = Arg { short: Some(b'h'), long: "header", takes_value: TakesValue::Forbidden }; +pub static ICONS: Arg = Arg { short: None, long: "icons", takes_value: TakesValue::Forbidden }; pub static INODE: Arg = Arg { short: Some(b'i'), long: "inode", takes_value: TakesValue::Forbidden }; pub static LINKS: Arg = Arg { short: Some(b'H'), long: "links", takes_value: TakesValue::Forbidden }; pub static MODIFIED: Arg = Arg { short: Some(b'm'), long: "modified", takes_value: TakesValue::Forbidden }; @@ -66,7 +67,7 @@ pub static ALL_ARGS: Args = Args(&[ &ALL, &LIST_DIRS, &LEVEL, &REVERSE, &SORT, &DIRS_FIRST, &IGNORE_GLOB, &GIT_IGNORE, &ONLY_DIRS, - &BINARY, &BYTES, &GROUP, &HEADER, &INODE, &LINKS, &MODIFIED, &CHANGED, + &BINARY, &BYTES, &GROUP, &HEADER, &ICONS, &INODE, &LINKS, &MODIFIED, &CHANGED, &BLOCKS, &TIME, &ACCESSED, &CREATED, &TIME_STYLE, &GIT, &EXTENDED, diff --git a/src/options/view.rs b/src/options/view.rs index 87e65d1..23e9a44 100644 --- a/src/options/view.rs +++ b/src/options/view.rs @@ -1,4 +1,4 @@ -use output::{View, Mode, grid, details}; +use output::{View, Mode, grid, details, lines}; use output::grid_details::{self, RowThreshold}; use output::table::{TimeTypes, Environment, SizeFormat, Columns, Options as TableOptions}; use output::time::TimeFormat; @@ -41,6 +41,7 @@ impl Mode { table: Some(TableOptions::deduce(matches, vars)?), header: matches.has(&flags::HEADER)?, xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?, + icons: matches.has(&flags::ICONS)?, }) } }; @@ -52,7 +53,8 @@ impl Mode { Err(Useless(&flags::ACROSS, true, &flags::ONE_LINE)) } else { - Ok(Mode::Lines) + let lines = lines::Options { icons: matches.has(&flags::ICONS)? }; + Ok(Mode::Lines(lines)) } } else if matches.has(&flags::TREE)? { @@ -60,6 +62,7 @@ impl Mode { table: None, header: false, xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?, + icons: matches.has(&flags::ICONS)?, }; Ok(Mode::Details(details)) @@ -68,11 +71,13 @@ impl Mode { let grid = grid::Options { across: matches.has(&flags::ACROSS)?, console_width: width, + icons: matches.has(&flags::ICONS)?, }; Ok(Mode::Grid(grid)) } } + // If the terminal width couldn’t be matched for some reason, such // as the program’s stdout being connected to a file, then // fallback to the lines view. @@ -81,12 +86,14 @@ impl Mode { table: None, header: false, xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?, + icons: matches.has(&flags::ICONS)?, }; Ok(Mode::Details(details)) } else { - Ok(Mode::Lines) + let lines = lines::Options { icons: matches.has(&flags::ICONS)?, }; + Ok(Mode::Lines(lines)) } }; @@ -380,7 +387,7 @@ mod test { static TEST_ARGS: &[&Arg] = &[ &flags::BINARY, &flags::BYTES, &flags::TIME_STYLE, &flags::TIME, &flags::MODIFIED, &flags::CHANGED, - &flags::CREATED, &flags::ACCESSED, + &flags::CREATED, &flags::ACCESSED, &flags::ICONS, &flags::HEADER, &flags::GROUP, &flags::INODE, &flags::GIT, &flags::LINKS, &flags::BLOCKS, &flags::LONG, &flags::LEVEL, &flags::GRID, &flags::ACROSS, &flags::ONE_LINE ]; @@ -563,19 +570,22 @@ mod test { mod views { use super::*; use output::grid::Options as GridOptions; + use output::lines::Options as LineOptions; // Default test!(empty: Mode <- [], None; Both => like Ok(Mode::Grid(_))); // Grid views - test!(original_g: Mode <- ["-G"], None; Both => like Ok(Mode::Grid(GridOptions { across: false, console_width: _ }))); - test!(grid: Mode <- ["--grid"], None; Both => like Ok(Mode::Grid(GridOptions { across: false, console_width: _ }))); - test!(across: Mode <- ["--across"], None; Both => like Ok(Mode::Grid(GridOptions { across: true, console_width: _ }))); - test!(gracross: Mode <- ["-xG"], None; Both => like Ok(Mode::Grid(GridOptions { across: true, console_width: _ }))); + test!(original_g: Mode <- ["-G"], None; Both => like Ok(Mode::Grid(GridOptions { across: false, console_width: _, icons: _ }))); + test!(grid: Mode <- ["--grid"], None; Both => like Ok(Mode::Grid(GridOptions { across: false, console_width: _, icons: _ }))); + test!(across: Mode <- ["--across"], None; Both => like Ok(Mode::Grid(GridOptions { across: true, console_width: _, icons: _ }))); + test!(gracross: Mode <- ["-xG"], None; Both => like Ok(Mode::Grid(GridOptions { across: true, console_width: _, icons: _ }))); + test!(icons: Mode <- ["--icons"], None; Both => like Ok(Mode::Grid(GridOptions { across: _, console_width: _, icons: true}))); // Lines views - test!(lines: Mode <- ["--oneline"], None; Both => like Ok(Mode::Lines)); - test!(prima: Mode <- ["-1"], None; Both => like Ok(Mode::Lines)); + test!(lines: Mode <- ["--oneline"], None; Both => like Ok(Mode::Lines(LineOptions{ icons: _ }))); + test!(prima: Mode <- ["-1"], None; Both => like Ok(Mode::Lines(LineOptions{ icons: _ }))); + test!(line_icon: Mode <- ["-1", "--icons"], None; Both => like Ok(Mode::Lines(LineOptions { icons: true }))); // Details views test!(long: Mode <- ["--long"], None; Both => like Ok(Mode::Details(_))); diff --git a/src/output/details.rs b/src/output/details.rs index e30c2b9..577ed74 100644 --- a/src/output/details.rs +++ b/src/output/details.rs @@ -64,7 +64,7 @@ use std::io::{Write, Error as IOError, Result as IOResult}; use std::path::PathBuf; use std::vec::IntoIter as VecIntoIter; -use ansi_term::Style; +use ansi_term::{ANSIGenericString, Style}; use fs::{Dir, File}; use fs::dir_action::RecurseOptions; @@ -77,6 +77,7 @@ use output::cell::TextCell; use output::tree::{TreeTrunk, TreeParams, TreeDepth}; use output::file_name::FileStyle; use output::table::{Table, Options as TableOptions, Row as TableRow}; +use output::icons::painted_icon; use scoped_threadpool::Pool; @@ -105,6 +106,9 @@ pub struct Options { /// Whether to show each file's extended attributes. pub xattr: bool, + + /// Enables --icons mode + pub icons: bool, } @@ -132,6 +136,7 @@ struct Egg<'a> { errors: Vec<(IOError, Option)>, dir: Option, file: &'a File<'a>, + icon: Option, } impl<'a> AsRef> for Egg<'a> { @@ -194,7 +199,7 @@ impl<'a> Render<'a> { let table = table.as_ref(); for file in src { - let file_eggs = file_eggs.clone(); + let file_eggs = Arc::clone(&file_eggs); scoped.execute(move || { let mut errors = Vec::new(); @@ -255,7 +260,11 @@ impl<'a> Render<'a> { } }; - let egg = Egg { table_row, xattrs, errors, dir, file }; + let icon = if self.opts.icons { + Some(painted_icon(&file, &self.style)) + } else { None }; + + let egg = Egg { table_row, xattrs, errors, dir, file, icon }; file_eggs.lock().unwrap().push(egg); }); } @@ -271,12 +280,20 @@ impl<'a> Render<'a> { t.add_widths(row); } + let mut name_cell = TextCell::default(); + if let Some(icon) = egg.icon { + name_cell.push(ANSIGenericString::from(icon), 2) + } + name_cell.append(self.style.for_file(&egg.file, self.colours) + .with_link_paths() + .paint() + .promote()); + + let row = Row { tree: tree_params, cells: egg.table_row, - name: self.style.for_file(&egg.file, self.colours) - .with_link_paths() - .paint().promote(), + name: name_cell, }; rows.push(row); diff --git a/src/output/grid.rs b/src/output/grid.rs index c07e7aa..876e100 100644 --- a/src/output/grid.rs +++ b/src/output/grid.rs @@ -5,12 +5,15 @@ use term_grid as tg; use fs::File; use style::Colours; use output::file_name::FileStyle; +use output::icons::painted_icon; +use output::cell::DisplayWidth; #[derive(PartialEq, Debug, Copy, Clone)] pub struct Options { pub across: bool, pub console_width: usize, + pub icons: bool, } impl Options { @@ -38,11 +41,16 @@ impl<'a> Render<'a> { grid.reserve(self.files.len()); for file in &self.files { + let icon = if self.opts.icons { Some(painted_icon(&file, &self.style)) } else { None }; let filename = self.style.for_file(file, self.colours).paint(); - let width = filename.width(); + let width = if self.opts.icons { + DisplayWidth::from(2) + filename.width() + } else { + filename.width() + }; grid.add(tg::Cell { - contents: filename.strings().to_string(), + contents: format!("{icon}{filename}", icon=&icon.unwrap_or("".to_string()), filename=filename.strings().to_string()), width: *width, }); } diff --git a/src/output/grid_details.rs b/src/output/grid_details.rs index 408dc9e..ca2ffc8 100644 --- a/src/output/grid_details.rs +++ b/src/output/grid_details.rs @@ -2,7 +2,7 @@ use std::io::{Write, Result as IOResult}; -use ansi_term::ANSIStrings; +use ansi_term::{ANSIGenericString, ANSIStrings}; use term_grid as grid; use fs::{Dir, File}; @@ -17,7 +17,7 @@ use output::grid::Options as GridOptions; use output::file_name::FileStyle; use output::table::{Table, Row as TableRow, Options as TableOptions}; use output::tree::{TreeParams, TreeDepth}; - +use output::icons::painted_icon; #[derive(Debug)] pub struct Options { @@ -135,7 +135,17 @@ impl<'a> Render<'a> { .collect::>(); let file_names = self.files.iter() - .map(|file| self.style.for_file(file, self.colours).paint().promote()) + .map(|file| { + if self.details.icons { + let mut icon_cell = TextCell::default(); + icon_cell.push(ANSIGenericString::from(painted_icon(&file, &self.style)), 2); + let file_cell = self.style.for_file(file, self.colours).paint().promote(); + icon_cell.append(file_cell); + icon_cell + } else { + self.style.for_file(file, self.colours).paint().promote() + } + }) .collect::>(); let mut last_working_table = self.make_grid(1, options, git, &file_names, rows.clone(), &drender); diff --git a/src/output/icons.rs b/src/output/icons.rs new file mode 100644 index 0000000..799ead0 --- /dev/null +++ b/src/output/icons.rs @@ -0,0 +1,120 @@ +use ansi_term::Style; +use fs::File; +use info::filetype::FileExtensions; +use output::file_name::FileStyle; + +pub trait FileIcon { + fn icon_file(&self, file: &File) -> Option; +} + +pub enum Icons { + Audio, + Image, + Video, +} + +impl Icons { + pub fn value(&self) -> char { + match *self { + Icons::Audio => '\u{f001}', + Icons::Image => '\u{f1c5}', + Icons::Video => '\u{f03d}', + } + } +} + +pub fn painted_icon(file: &File, style: &FileStyle) -> String { + let file_icon = icon(&file).to_string(); + let painted = style.exts + .colour_file(&file) + .map_or(file_icon.to_string(), |c| { + // Remove underline from icon + if c.is_underline { + match c.foreground { + Some(color) => Style::from(color).paint(file_icon).to_string(), + None => Style::default().paint(file_icon).to_string(), + } + } else { + c.paint(file_icon).to_string() + } + }); + format!("{} ", painted) +} + +fn icon(file: &File) -> char { + let extensions = Box::new(FileExtensions); + if file.is_directory() { '\u{f115}' } + else if let Some(icon) = extensions.icon_file(file) { icon } + else { + if let Some(ext) = file.ext.as_ref() { + match ext.as_str() { + "ai" => '\u{e7b4}', + "android" => '\u{e70e}', + "apple" => '\u{f179}', + "avro" => '\u{e60b}', + "c" => '\u{e61e}', + "clj" => '\u{e768}', + "coffee" => '\u{f0f4}', + "conf" => '\u{e615}', + "cpp" => '\u{e61d}', + "css" => '\u{e749}', + "d" => '\u{e7af}', + "dart" => '\u{e798}', + "db" => '\u{f1c0}', + "diff" => '\u{f440}', + "doc" => '\u{f1c2}', + "ebook" => '\u{e28b}', + "env" => '\u{f462}', + "epub" => '\u{e28a}', + "erl" => '\u{e7b1}', + "font" => '\u{f031}', + "gform" => '\u{f298}', + "git" => '\u{f1d3}', + "go" => '\u{e626}', + "hs" => '\u{e777}', + "html" => '\u{f13b}', + "iml" => '\u{e7b5}', + "java" => '\u{e204}', + "js" => '\u{e74e}', + "json" => '\u{e60b}', + "jsx" => '\u{e7ba}', + "less" => '\u{e758}', + "log" => '\u{f18d}', + "lua" => '\u{e620}', + "md" => '\u{f48a}', + "mustache" => '\u{e60f}', + "npmignore" => '\u{e71e}', + "pdf" => '\u{f1c1}', + "php" => '\u{e73d}', + "pl" => '\u{e769}', + "ppt" => '\u{f1c4}', + "psd" => '\u{e7b8}', + "py" => '\u{e606}', + "r" => '\u{f25d}', + "rb" => '\u{e21e}', + "rdb" => '\u{e76d}', + "rs" => '\u{e7a8}', + "rss" => '\u{f09e}', + "rubydoc" => '\u{e73b}', + "sass" => '\u{e603}', + "scala" => '\u{e737}', + "shell" => '\u{f489}', + "sqlite3" => '\u{e7c4}', + "styl" => '\u{e600}', + "tex" => '\u{e600}', + "ts" => '\u{e628}', + "twig" => '\u{e61c}', + "txt" => '\u{f15c}', + "video" => '\u{f03d}', + "vim" => '\u{e62b}', + "xls" => '\u{f1c3}', + "xml" => '\u{e619}', + "yml" => '\u{f481}', + "zip" => '\u{f410}', + _ => '\u{f15b}' + } + } else { + '\u{f15b}' + } + } +} diff --git a/src/output/lines.rs b/src/output/lines.rs index 31ed017..5326a5d 100644 --- a/src/output/lines.rs +++ b/src/output/lines.rs @@ -1,24 +1,40 @@ use std::io::{Write, Result as IOResult}; -use ansi_term::ANSIStrings; +use ansi_term::{ANSIStrings, ANSIGenericString}; use fs::File; use output::file_name::{FileName, FileStyle}; use style::Colours; +use output::icons::painted_icon; +use output::cell::TextCell; +#[derive(PartialEq, Debug, Copy, Clone)] +pub struct Options { + pub icons: bool +} /// The lines view literally just displays each file, line-by-line. pub struct Render<'a> { pub files: Vec>, pub colours: &'a Colours, pub style: &'a FileStyle, + pub opts: &'a Options, } impl<'a> Render<'a> { pub fn render(&self, w: &mut W) -> IOResult<()> { for file in &self.files { let name_cell = self.render_file(file).paint(); - writeln!(w, "{}", ANSIStrings(&name_cell))?; + if self.opts.icons { + // Create a TextCell for the icon then append the text to it + let mut cell = TextCell::default(); + let icon = painted_icon(&file, self.style); + cell.push(ANSIGenericString::from(icon), 2); + cell.append(name_cell.promote()); + writeln!(w, "{}", ANSIStrings(&cell))?; + } else { + writeln!(w, "{}", ANSIStrings(&name_cell))?; + } } Ok(()) diff --git a/src/output/mod.rs b/src/output/mod.rs index ebbca34..f2761e8 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -8,6 +8,7 @@ pub mod details; pub mod file_name; pub mod grid_details; pub mod grid; +pub mod icons; pub mod lines; pub mod render; pub mod table; @@ -34,5 +35,5 @@ pub enum Mode { Grid(grid::Options), Details(details::Options), GridDetails(grid_details::Options), - Lines, + Lines(lines::Options), }