diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b60c097f..9bcd57242 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - In keeping with the coreutils change, add quotes and escapes for necessary filenames from [merelymyself](https://github.com/merelymyself) +- Add support for icon theme from [zwpaper](https://github.com/zwpaper) ## [0.23.1] - 2022-09-13 @@ -20,7 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Reduce the binary size and improve the performance from [sabify](https://github.com/sabify) ### Fixed -- Fix rendering issues in Windows from [meain](https://gitHub.com/meain) +- Fix rendering issues in Windows from [meain](https://github.com/meain) ## [0.22.0] - 2022-06-12 ### Added diff --git a/Cargo.lock b/Cargo.lock index 62665b1c1..872812e8f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -435,6 +435,7 @@ dependencies = [ "tempfile", "term_grid", "terminal_size", + "thiserror", "unicode-width", "url", "users", diff --git a/Cargo.toml b/Cargo.toml index 0f6807e75..3e4421a1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ libc = "0.2.*" human-sort = "0.2.2" term_grid = "0.1.*" terminal_size = "0.1.*" +thiserror = "1.0" chrono = "0.4.*" chrono-humanize = "0.1.*" unicode-width = "0.1.*" diff --git a/README.md b/README.md index 7a115cf95..9c8e0990c 100644 --- a/README.md +++ b/README.md @@ -233,9 +233,11 @@ header: false ## Theme -`lsd` can be configured with a theme file to set the colors. +`lsd` can be configured with theme files to set the colors or icons. -Theme can be configured in the [configuration file](#configuration)(color.theme), +### Color Theme + +Color theme can be configured in the [configuration file](#configuration)(color.theme), The valid theme configurations are: - `default`: the default color scheme shipped in `lsd` @@ -244,12 +246,12 @@ The valid theme configurations are: when configured with the `theme-file-name` which is a `yaml` file, `lsd` will look up the theme file in the following way: -- relative name: check the themes under XDG Base Directory, e.g. ~/.config/lsd/themes/.yaml +- relative name: check the XDG Base Directory, e.g. ~/.config/lsd/themes/.yaml - absolute name: use the file path and name to find theme file -Check [Theme file content](#theme-file-content) for details. +Check [Color Theme file content](#color-theme-file-content) for details. -### Theme file content +#### Color Theme file content Theme file use the [crossterm](https://crates.io/crates/crossterm) to configure the colors, check [crossterm](https://docs.rs/crossterm/0.20.0/crossterm/style/enum.Color.html) @@ -297,6 +299,46 @@ and then change its colors, the items missed would fallback to use the default c Please also notice that an empty theme is **NOT** supported due to [a bug in serde lib](https://github.com/dtolnay/serde-yaml/issues/86). +### Icon Theme + +Icon theme can be configured in a fixed location, `$XDG_CONFIG_DIR/lsd/icons.yaml`, +for example, `~/.config/lsd/icons.yaml` on macOS, +please check [Config file location](#config-file-location) to make sure where is `$XDG_CONFIG_DIR`. + +As the file name indicated, the icon theme file is a `yaml` file. + +Check [Icon Theme file content](#icon-theme-file-content) for details. + +#### Icon Theme file content + +`lsd` support 3 kinds of icon overrides, by `name`, by `filetype` and by `extension`. +The final set of icons used will be a combination of what is shipped with in `lsd` with overrides from config applied on top of it. +*You can find the default set of icons [here](src/theme/icon.rs).* + +Both nerd font glyphs and unicode emojis can be used for icons. You can find an example of icons customization below. + +```yaml +name: + .trash:  + .cargo:  + .emacs.d:  + a.out:  +extension: + go:  + hs:  + rs: 🦀 +filetype: + dir: 📂 + file: 📄 + pipe: 📩 + socket:  + executable:  + symlink-dir:  + symlink-file:  + device-char:  + device-block: ﰩ + special:  + ## External Configurations ### Required diff --git a/src/app.rs b/src/app.rs index 1e9357f65..971d01353 100644 --- a/src/app.rs +++ b/src/app.rs @@ -53,9 +53,9 @@ pub fn build() -> App<'static> { .arg( Arg::with_name("icon-theme") .long("icon-theme") + .default_value("fancy") .possible_value("fancy") .possible_value("unicode") - .default_value("fancy") .multiple_occurrences(true) .takes_value(true) .number_of_values(1) diff --git a/src/color.rs b/src/color.rs index c3bd938b9..4b761a397 100644 --- a/src/color.rs +++ b/src/color.rs @@ -1,14 +1,11 @@ -mod theme; - -use crossterm::style::{Attribute, ContentStyle, StyledContent, Stylize}; -use theme::Theme; - -pub use crate::flags::color::ThemeOption; - use crossterm::style::Color; +use crossterm::style::{Attribute, ContentStyle, StyledContent, Stylize}; use lscolors::{Indicator, LsColors}; use std::path::Path; +pub use crate::flags::color::ThemeOption; +use crate::theme::{color::ColorTheme, Theme}; + #[allow(dead_code)] #[derive(Hash, Debug, Eq, PartialEq, Clone)] pub enum Elem { @@ -71,7 +68,7 @@ impl Elem { matches!(self, Elem::Dir { uid: true } | Elem::File { uid: true, .. }) } - fn get_color(&self, theme: &theme::Theme) -> Color { + pub fn get_color(&self, theme: &ColorTheme) -> Color { match self { Elem::File { exec: true, @@ -131,7 +128,7 @@ impl Elem { pub type ColoredString = StyledContent; pub struct Colors { - theme: Option, + theme: Option, lscolors: Option, } @@ -139,8 +136,16 @@ impl Colors { pub fn new(t: ThemeOption) -> Self { let theme = match t { ThemeOption::NoColor => None, - ThemeOption::Default | ThemeOption::NoLscolors => Some(Theme::default()), - ThemeOption::Custom(ref file) => Some(Theme::from_path(file).unwrap_or_default()), + ThemeOption::Default | ThemeOption::NoLscolors => Some(Theme::default().color), + ThemeOption::Custom(ref file) => { + // TODO: drop the `themes` dir prefix, adding it here only for backwards compatibility + Some( + Theme::from_path::( + Path::new("themes").join(file).to_str().unwrap_or(file), + ) + .unwrap_or_default(), + ) + } }; let lscolors = match t { ThemeOption::Default | ThemeOption::Custom(_) => { @@ -301,8 +306,8 @@ fn to_content_style(ls: &lscolors::Style) -> ContentStyle { #[cfg(test)] mod tests { use super::Colors; - use crate::color::Theme; use crate::color::ThemeOption; + use crate::theme::color::ColorTheme; #[test] fn test_color_new_no_color_theme() { assert!(Colors::new(ThemeOption::NoColor).theme.is_none()); @@ -312,7 +317,7 @@ mod tests { fn test_color_new_default_theme() { assert_eq!( Colors::new(ThemeOption::Default).theme, - Some(Theme::default_dark()), + Some(ColorTheme::default_dark()), ); } @@ -320,7 +325,7 @@ mod tests { fn test_color_new_bad_custom_theme() { assert_eq!( Colors::new(ThemeOption::Custom("not-existed".to_string())).theme, - Some(Theme::default_dark()), + Some(ColorTheme::default_dark()), ); } } @@ -328,15 +333,15 @@ mod tests { #[cfg(test)] mod elem { use super::Elem; - use crate::color::{theme, Theme}; + use crate::theme::{color, color::ColorTheme}; use crossterm::style::Color; #[cfg(test)] - fn test_theme() -> Theme { - Theme { + fn test_theme() -> ColorTheme { + ColorTheme { user: Color::AnsiValue(230), // Cornsilk1 group: Color::AnsiValue(187), // LightYellow3 - permission: theme::Permission { + permission: color::Permission { read: Color::Green, write: Color::Yellow, exec: Color::Red, @@ -346,19 +351,19 @@ mod elem { acl: Color::DarkCyan, context: Color::Cyan, }, - file_type: theme::FileType { - file: theme::File { + file_type: color::FileType { + file: color::File { exec_uid: Color::AnsiValue(40), // Green3 uid_no_exec: Color::AnsiValue(184), // Yellow3 exec_no_uid: Color::AnsiValue(40), // Green3 no_exec_no_uid: Color::AnsiValue(184), // Yellow3 }, - dir: theme::Dir { + dir: color::Dir { uid: Color::AnsiValue(33), // DodgerBlue1 no_uid: Color::AnsiValue(33), // DodgerBlue1 }, pipe: Color::AnsiValue(44), // DarkTurquoise - symlink: theme::Symlink { + symlink: color::Symlink { default: Color::AnsiValue(44), // DarkTurquoise broken: Color::AnsiValue(124), // Red3 missing_target: Color::AnsiValue(124), // Red3 @@ -368,22 +373,22 @@ mod elem { socket: Color::AnsiValue(44), // DarkTurquoise special: Color::AnsiValue(44), // DarkTurquoise }, - date: theme::Date { + date: color::Date { hour_old: Color::AnsiValue(40), // Green3 day_old: Color::AnsiValue(42), // SpringGreen2 older: Color::AnsiValue(36), // DarkCyan }, - size: theme::Size { + size: color::Size { none: Color::AnsiValue(245), // Grey small: Color::AnsiValue(229), // Wheat1 medium: Color::AnsiValue(216), // LightSalmon1 large: Color::AnsiValue(172), // Orange3 }, - inode: theme::INode { + inode: color::INode { valid: Color::AnsiValue(13), // Pink invalid: Color::AnsiValue(245), // Grey }, - links: theme::Links { + links: color::Links { valid: Color::AnsiValue(13), // Pink invalid: Color::AnsiValue(245), // Grey }, diff --git a/src/core.rs b/src/core.rs index 407d42c9d..231437490 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,10 +1,7 @@ use crate::color::Colors; use crate::display; -use crate::flags::{ - ColorOption, Display, Flags, HyperlinkOption, IconOption, IconTheme, Layout, SortOrder, - ThemeOption, -}; -use crate::icon::{self, Icons}; +use crate::flags::{ColorOption, Display, Flags, HyperlinkOption, Layout, SortOrder, ThemeOption}; +use crate::icon::Icons; use crate::meta::Meta; use crate::{print_error, print_output, sort, ExitCode}; use std::path::PathBuf; @@ -47,11 +44,8 @@ impl Core { _ => flags.color.theme.clone(), }; - let icon_theme = match (tty_available, flags.icons.when, flags.icons.theme) { - (_, IconOption::Never, _) | (false, IconOption::Auto, _) => icon::Theme::NoIcon, - (_, _, IconTheme::Fancy) => icon::Theme::Fancy, - (_, _, IconTheme::Unicode) => icon::Theme::Unicode, - }; + let icon_when = flags.icons.when; + let icon_theme = flags.icons.theme.clone(); // TODO: Rework this so that flags passed downstream does not // have Auto option for any (icon, color, hyperlink). @@ -78,7 +72,7 @@ impl Core { Self { flags, colors: Colors::new(color_theme), - icons: Icons::new(icon_theme, icon_separator), + icons: Icons::new(tty_available, icon_when, icon_theme, icon_separator), sorters, } } diff --git a/src/display.rs b/src/display.rs index 21139a36f..83697ec2c 100644 --- a/src/display.rs +++ b/src/display.rs @@ -408,11 +408,11 @@ mod tests { use super::*; use crate::color; use crate::color::Colors; - use crate::flags::HyperlinkOption; + use crate::flags::{HyperlinkOption, IconOption, IconTheme as FlagTheme}; use crate::icon::Icons; use crate::meta::{FileType, Name}; use crate::Config; - use crate::{app, flags, icon, sort}; + use crate::{app, flags, sort}; use assert_fs::prelude::*; use std::path::Path; @@ -438,7 +438,7 @@ mod tests { let output = name .render( &Colors::new(color::ThemeOption::NoColor), - &Icons::new(icon::Theme::NoIcon, " ".to_string()), + &Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()), &DisplayOption::FileName, HyperlinkOption::Never, ) @@ -472,7 +472,7 @@ mod tests { let output = name .render( &Colors::new(color::ThemeOption::NoColor), - &Icons::new(icon::Theme::Fancy, " ".to_string()), + &Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()), &DisplayOption::FileName, HyperlinkOption::Never, ) @@ -505,7 +505,7 @@ mod tests { let output = name .render( &Colors::new(color::ThemeOption::NoLscolors), - &Icons::new(icon::Theme::NoIcon, " ".to_string()), + &Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()), &DisplayOption::FileName, HyperlinkOption::Never, ) @@ -546,7 +546,7 @@ mod tests { let output = name .render( &Colors::new(color::ThemeOption::NoColor), - &Icons::new(icon::Theme::NoIcon, " ".to_string()), + &Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()), &DisplayOption::FileName, HyperlinkOption::Never, ) @@ -609,7 +609,7 @@ mod tests { &metas, &flags, &Colors::new(color::ThemeOption::NoColor), - &Icons::new(icon::Theme::NoIcon, " ".to_string()), + &Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()), ); assert_eq!("one.d\n├── .hidden\n└── two\n", output); @@ -640,7 +640,7 @@ mod tests { &metas, &flags, &Colors::new(color::ThemeOption::NoColor), - &Icons::new(icon::Theme::NoIcon, " ".to_string()), + &Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()), ); let length_before_b = |i| -> usize { @@ -680,7 +680,7 @@ mod tests { &metas, &flags, &Colors::new(color::ThemeOption::NoColor), - &Icons::new(icon::Theme::NoIcon, " ".to_string()), + &Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()), ); assert_eq!(output.lines().nth(1).unwrap().chars().next().unwrap(), '└'); @@ -719,7 +719,7 @@ mod tests { &metas, &flags, &Colors::new(color::ThemeOption::NoColor), - &Icons::new(icon::Theme::NoIcon, " ".to_string()), + &Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()), ); assert!(output.ends_with("└── two\n")); @@ -749,7 +749,7 @@ mod tests { &metas, &flags, &Colors::new(color::ThemeOption::NoColor), - &Icons::new(icon::Theme::NoIcon, " ".to_string()), + &Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()), ); dir.close().unwrap(); @@ -782,7 +782,7 @@ mod tests { &metas, &flags, &Colors::new(color::ThemeOption::NoColor), - &Icons::new(icon::Theme::NoIcon, " ".to_string()), + &Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()), ); dir.close().unwrap(); diff --git a/src/flags/icons.rs b/src/flags/icons.rs index 481d37441..5db45885e 100644 --- a/src/flags/icons.rs +++ b/src/flags/icons.rs @@ -90,7 +90,7 @@ impl Configurable for IconOption { } /// The flag showing which icon theme to use. -#[derive(Clone, Debug, Copy, PartialEq, Eq, Deserialize, Default)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum IconTheme { Unicode, @@ -131,7 +131,7 @@ impl Configurable for IconTheme { /// this returns its corresponding variant in a [Some]. /// Otherwise this returns [None]. fn from_config(config: &Config) -> Option { - config.icons.as_ref().and_then(|icon| icon.theme) + config.icons.as_ref().and_then(|icon| icon.theme.clone()) } } diff --git a/src/icon.rs b/src/icon.rs index d7323d0e5..a3eb6bdc9 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -1,20 +1,10 @@ +use crate::flags::{IconOption, IconTheme as FlagTheme}; use crate::meta::{FileType, Name}; -use std::collections::HashMap; +use crate::theme::{icon::IconTheme, Theme}; pub struct Icons { - display_icons: bool, - icons_by_name: HashMap<&'static str, &'static str>, - icons_by_extension: HashMap<&'static str, &'static str>, - default_folder_icon: &'static str, - default_file_icon: &'static str, icon_separator: String, -} - -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum Theme { - NoIcon, - Fancy, - Unicode, + theme: Option, } // In order to add a new icon, write the unicode value like "\ue5fb" then @@ -22,558 +12,207 @@ pub enum Theme { // // s#\\u[0-9a-f]*#\=eval('"'.submatch(0).'"')# impl Icons { - pub fn new(theme: Theme, icon_separator: String) -> Self { - let display_icons = matches!(theme, Theme::Fancy | Theme::Unicode); - let (icons_by_name, icons_by_extension, default_file_icon, default_folder_icon) = - if theme == Theme::Fancy { - ( - Self::get_default_icons_by_name(), - Self::get_default_icons_by_extension(), - "\u{f016}", //  - "\u{f115}", //  - ) - } else { - ( - HashMap::new(), - HashMap::new(), - "\u{1f5cb}", // 🗋 - "\u{1f5c1}", // 🗁 - ) - }; + pub fn new(tty: bool, when: IconOption, theme: FlagTheme, icon_separator: String) -> Self { + let icon_theme = match (tty, when, theme) { + (_, IconOption::Never, _) | (false, IconOption::Auto, _) => None, + (_, _, FlagTheme::Fancy) => { + if let Ok(t) = Theme::from_path::("icons") { + Some(t) + } else { + Some(IconTheme::default()) + } + } + (_, _, FlagTheme::Unicode) => Some(IconTheme::unicode()), + }; Self { - display_icons, - icons_by_name, - icons_by_extension, - default_file_icon, - default_folder_icon, icon_separator, + theme: icon_theme, } } pub fn get(&self, name: &Name) -> String { - if !self.display_icons { - return String::new(); - } - - // Check file types - let file_type: FileType = name.file_type(); - let icon = match file_type { - FileType::SymLink { is_dir: true } => "\u{f482}", // "" - FileType::SymLink { is_dir: false } => "\u{f481}", // "" - FileType::Socket => "\u{f6a7}", // "" - FileType::Pipe => "\u{f731}", // "" - FileType::CharDevice => "\u{e601}", // "" - FileType::BlockDevice => "\u{fc29}", // "ﰩ" - FileType::Special => "\u{f2dc}", // "" - _ => { - // Use the known names - if let Some(icon) = self - .icons_by_name - .get(name.file_name().to_lowercase().as_str()) - { - icon - } - // Use the known extensions - else if let Some(icon) = name.extension().and_then(|extension| { - self.icons_by_extension - .get(extension.to_lowercase().as_str()) - }) { - icon - } else { - match file_type { - FileType::Directory { .. } => self.default_folder_icon, - // If a file has no extension and is executable, show an icon. - // Except for Windows, it marks everything as an executable. - #[cfg(not(windows))] - FileType::File { exec: true, .. } => "\u{f489}", // "" - _ => self.default_file_icon, + match &self.theme { + None => String::new(), + Some(t) => { + // Check file types + let file_type: FileType = name.file_type(); + let icon = match file_type { + FileType::SymLink { is_dir: true } => &t.filetype.symlink_dir, + FileType::SymLink { is_dir: false } => &t.filetype.symlink_file, + FileType::Socket => &t.filetype.socket, + FileType::Pipe => &t.filetype.pipe, + FileType::CharDevice => &t.filetype.device_char, + FileType::BlockDevice => &t.filetype.device_block, + FileType::Special => &t.filetype.special, + _ => { + if let Some(icon) = t.name.get(name.file_name().to_lowercase().as_str()) { + icon + } else if let Some(icon) = name + .extension() + .and_then(|ext| t.extension.get(ext.to_lowercase().as_str())) + { + icon + } else { + match file_type { + FileType::Directory { .. } => &t.filetype.dir, + // If a file has no extension and is executable, show an icon. + // Except for Windows, it marks everything as an executable. + #[cfg(not(windows))] + FileType::File { exec: true, .. } => &t.filetype.executable, + _ => &t.filetype.file, + } + } } - } - } - }; - - format!("{}{}", icon, self.icon_separator) - } - - fn get_default_icons_by_name() -> HashMap<&'static str, &'static str> { - // Note: filenames must be lower-case - HashMap::from([ - (".trash", "\u{f1f8}"), // "" - (".atom", "\u{e764}"), // "" - (".bash_profile", "\u{e615}"), // "" - (".bash_logout", "\u{e615}"), // "" - (".bashrc", "\u{f489}"), // "" - (".cargo", "\u{e7a8}"), // "" - (".clang-format", "\u{e615}"), // "" - (".config", "\u{e5fc}"), // "" - (".emacs.d", "\u{e779}"), // "" - (".doom.d", "\u{e779}"), // "" - (".git", "\u{e5fb}"), // "" - (".git-credentials", "\u{e60a}"), // "" - (".gitattributes", "\u{f1d3}"), // "" - (".gitconfig", "\u{f1d3}"), // "" - (".github", "\u{e5fd}"), // "" - (".gitignore", "\u{f1d3}"), // "" - (".gitlab-ci.yml", "\u{f296}"), // "" - (".gitmodules", "\u{f1d3}"), // "" - (".htaccess", "\u{e615}"), // "" - (".htpasswd", "\u{e615}"), // "" - (".inputrc", "\u{e615}"), // "" - (".node_repl_history", "\u{e718}"), // "" - (".npm", "\u{e5fa}"), // "" - (".profile", "\u{f68c}"), // "" - (".python_history", "\u{e606}"), // "" - (".release.toml", "\u{e7a8}"), // "" - (".rvm", "\u{e21e}"), // "" - (".ssh", "\u{f023}"), // "" - (".vim", "\u{e62b}"), // "" - (".vimrc", "\u{e62b}"), // "" - (".viminfo", "\u{e62b}"), // "" - (".vscode", "\u{e70c}"), // "" - (".xauthority", "\u{e615}"), // "" - (".xinitrc", "\u{e615}"), // "" - (".xresources", "\u{e615}"), // "" - (".zshrc", "\u{f489}"), // "" - (".zsh_history", "\u{e615}"), // "" - ("a.out", "\u{f489}"), // "" - ("authorized_keys", "\u{e60a}"), // "" - ("bin", "\u{e5fc}"), // "" - ("bspwmrc", "\u{e615}"), // "" - ("cargo.toml", "\u{e7a8}"), // "" - ("cargo.lock", "\u{e7a8}"), // "" - ("changelog", "\u{e609}"), // "" - ("composer.json", "\u{e608}"), // "" - ("config", "\u{e5fc}"), // "" - ("config.ac", "\u{e615}"), // "" - ("config.mk", "\u{e615}"), // "" - ("config.el", "\u{e779}"), // "" - ("custom.el", "\u{e779}"), // "" - ("contributing", "\u{e60a}"), // "" - ("cron.d", "\u{e5fc}"), // "" - ("cron.daily", "\u{e5fc}"), // "" - ("cron.hourly", "\u{e5fc}"), // "" - ("cron.weekly", "\u{e5fc}"), // "" - ("cron.monthly", "\u{e5fc}"), // "" - ("crontab", "\u{e615}"), // "" - ("crypttab", "\u{e615}"), // "" - ("desktop", "\u{f108}"), // "" - ("downloads", "\u{f498}"), // "" - ("docker-compose.yml", "\u{f308}"), // "" - ("dockerfile", "\u{f308}"), // "" - ("documents", "\u{f02d}"), // "" - (".ds_store", "\u{f179}"), // "" - ("etc", "\u{e5fc}"), // "" - ("favicon.ico", "\u{f005}"), // "" - ("fstab", "\u{f1c0}"), // "" - ("gitignore_global", "\u{f1d3}"), // "" - ("gradle", "\u{e70e}"), // "" - ("group", "\u{e615}"), // "" - ("gruntfile.coffee", "\u{e611}"), // "" - ("gruntfile.js", "\u{e611}"), // "" - ("gruntfile.ls", "\u{e611}"), // "" - ("gshadow", "\u{e615}"), // "" - ("gulpfile.coffee", "\u{e610}"), // "" - ("gulpfile.js", "\u{e610}"), // "" - ("gulpfile.ls", "\u{e610}"), // "" - ("hidden", "\u{f023}"), // "" - ("hosts", "\u{f502}"), // "" - ("htoprc", "\u{e615}"), // "" - ("include", "\u{e5fc}"), // "" - ("init.el", "\u{e779}"), // "" - ("known_hosts", "\u{e60a}"), // "" - ("lib", "\u{f121}"), // "" - ("license", "\u{e60a}"), // "" - ("license.md", "\u{e60a}"), // "" - ("license.txt", "\u{e60a}"), // "" - ("localized", "\u{f179}"), // "" - ("mail", "\u{f6ef}"), // "" - ("makefile", "\u{e615}"), // "" - ("makefile.ac", "\u{e615}"), // "" - ("music", "\u{f025}"), // "" - ("muttrc", "\u{e615}"), // "" - ("node_modules", "\u{e5fa}"), // "" - ("npmignore", "\u{e71e}"), // "" - ("package.json", "\u{e718}"), // "" - ("packages.el", "\u{e779}"), // "" - ("package-lock.json", "\u{e718}"), // "" - ("passwd", "\u{f023}"), // "" - ("pictures", "\u{f03e}"), // "" - ("profile", "\u{e615}"), // "" - ("readme", "\u{e609}"), // "" - ("rc.lua", "\u{e615}"), // "" - ("rubydoc", "\u{e73b}"), // "" - ("robots.txt", "\u{fba7}"), // "ﮧ" - ("root", "\u{f023}"), // "" - ("shadow", "\u{e615}"), // "" - ("shells", "\u{e615}"), // "" - ("sudoers", "\u{f023}"), // "" - ("sxhkdrc", "\u{e615}"), // "" - ("tigrc", "\u{e615}"), // "" - ("vagrantfile", "\u{e615}"), // "" - ("videos", "\u{f03d}"), // "" - ("hostname", "\u{e615}"), // "" - ("webpack.config.js", "\u{fc29}"), // "ﰩ" - ("xmonad.hs", "\u{e615}"), // "" - ("xorg.conf.d", "\u{e5fc}"), // "" - ("xbps.d", "\u{e5fc}"), // "" - ]) - } + }; - fn get_default_icons_by_extension() -> HashMap<&'static str, &'static str> { - // Note: extensions must be lower-case - HashMap::from([ - ("1", "\u{f02d}"), // "" - ("7z", "\u{f410}"), // "" - ("a", "\u{e624}"), // "" - ("ai", "\u{e7b4}"), // "" - ("ape", "\u{f001}"), // "" - ("apk", "\u{e70e}"), // "" - ("asc", "\u{f023}"), // "" - ("asm", "\u{e614}"), // "" - ("asp", "\u{f121}"), // "" - ("avi", "\u{f008}"), // "" - ("avro", "\u{e60b}"), // "" - ("awk", "\u{f489}"), // "" - ("bash", "\u{f489}"), // "" - ("bash_history", "\u{f489}"), // "" - ("bash_profile", "\u{f489}"), // "" - ("bashrc", "\u{f489}"), // "" - ("bat", "\u{f17a}"), // "" - ("bin", "\u{f489}"), // "" - ("bio", "\u{f910}"), // "蘿" - ("bmp", "\u{f1c5}"), // "" - ("bz2", "\u{f410}"), // "" - ("c", "\u{e61e}"), // "" - ("c++", "\u{e61d}"), // "" - ("cc", "\u{e61d}"), // "" - ("cfg", "\u{e615}"), // "" - ("cl", "\u{f671}"), // "" - ("class", "\u{e738}"), // "" - ("clj", "\u{e768}"), // "" - ("cljs", "\u{e76a}"), // "" - ("cls", "\u{e600}"), // "" - ("coffee", "\u{f0f4}"), // "" - ("conf", "\u{e615}"), // "" - ("cp", "\u{e61d}"), // "" - ("cpp", "\u{e61d}"), // "" - ("cs", "\u{f81a}"), // "" - ("cshtml", "\u{f1fa}"), // "" - ("csproj", "\u{f81a}"), // "" - ("csx", "\u{f81a}"), // "" - ("csh", "\u{f489}"), // "" - ("css", "\u{e749}"), // "" - ("csv", "\u{f1c3}"), // "" - ("cue", "\u{f001}"), // "" - ("cxx", "\u{e61d}"), // "" - ("dart", "\u{e798}"), // "" - ("db", "\u{f1c0}"), // "" - ("deb", "\u{f187}"), // "" - ("desktop", "\u{f108}"), // "" - ("diff", "\u{e728}"), // "" - ("dll", "\u{f17a}"), // "" - ("doc", "\u{f1c2}"), // "" - ("dockerfile", "\u{f308}"), // "" - ("docx", "\u{f1c2}"), // "" - ("ds_store", "\u{f179}"), // "" - ("dump", "\u{f1c0}"), // "" - ("ebook", "\u{e28b}"), // "" - ("editorconfig", "\u{e615}"), // "" - ("ejs", "\u{e618}"), // "" - ("el", "\u{f671}"), // "" - ("elc", "\u{f671}"), // "" - ("elf", "\u{f489}"), // "" - ("elm", "\u{e62c}"), // "" - ("env", "\u{f462}"), // "" - ("eot", "\u{f031}"), // "" - ("epub", "\u{e28a}"), // "" - ("erb", "\u{e73b}"), // "" - ("erl", "\u{e7b1}"), // "" - ("exe", "\u{f17a}"), // "" - ("ex", "\u{e62d}"), // "" - ("exs", "\u{e62d}"), // "" - ("fish", "\u{f489}"), // "" - ("flac", "\u{f001}"), // "" - ("flv", "\u{f008}"), // "" - ("font", "\u{f031}"), // "" - ("fpl", "\u{f910}"), // "蘿" - ("fs", "\u{e7a7}"), // "" - ("fsx", "\u{e7a7}"), // "" - ("fsi", "\u{e7a7}"), // "" - ("gdoc", "\u{f1c2}"), // "" - ("gemfile", "\u{e21e}"), // "" - ("gemspec", "\u{e21e}"), // "" - ("gform", "\u{f298}"), // "" - ("gif", "\u{f1c5}"), // "" - ("git", "\u{f1d3}"), // "" - ("go", "\u{e627}"), // "" - ("gradle", "\u{e70e}"), // "" - ("gsheet", "\u{f1c3}"), // "" - ("gslides", "\u{f1c4}"), // "" - ("guardfile", "\u{e21e}"), // "" - ("gz", "\u{f410}"), // "" - ("h", "\u{f0fd}"), // "" - ("hbs", "\u{e60f}"), // "" - ("heic", "\u{f1c5}"), // "" - ("heif", "\u{f1c5}"), // "" - ("heix", "\u{f1c5}"), // "" - ("hpp", "\u{f0fd}"), // "" - ("hs", "\u{e777}"), // "" - ("htm", "\u{f13b}"), // "" - ("html", "\u{f13b}"), // "" - ("hxx", "\u{f0fd}"), // "" - ("ico", "\u{f1c5}"), // "" - ("image", "\u{f1c5}"), // "" - ("img", "\u{f1c0}"), // "" - ("iml", "\u{e7b5}"), // "" - ("ini", "\u{e615}"), // "" - ("ipynb", "\u{e606}"), // "" - ("iso", "\u{f1c0}"), // "" - ("jar", "\u{e738}"), // "" - ("java", "\u{e738}"), // "" - ("jpeg", "\u{f1c5}"), // "" - ("jpg", "\u{f1c5}"), // "" - ("js", "\u{e74e}"), // "" - ("json", "\u{e60b}"), // "" - ("jsx", "\u{e7ba}"), // "" - ("jl", "\u{e624}"), // "" - ("key", "\u{e60a}"), // "" - ("ksh", "\u{f489}"), // "" - ("ld", "\u{e624}"), // "" - ("ldb", "\u{f1c0}"), // "" - ("less", "\u{e758}"), // "" - ("lhs", "\u{e777}"), // "" - ("license", "\u{e60a}"), // "" - ("lisp", "\u{f671}"), // "" - ("localized", "\u{f179}"), // "" - ("lock", "\u{f023}"), // "" - ("log", "\u{f18d}"), // "" - ("lua", "\u{e620}"), // "" - ("lz", "\u{f410}"), // "" - ("m3u", "\u{f910}"), // "蘿" - ("m3u8", "\u{f910}"), // "蘿" - ("m4a", "\u{f001}"), // "" - ("m4v", "\u{f008}"), // "" - ("magnet", "\u{f076}"), // "" - ("markdown", "\u{e609}"), // "" - ("md", "\u{e609}"), // "" - ("mjs", "\u{e74e}"), // "" - ("mkd", "\u{e609}"), // "" - ("mkv", "\u{f008}"), // "" - ("mobi", "\u{e28b}"), // "" - ("mov", "\u{f008}"), // "" - ("mp3", "\u{f001}"), // "" - ("mp4", "\u{f008}"), // "" - ("msi", "\u{f17a}"), // "" - ("mustache", "\u{e60f}"), // "" - ("nix", "\u{f313}"), // "" - ("npmignore", "\u{e71e}"), // "" - ("o", "\u{e624}"), // "" - ("opus", "\u{f001}"), // "" - ("ogg", "\u{f001}"), // "" - ("ogv", "\u{f008}"), // "" - ("otf", "\u{f031}"), // "" - ("pdf", "\u{f1c1}"), // "" - ("pem", "\u{f805}"), // "" - ("phar", "\u{e608}"), // "" - ("php", "\u{e608}"), // "" - ("pkg", "\u{f187}"), // "" - ("pl", "\u{e769}"), // "" - ("plist", "\u{f302}"), // "" - ("pls", "\u{f910}"), // "蘿" - ("pm", "\u{e769}"), // "" - ("png", "\u{f1c5}"), // "" - ("ppt", "\u{f1c4}"), // "" - ("pptx", "\u{f1c4}"), // "" - ("procfile", "\u{e21e}"), // "" - ("properties", "\u{e60b}"), // "" - ("ps1", "\u{f489}"), // "" - ("psd", "\u{e7b8}"), // "" - ("pub", "\u{e60a}"), // "" - ("pxm", "\u{f1c5}"), // "" - ("py", "\u{e606}"), // "" - ("pyc", "\u{e606}"), // "" - ("r", "\u{fcd2}"), // "ﳒ" - ("rakefile", "\u{e21e}"), // "" - ("rar", "\u{f410}"), // "" - ("razor", "\u{f1fa}"), // "" - ("rb", "\u{e21e}"), // "" - ("rdata", "\u{fcd2}"), // "ﳒ" - ("rdb", "\u{e76d}"), // "" - ("rdoc", "\u{e609}"), // "" - ("rds", "\u{fcd2}"), // "ﳒ" - ("readme", "\u{e609}"), // "" - ("rlib", "\u{e7a8}"), // "" - ("rmd", "\u{e609}"), // "" - ("rpm", "\u{f187}"), // "" - ("rproj", "\u{fac5}"), // "鉶" - ("rs", "\u{e7a8}"), // "" - ("rspec", "\u{e21e}"), // "" - ("rspec_parallel", "\u{e21e}"), // "" - ("rspec_status", "\u{e21e}"), // "" - ("rss", "\u{f09e}"), // "" - ("rtf", "\u{f15c}"), // "" - ("ru", "\u{e21e}"), // "" - ("rubydoc", "\u{e73b}"), // "" - ("s", "\u{e614}"), // "" - ("sass", "\u{e603}"), // "" - ("scala", "\u{e737}"), // "" - ("scpt", "\u{f302}"), // "" - ("scss", "\u{e603}"), // "" - ("sh", "\u{f489}"), // "" - ("shell", "\u{f489}"), // "" - ("sig", "\u{e60a}"), // "" - ("slim", "\u{e73b}"), // "" - ("sln", "\u{e70c}"), // "" - ("so", "\u{e624}"), // "" - ("sql", "\u{f1c0}"), // "" - ("sqlite3", "\u{e7c4}"), // "" - ("srt", "\u{f02d}"), // "" - ("styl", "\u{e600}"), // "" - ("stylus", "\u{e600}"), // "" - ("sub", "\u{f02d}"), // "" - ("sublime-package", "\u{e7aa}"), // "" - ("sublime-session", "\u{e7aa}"), // "" - ("svg", "\u{f1c5}"), // "" - ("swift", "\u{e755}"), // "" - ("swp", "\u{e62b}"), // "" - ("sym", "\u{e624}"), // "" - ("t", "\u{e769}"), // "" - ("tar", "\u{f410}"), // "" - ("tex", "\u{e600}"), // "" - ("tgz", "\u{f410}"), // "" - ("tiff", "\u{f1c5}"), // "" - ("toml", "\u{e60b}"), // "" - ("torrent", "\u{f98c}"), // "歷" - ("ts", "\u{e628}"), // "" - ("tsx", "\u{e7ba}"), // "" - ("ttc", "\u{f031}"), // "" - ("ttf", "\u{f031}"), // "" - ("twig", "\u{e61c}"), // "" - ("txt", "\u{f15c}"), // "" - ("video", "\u{f008}"), // "" - ("vim", "\u{e62b}"), // "" - ("vlc", "\u{f910}"), // "蘿" - ("vue", "\u{fd42}"), // "﵂" - ("wav", "\u{f001}"), // "" - ("webm", "\u{f008}"), // "" - ("webp", "\u{f1c5}"), // "" - ("windows", "\u{f17a}"), // "" - ("wma", "\u{f001}"), // "" - ("wmv", "\u{f008}"), // "" - ("wpl", "\u{f910}"), // "蘿" - ("woff", "\u{f031}"), // "" - ("woff2", "\u{f031}"), // "" - ("xbps", "\u{f187}"), // "" - ("xcf", "\u{f1c5}"), // "" - ("xls", "\u{f1c3}"), // "" - ("xlsx", "\u{f1c3}"), // "" - ("xml", "\u{f121}"), // "" - ("xul", "\u{f269}"), // "" - ("xz", "\u{f410}"), // "" - ("yaml", "\u{e60b}"), // "" - ("yml", "\u{e60b}"), // "" - ("zip", "\u{f410}"), // "" - ("zsh", "\u{f489}"), // "" - ("zsh-theme", "\u{f489}"), // "" - ("zshrc", "\u{f489}"), // "" - ("zst", "\u{f410}"), // "" - ]) + format!("{}{}", icon, self.icon_separator) + } + } } } #[cfg(test)] mod test { - use super::{Icons, Theme}; + use super::{IconTheme, Icons}; + use crate::flags::{IconOption, IconTheme as FlagTheme}; use crate::meta::Meta; use std::fs::File; use tempfile::tempdir; #[test] - fn get_no_icon() { + fn get_no_icon_never_tty() { + let tmp_dir = tempdir().expect("failed to create temp dir"); + let file_path = tmp_dir.path().join("file.txt"); + File::create(&file_path).expect("failed to create file"); + let meta = Meta::from_path(&file_path, false).unwrap(); + + let icons = Icons::new(true, IconOption::Never, FlagTheme::Fancy, " ".to_string()); + let icon = icons.get(&meta.name); + + assert_eq!(icon, ""); + } + #[test] + fn get_no_icon_never_not_tty() { let tmp_dir = tempdir().expect("failed to create temp dir"); let file_path = tmp_dir.path().join("file.txt"); File::create(&file_path).expect("failed to create file"); let meta = Meta::from_path(&file_path, false).unwrap(); - let icon = Icons::new(Theme::NoIcon, " ".to_string()); - let icon = icon.get(&meta.name); + let icons = Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()); + let icon = icons.get(&meta.name); assert_eq!(icon, ""); } #[test] - fn get_default_file_icon() { + fn get_no_icon_auto() { + let tmp_dir = tempdir().expect("failed to create temp dir"); + let file_path = tmp_dir.path().join("file.txt"); + File::create(&file_path).expect("failed to create file"); + let meta = Meta::from_path(&file_path, false).unwrap(); + + let icons = Icons::new(false, IconOption::Auto, FlagTheme::Fancy, " ".to_string()); + let icon = icons.get(&meta.name); + + assert_eq!(icon, ""); + } + #[test] + fn get_icon_auto_tty() { + let tmp_dir = tempdir().expect("failed to create temp dir"); + let file_path = tmp_dir.path().join("file.txt"); + File::create(&file_path).expect("failed to create file"); + let meta = Meta::from_path(&file_path, false).unwrap(); + + let icons = Icons::new(true, IconOption::Auto, FlagTheme::Fancy, " ".to_string()); + let icon = icons.get(&meta.name); + + assert_eq!(icon, "\u{f15c} "); + } + + #[test] + fn get_icon_always_tty_default_file() { let tmp_dir = tempdir().expect("failed to create temp dir"); let file_path = tmp_dir.path().join("file"); File::create(&file_path).expect("failed to create file"); let meta = Meta::from_path(&file_path, false).unwrap(); - let icon = Icons::new(Theme::Fancy, " ".to_string()); + let icon = Icons::new(true, IconOption::Always, FlagTheme::Fancy, " ".to_string()); let icon_str = icon.get(&meta.name); assert_eq!(icon_str, "\u{f016} "); //  } #[test] - fn get_default_file_icon_unicode() { + fn get_icon_always_not_tty_default_file() { let tmp_dir = tempdir().expect("failed to create temp dir"); let file_path = tmp_dir.path().join("file"); File::create(&file_path).expect("failed to create file"); let meta = Meta::from_path(&file_path, false).unwrap(); - let icon = Icons::new(Theme::Unicode, " ".to_string()); + let icon = Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()); let icon_str = icon.get(&meta.name); - assert_eq!(icon_str, "\u{1f5cb} "); + assert_eq!(icon_str, "\u{f016} "); //  } #[test] - fn get_directory_icon() { + fn get_icon_default_file_icon_unicode() { let tmp_dir = tempdir().expect("failed to create temp dir"); - let file_path = tmp_dir.path(); - let meta = Meta::from_path(file_path, false).unwrap(); + let file_path = tmp_dir.path().join("file"); + File::create(&file_path).expect("failed to create file"); + let meta = Meta::from_path(&file_path, false).unwrap(); - let icon = Icons::new(Theme::Fancy, " ".to_string()); + let icon = Icons::new( + false, + IconOption::Always, + FlagTheme::Unicode, + " ".to_string(), + ); let icon_str = icon.get(&meta.name); - assert_eq!(icon_str, "\u{f115} "); //  + assert_eq!(icon_str, format!("{}{}", "\u{1f4c4}", icon.icon_separator)); } #[test] - fn get_directory_icon_unicode() { + fn get_icon_default_directory() { let tmp_dir = tempdir().expect("failed to create temp dir"); let file_path = tmp_dir.path(); let meta = Meta::from_path(file_path, false).unwrap(); - let icon = Icons::new(Theme::Unicode, " ".to_string()); + let icon = Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()); let icon_str = icon.get(&meta.name); - assert_eq!(icon_str, "\u{1f5c1} "); + assert_eq!(icon_str, "\u{f115} "); //  } #[test] - fn get_directory_icon_with_ext() { + fn get_icon_default_directory_unicode() { let tmp_dir = tempdir().expect("failed to create temp dir"); let file_path = tmp_dir.path(); let meta = Meta::from_path(file_path, false).unwrap(); - let icon = Icons::new(Theme::Fancy, " ".to_string()); + let icon = Icons::new( + false, + IconOption::Always, + FlagTheme::Unicode, + " ".to_string(), + ); let icon_str = icon.get(&meta.name); - assert_eq!(icon_str, "\u{f115} "); //  + assert_eq!(icon_str, format!("{}{}", "\u{1f4c2}", icon.icon_separator)); } #[test] fn get_icon_by_name() { let tmp_dir = tempdir().expect("failed to create temp dir"); - for (file_name, file_icon) in &Icons::get_default_icons_by_name() { + for (file_name, file_icon) in &IconTheme::get_default_icons_by_name() { let file_path = tmp_dir.path().join(file_name); File::create(&file_path).expect("failed to create file"); let meta = Meta::from_path(&file_path, false).unwrap(); - let icon = Icons::new(Theme::Fancy, " ".to_string()); + let icon = Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()); let icon_str = icon.get(&meta.name); assert_eq!(icon_str, format!("{}{}", file_icon, icon.icon_separator)); @@ -584,12 +223,12 @@ mod test { fn get_icon_by_extension() { let tmp_dir = tempdir().expect("failed to create temp dir"); - for (ext, file_icon) in &Icons::get_default_icons_by_extension() { + for (ext, file_icon) in &IconTheme::get_default_icons_by_extension() { let file_path = tmp_dir.path().join(format!("file.{}", ext)); File::create(&file_path).expect("failed to create file"); let meta = Meta::from_path(&file_path, false).unwrap(); - let icon = Icons::new(Theme::Fancy, " ".to_string()); + let icon = Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()); let icon_str = icon.get(&meta.name); assert_eq!(icon_str, format!("{}{}", file_icon, icon.icon_separator)); diff --git a/src/main.rs b/src/main.rs index 44829e301..78bdac926 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,6 +36,7 @@ mod flags; mod icon; mod meta; mod sort; +mod theme; use crate::config_file::Config; use crate::core::Core; diff --git a/src/meta/name.rs b/src/meta/name.rs index a9e092ca9..584046acd 100644 --- a/src/meta/name.rs +++ b/src/meta/name.rs @@ -216,8 +216,8 @@ mod test { use super::DisplayOption; use super::Name; use crate::color::{self, Colors}; - use crate::flags::HyperlinkOption; - use crate::icon::{self, Icons}; + use crate::flags::{HyperlinkOption, IconOption, IconTheme as FlagTheme}; + use crate::icon::Icons; use crate::meta::FileType; use crate::meta::Meta; #[cfg(unix)] @@ -237,7 +237,7 @@ mod test { #[cfg(unix)] // Windows uses different default permissions fn test_print_file_name() { let tmp_dir = tempdir().expect("failed to create temp dir"); - let icons = Icons::new(icon::Theme::Fancy, " ".to_string()); + let icons = Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()); // Create the file; let file_path = tmp_dir.path().join("file.txt"); @@ -262,7 +262,7 @@ mod test { #[test] fn test_print_dir_name() { let tmp_dir = tempdir().expect("failed to create temp dir"); - let icons = Icons::new(icon::Theme::Fancy, " ".to_string()); + let icons = &Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()); // Create the directory let dir_path = tmp_dir.path().join("directory"); @@ -275,7 +275,7 @@ mod test { " directory".to_string().with(Color::AnsiValue(33)), meta.name.render( &colors, - &icons, + icons, &DisplayOption::FileName, HyperlinkOption::Never ) @@ -286,7 +286,7 @@ mod test { #[cfg(unix)] // Symlinks are hard on Windows fn test_print_symlink_name_file() { let tmp_dir = tempdir().expect("failed to create temp dir"); - let icons = Icons::new(icon::Theme::Fancy, " ".to_string()); + let icons = &Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()); // Create the file; let file_path = tmp_dir.path().join("file.tmp"); @@ -308,7 +308,7 @@ mod test { " target.tmp".to_string().with(Color::AnsiValue(44)), name.render( &colors, - &icons, + icons, &DisplayOption::FileName, HyperlinkOption::Never ) @@ -319,7 +319,7 @@ mod test { #[cfg(unix)] // Symlinks are hard on Windows fn test_print_symlink_name_dir() { let tmp_dir = tempdir().expect("failed to create temp dir"); - let icons = Icons::new(icon::Theme::Fancy, " ".to_string()); + let icons = Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()); // Create the directory; let dir_path = tmp_dir.path().join("tmp.d"); @@ -352,7 +352,7 @@ mod test { #[cfg(unix)] fn test_print_other_type_name() { let tmp_dir = tempdir().expect("failed to create temp dir"); - let icons = Icons::new(icon::Theme::Fancy, " ".to_string()); + let icons = &Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()); // Create the pipe; let pipe_path = tmp_dir.path().join("pipe.tmp"); @@ -372,7 +372,7 @@ mod test { " pipe.tmp".to_string().with(Color::AnsiValue(184)), name.render( &colors, - &icons, + icons, &DisplayOption::FileName, HyperlinkOption::Never ) @@ -382,7 +382,7 @@ mod test { #[test] fn test_print_without_icon_or_color() { let tmp_dir = tempdir().expect("failed to create temp dir"); - let icons = Icons::new(icon::Theme::NoIcon, " ".to_string()); + let icons = Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()); // Create the file; let file_path = tmp_dir.path().join("file.txt"); @@ -407,7 +407,7 @@ mod test { #[test] fn test_print_hyperlink() { let tmp_dir = tempdir().expect("failed to create temp dir"); - let icons = Icons::new(icon::Theme::NoIcon, " ".to_string()); + let icons = Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()); // Create the file; let file_path = tmp_dir.path().join("file.txt"); @@ -633,7 +633,7 @@ mod test { #[cfg(unix)] fn test_special_chars_in_filename() { let tmp_dir = tempdir().expect("failed to create temp dir"); - let icons = Icons::new(icon::Theme::Fancy, " ".to_string()); + let icons = Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()); // Create the file; let file_path = tmp_dir.path().join("file\ttab.txt"); diff --git a/src/theme.rs b/src/theme.rs new file mode 100644 index 000000000..4ff78bf00 --- /dev/null +++ b/src/theme.rs @@ -0,0 +1,87 @@ +pub mod color; +pub mod icon; + +use std::path::Path; +use std::{fs, io}; + +use serde::{de::DeserializeOwned, Deserialize}; +use thiserror::Error; + +use crate::config_file; +use crate::print_error; + +use color::ColorTheme; +use icon::IconTheme; + +#[derive(Debug, Deserialize, Default, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +#[serde(deny_unknown_fields)] +#[serde(default)] +pub struct Theme { + pub color: ColorTheme, + pub icon: IconTheme, +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("Theme file not existed")] + NotExisted(#[from] io::Error), + #[error("Theme file format invalid")] + InvalidFormat(#[from] serde_yaml::Error), + #[error("Theme file path invalid {0}")] + InvalidPath(String), + #[error("Unknown Theme error")] + Unknown(), +} + +impl Theme { + /// This read theme from file, + /// use the file path if it is absolute + /// prefix the config_file dir to it if it is not + pub fn from_path(file: &str) -> Result + where + D: DeserializeOwned + Default, + { + let real = if let Some(path) = config_file::Config::expand_home(file) { + path + } else { + print_error!("Not a valid theme file path: {}.", &file); + return Err(Error::InvalidPath(file.to_string())); + }; + let path = if Path::new(&real).is_absolute() { + real + } else { + match config_file::Config::config_file_path() { + Some(p) => p.join(real), + None => return Err(Error::InvalidPath("config home not existed".into())), + } + }; + + // try `yml` if `yaml` extension file not found or error + let mut err: Error = Error::Unknown(); + for ext in ["yaml", "yml"] { + match fs::read(&path.with_extension(ext)) { + Ok(f) => match Self::with_yaml(&String::from_utf8_lossy(&f)) { + Ok(t) => return Ok(t), + Err(e) => { + err = Error::from(e); + } + }, + Err(e) => err = Error::from(e), + } + } + + Err(err) + } + + /// This constructs a Theme struct with a passed [Yaml] str. + fn with_yaml(yaml: &str) -> Result + where + D: DeserializeOwned + Default, + { + if yaml.trim() == "" { + return Ok(D::default()); + } + serde_yaml::from_str::(yaml) + } +} diff --git a/src/color/theme.rs b/src/theme/color.rs similarity index 82% rename from src/color/theme.rs rename to src/theme/color.rs index 6a49261da..2176791de 100644 --- a/src/color/theme.rs +++ b/src/theme/color.rs @@ -1,12 +1,8 @@ ///! This module provides methods to create theme from files and operations related to ///! this. -use crate::config_file; -use crate::print_error; - use crossterm::style::Color; use serde::Deserialize; -use std::path::Path; -use std::{fmt, fs}; +use std::fmt; // Custom color deserialize fn deserialize_color<'de, D>(deserializer: D) -> Result @@ -88,7 +84,7 @@ where #[serde(rename_all = "kebab-case")] #[serde(deny_unknown_fields)] #[serde(default)] -pub struct Theme { +pub struct ColorTheme { #[serde(deserialize_with = "deserialize_color")] pub user: Color, #[serde(deserialize_with = "deserialize_color")] @@ -328,65 +324,16 @@ impl Default for Links { } } -impl Default for Theme { +impl Default for ColorTheme { fn default() -> Self { // TODO(zwpaper): check terminal color and return light or dark Self::default_dark() } } -impl Theme { - /// This read theme from file, - /// use the file path if it is absolute - /// prefix the config_file dir to it if it is not - pub fn from_path(file: &str) -> Option { - let real = if let Some(path) = config_file::Config::expand_home(file) { - path - } else { - print_error!("Not a valid theme file path: {}.", &file); - return None; - }; - let path = if Path::new(&real).is_absolute() { - real - } else { - config_file::Config::config_file_path()? - .join("themes") - .join(real) - }; - match fs::read(&path.with_extension("yaml")) { - Ok(f) => match Self::with_yaml(&String::from_utf8_lossy(&f)) { - Ok(t) => Some(t), - Err(e) => { - print_error!("Theme file {} format error: {}.", &file, e); - None - } - }, - Err(_) => { - // try `yml` if `yaml` extension file not found - match fs::read(&path.with_extension("yml")) { - Ok(f) => match Self::with_yaml(&String::from_utf8_lossy(&f)) { - Ok(t) => Some(t), - Err(e) => { - print_error!("Theme file {} format error: {}.", &file, e); - None - } - }, - Err(e) => { - print_error!("Not a valid theme: {}, {}.", path.to_string_lossy(), e); - None - } - } - } - } - } - - /// This constructs a Theme struct with a passed [Yaml] str. - fn with_yaml(yaml: &str) -> Result { - serde_yaml::from_str::(yaml) - } - +impl ColorTheme { pub fn default_dark() -> Self { - Theme { + ColorTheme { user: Color::AnsiValue(230), // Cornsilk1 group: Color::AnsiValue(187), // LightYellow3 permission: Permission::default(), @@ -398,9 +345,14 @@ impl Theme { tree_edge: Color::AnsiValue(245), // Grey } } +} + +#[cfg(test)] +mod tests { + use super::ColorTheme; + use crate::theme::Theme; - #[cfg(test)] - pub fn default_yaml() -> &'static str { + fn default_yaml() -> &'static str { r#"--- user: 230 group: 187 @@ -428,17 +380,12 @@ links: tree-edge: 245 "# } -} - -#[cfg(test)] -mod tests { - use super::Theme; #[test] fn test_default_theme() { assert_eq!( - Theme::default_dark(), - Theme::with_yaml(Theme::default_yaml()).unwrap() + ColorTheme::default_dark(), + Theme::with_yaml(default_yaml()).unwrap() ); } @@ -449,10 +396,10 @@ mod tests { let dir = assert_fs::TempDir::new().unwrap(); let theme = dir.path().join("theme.yaml"); let mut file = File::create(&theme).unwrap(); - writeln!(file, "{}", Theme::default_yaml()).unwrap(); + writeln!(file, "{}", default_yaml()).unwrap(); assert_eq!( - Theme::default_dark(), + ColorTheme::default_dark(), Theme::from_path(theme.to_str().unwrap()).unwrap() ); } @@ -461,8 +408,8 @@ mod tests { fn test_empty_theme_return_default() { // Must contain one field at least // ref https://github.com/dtolnay/serde-yaml/issues/86 - let empty_theme = Theme::with_yaml("user: 230").unwrap(); // 230 is the default value - let default_theme = Theme::default_dark(); + let empty_theme: ColorTheme = Theme::with_yaml("user: 230").unwrap(); // 230 is the default value + let default_theme = ColorTheme::default_dark(); assert_eq!(empty_theme, default_theme); } @@ -470,8 +417,8 @@ mod tests { fn test_first_level_theme_return_default_but_changed() { // Must contain one field at least // ref https://github.com/dtolnay/serde-yaml/issues/86 - let empty_theme = Theme::with_yaml("user: 130").unwrap(); - let mut theme = Theme::default_dark(); + let empty_theme: ColorTheme = Theme::with_yaml("user: 130").unwrap(); + let mut theme = ColorTheme::default_dark(); use crossterm::style::Color; theme.user = Color::AnsiValue(130); assert_eq!(empty_theme, theme); @@ -481,13 +428,13 @@ mod tests { fn test_second_level_theme_return_default_but_changed() { // Must contain one field at least // ref https://github.com/dtolnay/serde-yaml/issues/86 - let empty_theme = Theme::with_yaml( + let empty_theme: ColorTheme = Theme::with_yaml( r#"--- permission: read: 130"#, ) .unwrap(); - let mut theme = Theme::default_dark(); + let mut theme = ColorTheme::default_dark(); use crossterm::style::Color; theme.permission.read = Color::AnsiValue(130); assert_eq!(empty_theme, theme); diff --git a/src/theme/icon.rs b/src/theme/icon.rs new file mode 100644 index 000000000..488c0ea2b --- /dev/null +++ b/src/theme/icon.rs @@ -0,0 +1,557 @@ +use serde::Deserialize; +use std::collections::HashMap; + +#[derive(Debug, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +#[serde(deny_unknown_fields)] +#[serde(default)] +pub struct IconTheme { + pub name: HashMap, + pub extension: HashMap, + pub filetype: ByType, +} + +#[derive(Debug, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +#[serde(deny_unknown_fields)] +#[serde(default)] +pub struct ByType { + pub dir: String, + pub file: String, + pub pipe: String, + pub socket: String, + pub executable: String, + pub device_char: String, + pub device_block: String, + pub special: String, + pub symlink_dir: String, + pub symlink_file: String, +} + +impl Default for IconTheme { + fn default() -> Self { + IconTheme { + name: Self::get_default_icons_by_name(), + extension: Self::get_default_icons_by_extension(), + filetype: ByType::default(), + } + } +} + +impl Default for ByType { + fn default() -> ByType { + ByType { + dir: "\u{f115}".into(), //  + file: "\u{f016}".into(), //  + pipe: "\u{f731}".into(), //  + socket: "\u{f6a7}".into(), //  + executable: "\u{f489}".into(), //  + symlink_dir: "\u{f482}".into(), //  + symlink_file: "\u{f481}".into(), //  + device_char: "\u{e601}".into(), //  + device_block: "\u{fc29}".into(), // ﰩ + special: "\u{f2dc}".into(), //  + } + } +} + +impl ByType { + pub fn unicode() -> Self { + ByType { + dir: "\u{1f4c2}".into(), + file: "\u{1f4c4}".into(), + pipe: "\u{1f4e9}".into(), + socket: "\u{1f4ec}".into(), + executable: "\u{1f3d7}".into(), + symlink_dir: "\u{1f5c2}".into(), + symlink_file: "\u{1f516}".into(), + device_char: "\u{1f5a8}".into(), + device_block: "\u{1f4bd}".into(), + special: "\u{1f4df}".into(), + } + } +} + +impl IconTheme { + pub fn unicode() -> Self { + IconTheme { + name: HashMap::new(), + extension: HashMap::new(), + filetype: ByType::unicode(), + } + } + + // pub only for testing in icons.rs + pub fn get_default_icons_by_name() -> HashMap { + // Note: filenames must be lower-case + [ + (".trash", "\u{f1f8}"), // "" + (".atom", "\u{e764}"), // "" + (".bash_profile", "\u{e615}"), // "" + (".bash_logout", "\u{e615}"), // "" + (".bashrc", "\u{f489}"), // "" + (".cargo", "\u{e7a8}"), // "" + (".clang-format", "\u{e615}"), // "" + (".config", "\u{e5fc}"), // "" + (".emacs.d", "\u{e779}"), // "" + (".doom.d", "\u{e779}"), // "" + (".git", "\u{e5fb}"), // "" + (".git-credentials", "\u{e60a}"), // "" + (".gitattributes", "\u{f1d3}"), // "" + (".gitconfig", "\u{f1d3}"), // "" + (".github", "\u{e5fd}"), // "" + (".gitignore", "\u{f1d3}"), // "" + (".gitlab-ci.yml", "\u{f296}"), // "" + (".gitmodules", "\u{f1d3}"), // "" + (".htaccess", "\u{e615}"), // "" + (".htpasswd", "\u{e615}"), // "" + (".inputrc", "\u{e615}"), // "" + (".node_repl_history", "\u{e718}"), // "" + (".npm", "\u{e5fa}"), // "" + (".profile", "\u{f68c}"), // "" + (".python_history", "\u{e606}"), // "" + (".release.toml", "\u{e7a8}"), // "" + (".rvm", "\u{e21e}"), // "" + (".ssh", "\u{f023}"), // "" + (".vim", "\u{e62b}"), // "" + (".vimrc", "\u{e62b}"), // "" + (".viminfo", "\u{e62b}"), // "" + (".vscode", "\u{e70c}"), // "" + (".xauthority", "\u{e615}"), // "" + (".xinitrc", "\u{e615}"), // "" + (".xresources", "\u{e615}"), // "" + (".zshrc", "\u{f489}"), // "" + (".zsh_history", "\u{e615}"), // "" + ("a.out", "\u{f489}"), // "" + ("authorized_keys", "\u{e60a}"), // "" + ("bin", "\u{e5fc}"), // "" + ("bspwmrc", "\u{e615}"), // "" + ("cargo.toml", "\u{e7a8}"), // "" + ("cargo.lock", "\u{e7a8}"), // "" + ("changelog", "\u{e609}"), // "" + ("composer.json", "\u{e608}"), // "" + ("config", "\u{e5fc}"), // "" + ("config.ac", "\u{e615}"), // "" + ("config.mk", "\u{e615}"), // "" + ("config.el", "\u{e779}"), // "" + ("custom.el", "\u{e779}"), // "" + ("contributing", "\u{e60a}"), // "" + ("cron.d", "\u{e5fc}"), // "" + ("cron.daily", "\u{e5fc}"), // "" + ("cron.hourly", "\u{e5fc}"), // "" + ("cron.weekly", "\u{e5fc}"), // "" + ("cron.monthly", "\u{e5fc}"), // "" + ("crontab", "\u{e615}"), // "" + ("crypttab", "\u{e615}"), // "" + ("desktop", "\u{f108}"), // "" + ("downloads", "\u{f498}"), // "" + ("docker-compose.yml", "\u{f308}"), // "" + ("dockerfile", "\u{f308}"), // "" + ("documents", "\u{f02d}"), // "" + (".ds_store", "\u{f179}"), // "" + ("etc", "\u{e5fc}"), // "" + ("favicon.ico", "\u{f005}"), // "" + ("fstab", "\u{f1c0}"), // "" + ("gitignore_global", "\u{f1d3}"), // "" + ("gradle", "\u{e70e}"), // "" + ("group", "\u{e615}"), // "" + ("gruntfile.coffee", "\u{e611}"), // "" + ("gruntfile.js", "\u{e611}"), // "" + ("gruntfile.ls", "\u{e611}"), // "" + ("gshadow", "\u{e615}"), // "" + ("gulpfile.coffee", "\u{e610}"), // "" + ("gulpfile.js", "\u{e610}"), // "" + ("gulpfile.ls", "\u{e610}"), // "" + ("hidden", "\u{f023}"), // "" + ("hosts", "\u{f502}"), // "" + ("htoprc", "\u{e615}"), // "" + ("include", "\u{e5fc}"), // "" + ("init.el", "\u{e779}"), // "" + ("known_hosts", "\u{e60a}"), // "" + ("lib", "\u{f121}"), // "" + ("license", "\u{e60a}"), // "" + ("license.md", "\u{e60a}"), // "" + ("license.txt", "\u{e60a}"), // "" + ("localized", "\u{f179}"), // "" + ("mail", "\u{f6ef}"), // "" + ("makefile", "\u{e615}"), // "" + ("makefile.ac", "\u{e615}"), // "" + ("music", "\u{f025}"), // "" + ("muttrc", "\u{e615}"), // "" + ("node_modules", "\u{e5fa}"), // "" + ("npmignore", "\u{e71e}"), // "" + ("package.json", "\u{e718}"), // "" + ("packages.el", "\u{e779}"), // "" + ("package-lock.json", "\u{e718}"), // "" + ("passwd", "\u{f023}"), // "" + ("pictures", "\u{f03e}"), // "" + ("profile", "\u{e615}"), // "" + ("readme", "\u{e609}"), // "" + ("rc.lua", "\u{e615}"), // "" + ("rubydoc", "\u{e73b}"), // "" + ("robots.txt", "\u{fba7}"), // "ﮧ" + ("root", "\u{f023}"), // "" + ("shadow", "\u{e615}"), // "" + ("shells", "\u{e615}"), // "" + ("sudoers", "\u{f023}"), // "" + ("sxhkdrc", "\u{e615}"), // "" + ("tigrc", "\u{e615}"), // "" + ("vagrantfile", "\u{e615}"), // "" + ("videos", "\u{f03d}"), // "" + ("hostname", "\u{e615}"), // "" + ("webpack.config.js", "\u{fc29}"), // "ﰩ" + ("xmonad.hs", "\u{e615}"), // "" + ("xorg.conf.d", "\u{e5fc}"), // "" + ("xbps.d", "\u{e5fc}"), // "" + ] + .iter() + .map(|&s| (s.0.to_owned(), s.1.to_owned())) + .collect::>() + } + + // pub only for testing in icons.rs + pub fn get_default_icons_by_extension() -> HashMap { + // Note: extensions must be lower-case + [ + ("1", "\u{f02d}"), // "" + ("7z", "\u{f410}"), // "" + ("a", "\u{e624}"), // "" + ("ai", "\u{e7b4}"), // "" + ("ape", "\u{f001}"), // "" + ("apk", "\u{e70e}"), // "" + ("asc", "\u{f023}"), // "" + ("asm", "\u{e614}"), // "" + ("asp", "\u{f121}"), // "" + ("avi", "\u{f008}"), // "" + ("avro", "\u{e60b}"), // "" + ("awk", "\u{f489}"), // "" + ("bash", "\u{f489}"), // "" + ("bash_history", "\u{f489}"), // "" + ("bash_profile", "\u{f489}"), // "" + ("bashrc", "\u{f489}"), // "" + ("bat", "\u{f17a}"), // "" + ("bin", "\u{f489}"), // "" + ("bio", "\u{f910}"), // "蘿" + ("bmp", "\u{f1c5}"), // "" + ("bz2", "\u{f410}"), // "" + ("c", "\u{e61e}"), // "" + ("c++", "\u{e61d}"), // "" + ("cc", "\u{e61d}"), // "" + ("cfg", "\u{e615}"), // "" + ("cl", "\u{f671}"), // "" + ("class", "\u{e738}"), // "" + ("clj", "\u{e768}"), // "" + ("cljs", "\u{e76a}"), // "" + ("cls", "\u{e600}"), // "" + ("coffee", "\u{f0f4}"), // "" + ("conf", "\u{e615}"), // "" + ("cp", "\u{e61d}"), // "" + ("cpp", "\u{e61d}"), // "" + ("cs", "\u{f81a}"), // "" + ("cshtml", "\u{f1fa}"), // "" + ("csproj", "\u{f81a}"), // "" + ("csx", "\u{f81a}"), // "" + ("csh", "\u{f489}"), // "" + ("css", "\u{e749}"), // "" + ("csv", "\u{f1c3}"), // "" + ("cue", "\u{f001}"), // "" + ("cxx", "\u{e61d}"), // "" + ("dart", "\u{e798}"), // "" + ("db", "\u{f1c0}"), // "" + ("deb", "\u{f187}"), // "" + ("desktop", "\u{f108}"), // "" + ("diff", "\u{e728}"), // "" + ("dll", "\u{f17a}"), // "" + ("doc", "\u{f1c2}"), // "" + ("dockerfile", "\u{f308}"), // "" + ("docx", "\u{f1c2}"), // "" + ("ds_store", "\u{f179}"), // "" + ("dump", "\u{f1c0}"), // "" + ("ebook", "\u{e28b}"), // "" + ("editorconfig", "\u{e615}"), // "" + ("ejs", "\u{e618}"), // "" + ("el", "\u{f671}"), // "" + ("elc", "\u{f671}"), // "" + ("elf", "\u{f489}"), // "" + ("elm", "\u{e62c}"), // "" + ("env", "\u{f462}"), // "" + ("eot", "\u{f031}"), // "" + ("epub", "\u{e28a}"), // "" + ("erb", "\u{e73b}"), // "" + ("erl", "\u{e7b1}"), // "" + ("exe", "\u{f17a}"), // "" + ("ex", "\u{e62d}"), // "" + ("exs", "\u{e62d}"), // "" + ("fish", "\u{f489}"), // "" + ("flac", "\u{f001}"), // "" + ("flv", "\u{f008}"), // "" + ("font", "\u{f031}"), // "" + ("fpl", "\u{f910}"), // "蘿" + ("fs", "\u{e7a7}"), // "" + ("fsx", "\u{e7a7}"), // "" + ("fsi", "\u{e7a7}"), // "" + ("gdoc", "\u{f1c2}"), // "" + ("gemfile", "\u{e21e}"), // "" + ("gemspec", "\u{e21e}"), // "" + ("gform", "\u{f298}"), // "" + ("gif", "\u{f1c5}"), // "" + ("git", "\u{f1d3}"), // "" + ("go", "\u{e627}"), // "" + ("gradle", "\u{e70e}"), // "" + ("gsheet", "\u{f1c3}"), // "" + ("gslides", "\u{f1c4}"), // "" + ("guardfile", "\u{e21e}"), // "" + ("gz", "\u{f410}"), // "" + ("h", "\u{f0fd}"), // "" + ("hbs", "\u{e60f}"), // "" + ("heic", "\u{f1c5}"), // "" + ("heif", "\u{f1c5}"), // "" + ("heix", "\u{f1c5}"), // "" + ("hpp", "\u{f0fd}"), // "" + ("hs", "\u{e777}"), // "" + ("htm", "\u{f13b}"), // "" + ("html", "\u{f13b}"), // "" + ("hxx", "\u{f0fd}"), // "" + ("ico", "\u{f1c5}"), // "" + ("image", "\u{f1c5}"), // "" + ("img", "\u{f1c0}"), // "" + ("iml", "\u{e7b5}"), // "" + ("ini", "\u{e615}"), // "" + ("ipynb", "\u{e606}"), // "" + ("iso", "\u{f1c0}"), // "" + ("jar", "\u{e738}"), // "" + ("java", "\u{e738}"), // "" + ("jpeg", "\u{f1c5}"), // "" + ("jpg", "\u{f1c5}"), // "" + ("js", "\u{e74e}"), // "" + ("json", "\u{e60b}"), // "" + ("jsx", "\u{e7ba}"), // "" + ("jl", "\u{e624}"), // "" + ("key", "\u{e60a}"), // "" + ("ksh", "\u{f489}"), // "" + ("ld", "\u{e624}"), // "" + ("ldb", "\u{f1c0}"), // "" + ("less", "\u{e758}"), // "" + ("lhs", "\u{e777}"), // "" + ("license", "\u{e60a}"), // "" + ("lisp", "\u{f671}"), // "" + ("localized", "\u{f179}"), // "" + ("lock", "\u{f023}"), // "" + ("log", "\u{f18d}"), // "" + ("lua", "\u{e620}"), // "" + ("lz", "\u{f410}"), // "" + ("m3u", "\u{f910}"), // "蘿" + ("m3u8", "\u{f910}"), // "蘿" + ("m4a", "\u{f001}"), // "" + ("m4v", "\u{f008}"), // "" + ("magnet", "\u{f076}"), // "" + ("markdown", "\u{e609}"), // "" + ("md", "\u{e609}"), // "" + ("mjs", "\u{e74e}"), // "" + ("mkd", "\u{e609}"), // "" + ("mkv", "\u{f008}"), // "" + ("mobi", "\u{e28b}"), // "" + ("mov", "\u{f008}"), // "" + ("mp3", "\u{f001}"), // "" + ("mp4", "\u{f008}"), // "" + ("msi", "\u{f17a}"), // "" + ("mustache", "\u{e60f}"), // "" + ("nix", "\u{f313}"), // "" + ("npmignore", "\u{e71e}"), // "" + ("o", "\u{e624}"), // "" + ("opus", "\u{f001}"), // "" + ("ogg", "\u{f001}"), // "" + ("ogv", "\u{f008}"), // "" + ("otf", "\u{f031}"), // "" + ("pdf", "\u{f1c1}"), // "" + ("pem", "\u{f805}"), // "" + ("phar", "\u{e608}"), // "" + ("php", "\u{e608}"), // "" + ("pkg", "\u{f187}"), // "" + ("pl", "\u{e769}"), // "" + ("plist", "\u{f302}"), // "" + ("pls", "\u{f910}"), // "蘿" + ("pm", "\u{e769}"), // "" + ("png", "\u{f1c5}"), // "" + ("ppt", "\u{f1c4}"), // "" + ("pptx", "\u{f1c4}"), // "" + ("procfile", "\u{e21e}"), // "" + ("properties", "\u{e60b}"), // "" + ("ps1", "\u{f489}"), // "" + ("psd", "\u{e7b8}"), // "" + ("pub", "\u{e60a}"), // "" + ("pxm", "\u{f1c5}"), // "" + ("py", "\u{e606}"), // "" + ("pyc", "\u{e606}"), // "" + ("r", "\u{fcd2}"), // "ﳒ" + ("rakefile", "\u{e21e}"), // "" + ("rar", "\u{f410}"), // "" + ("razor", "\u{f1fa}"), // "" + ("rb", "\u{e21e}"), // "" + ("rdata", "\u{fcd2}"), // "ﳒ" + ("rdb", "\u{e76d}"), // "" + ("rdoc", "\u{e609}"), // "" + ("rds", "\u{fcd2}"), // "ﳒ" + ("readme", "\u{e609}"), // "" + ("rlib", "\u{e7a8}"), // "" + ("rmd", "\u{e609}"), // "" + ("rpm", "\u{f187}"), // "" + ("rproj", "\u{fac5}"), // "鉶" + ("rs", "\u{e7a8}"), // "" + ("rspec", "\u{e21e}"), // "" + ("rspec_parallel", "\u{e21e}"), // "" + ("rspec_status", "\u{e21e}"), // "" + ("rss", "\u{f09e}"), // "" + ("rtf", "\u{f15c}"), // "" + ("ru", "\u{e21e}"), // "" + ("rubydoc", "\u{e73b}"), // "" + ("s", "\u{e614}"), // "" + ("sass", "\u{e603}"), // "" + ("scala", "\u{e737}"), // "" + ("scpt", "\u{f302}"), // "" + ("scss", "\u{e603}"), // "" + ("sh", "\u{f489}"), // "" + ("shell", "\u{f489}"), // "" + ("sig", "\u{e60a}"), // "" + ("slim", "\u{e73b}"), // "" + ("sln", "\u{e70c}"), // "" + ("so", "\u{e624}"), // "" + ("sql", "\u{f1c0}"), // "" + ("sqlite3", "\u{e7c4}"), // "" + ("srt", "\u{f02d}"), // "" + ("styl", "\u{e600}"), // "" + ("stylus", "\u{e600}"), // "" + ("sub", "\u{f02d}"), // "" + ("sublime-package", "\u{e7aa}"), // "" + ("sublime-session", "\u{e7aa}"), // "" + ("svg", "\u{f1c5}"), // "" + ("swift", "\u{e755}"), // "" + ("swp", "\u{e62b}"), // "" + ("sym", "\u{e624}"), // "" + ("t", "\u{e769}"), // "" + ("tar", "\u{f410}"), // "" + ("tex", "\u{e600}"), // "" + ("tgz", "\u{f410}"), // "" + ("tiff", "\u{f1c5}"), // "" + ("toml", "\u{e60b}"), // "" + ("torrent", "\u{f98c}"), // "歷" + ("ts", "\u{e628}"), // "" + ("tsx", "\u{e7ba}"), // "" + ("ttc", "\u{f031}"), // "" + ("ttf", "\u{f031}"), // "" + ("twig", "\u{e61c}"), // "" + ("txt", "\u{f15c}"), // "" + ("video", "\u{f008}"), // "" + ("vim", "\u{e62b}"), // "" + ("vlc", "\u{f910}"), // "蘿" + ("vue", "\u{fd42}"), // "﵂" + ("wav", "\u{f001}"), // "" + ("webm", "\u{f008}"), // "" + ("webp", "\u{f1c5}"), // "" + ("windows", "\u{f17a}"), // "" + ("wma", "\u{f001}"), // "" + ("wmv", "\u{f008}"), // "" + ("wpl", "\u{f910}"), // "蘿" + ("woff", "\u{f031}"), // "" + ("woff2", "\u{f031}"), // "" + ("xbps", "\u{f187}"), // "" + ("xcf", "\u{f1c5}"), // "" + ("xls", "\u{f1c3}"), // "" + ("xlsx", "\u{f1c3}"), // "" + ("xml", "\u{f121}"), // "" + ("xul", "\u{f269}"), // "" + ("xz", "\u{f410}"), // "" + ("yaml", "\u{e60b}"), // "" + ("yml", "\u{e60b}"), // "" + ("zip", "\u{f410}"), // "" + ("zsh", "\u{f489}"), // "" + ("zsh-theme", "\u{f489}"), // "" + ("zshrc", "\u{f489}"), // "" + ("zst", "\u{f410}"), // "" + ] + .iter() + .map(|&s| (s.0.to_owned(), s.1.to_owned())) + .collect::>() + } +} + +#[cfg(test)] +mod tests { + use super::IconTheme; + use crate::theme::Theme; + + fn partial_default_yaml() -> &'static str { + r#"--- +name: + .trash:  + .cargo:  + .emacs.d:  + a.out:  +extension: + go:  + hs:  + rs:  +filetype: + dir:  + file:  + pipe:  + socket:  + executable:  + symlink-dir:  + symlink-file:  + device-char:  + device-block: ﰩ + special:  +"# + } + + fn check_partial_yaml(def: &IconTheme, yaml: &IconTheme) { + assert_eq!(def.filetype.dir, yaml.filetype.dir,); + } + + #[test] + fn test_default_theme() { + let def = IconTheme::default(); + let yaml = Theme::with_yaml(partial_default_yaml()).unwrap(); + check_partial_yaml(&def, &yaml); + } + + #[test] + fn test_tmp_partial_default_theme_file() { + use std::fs::File; + use std::io::Write; + let dir = assert_fs::TempDir::new().unwrap(); + let theme = dir.path().join("icon.yaml"); + let mut file = File::create(&theme).unwrap(); + writeln!(file, "{}", partial_default_yaml()).unwrap(); + let def = IconTheme::default(); + let decoded = Theme::from_path(theme.to_str().unwrap()).unwrap(); + check_partial_yaml(&def, &decoded); + } + + #[test] + fn test_empty_theme_return_default() { + // Must contain one field at least + // ref https://github.com/dtolnay/serde-yaml/issues/86 + let empty: IconTheme = Theme::with_yaml(" ").unwrap(); + let default = IconTheme::default(); + check_partial_yaml(&empty, &default); + } + + #[test] + fn test_partial_theme_return_default() { + // Must contain one field at least + // ref https://github.com/dtolnay/serde-yaml/issues/86 + let empty: IconTheme = Theme::with_yaml("filetype:\n dir: ").unwrap(); //  is the default value + let default = IconTheme::default(); + check_partial_yaml(&empty, &default); + } + + #[test] + fn test_serde_dir_from_yaml() { + // Must contain one field at least + // ref https://github.com/dtolnay/serde-yaml/issues/86 + let empty: IconTheme = Theme::with_yaml("filetype:\n dir: ").unwrap(); + assert_eq!(empty.filetype.dir, ""); + } +}