Parcourir la source

Merge pull request #245 from cfxegbert/file-type-themes

feat(ui): Make file types themeable
Christina Sørensen il y a 2 ans
Parent
commit
3f09e4b423
6 fichiers modifiés avec 181 ajouts et 107 suppressions
  1. 1 3
      man/eza_colors-explanation.5.md
  2. 30 0
      man/eza_colors.5.md
  3. 45 71
      src/info/filetype.rs
  4. 13 0
      src/theme/default_theme.rs
  5. 65 33
      src/theme/mod.rs
  6. 27 0
      src/theme/ui_styles.rs

+ 1 - 3
man/eza_colors-explanation.5.md

@@ -25,9 +25,7 @@ files; setting `EXA_COLORS="reset"` will highlight nothing.
 
 - eza now supports bright colours! As supported by most modern 256\-colour terminals, you can now choose from `bright` colour codes when selecting your custom colours in your `#EXA_COLORS` environment variable.
 
-"Immediate" files are the files you should look at when downloading and building a project for the first time: READMEs, Makefiles, Cargo.toml, and others.
-They are highlighted in _yellow_ and _underlined_.
-
+- Build (Makefile, Cargo.toml, package.json) are yellow and underlined.
 - Images (png, jpeg, gif) are purple.
 - Videos (mp4, ogv, m2ts) are a slightly purpler purple.
 - Music (mp3, m4a, ogg) is a deeper purple.

+ 30 - 0
man/eza_colors.5.md

@@ -223,6 +223,36 @@ LIST OF CODES
 `mp`
 : a mount point
 
+`im`
+: a regular file that is an image
+
+`vi`
+: a regular file that is a video
+
+`mu`
+: a regular file that is lossy music
+
+`lo`
+: a regular file that is lossless music
+
+`cr`
+: a regular file that is related to cryptography (ex: key or certificate)
+
+`do`
+: a regular file that is a document (ex: office suite document or PDF)
+
+`co`
+: a regular file this is compressed
+
+`tm`
+: a regular file that is temporary (ex: a text editor's backup file)
+
+`cm`
+: a regular file that is a compilation artifact (ex: Java class file)
+
+`bu`
+: a regular file that is used to build a project (ex: Makefile)
+
 Values in `EXA_COLORS` override those given in `LS_COLORS`, so you don’t need to re-write an existing `LS_COLORS` variable with proprietary extensions.
 
 

+ 45 - 71
src/info/filetype.rs

@@ -7,11 +7,9 @@
 //! # Contributors
 //! Please keep these lists sorted. If you're using vim, :sort i
 
-use ansiterm::Style;
 use phf::{phf_map, Map};
 
 use crate::fs::File;
-use crate::theme::FileColours;
 
 #[derive(Debug, Clone)]
 pub enum FileType {
@@ -24,7 +22,7 @@ pub enum FileType {
     Compressed,
     Temp,
     Compiled,
-    Immediate // An “immediate” file is something that can be run or activated somehow in order to
+    Build     // A “build file is something that can be run or activated somehow in order to
               // kick off the build of a project. It’s usually only present in directories full of
               // source code.
 }
@@ -32,47 +30,47 @@ pub enum FileType {
 /// Mapping from full filenames to file type.
 const FILENAME_TYPES: Map<&'static str, FileType> = phf_map! {
     /* Immediate file - kick off the build of a project */
-    "Brewfile"           => FileType::Immediate,
-    "bsconfig.json"      => FileType::Immediate,
-    "BUILD"              => FileType::Immediate,
-    "BUILD.bazel"        => FileType::Immediate,
-    "build.gradle"       => FileType::Immediate,
-    "build.sbt"          => FileType::Immediate,
-    "build.xml"          => FileType::Immediate,
-    "Cargo.toml"         => FileType::Immediate,
-    "CMakeLists.txt"     => FileType::Immediate,
-    "composer.json"      => FileType::Immediate,
-    "configure"          => FileType::Immediate,
-    "Containerfile"      => FileType::Immediate,
-    "Dockerfile"         => FileType::Immediate,
-    "Earthfile"          => FileType::Immediate,
-    "flake.nix"          => FileType::Immediate,
-    "Gemfile"            => FileType::Immediate,
-    "GNUmakefile"        => FileType::Immediate,
-    "Gruntfile.coffee"   => FileType::Immediate,
-    "Gruntfile.js"       => FileType::Immediate,
-    "jsconfig.json"      => FileType::Immediate,
-    "Justfile"           => FileType::Immediate,
-    "justfile"           => FileType::Immediate,
-    "Makefile"           => FileType::Immediate,
-    "makefile"           => FileType::Immediate,
-    "meson.build"        => FileType::Immediate,
-    "mix.exs"            => FileType::Immediate,
-    "package.json"       => FileType::Immediate,
-    "Pipfile"            => FileType::Immediate,
-    "PKGBUILD"           => FileType::Immediate,
-    "Podfile"            => FileType::Immediate,
-    "pom.xml"            => FileType::Immediate,
-    "Procfile"           => FileType::Immediate,
-    "pyproject.toml"     => FileType::Immediate,
-    "Rakefile"           => FileType::Immediate,
-    "RoboFile.php"       => FileType::Immediate,
-    "SConstruct"         => FileType::Immediate,
-    "tsconfig.json"      => FileType::Immediate,
-    "Vagrantfile"        => FileType::Immediate,
-    "webpack.config.cjs" => FileType::Immediate,
-    "webpack.config.js"  => FileType::Immediate,
-    "WORKSPACE"          => FileType::Immediate,
+    "Brewfile"           => FileType::Build,
+    "bsconfig.json"      => FileType::Build,
+    "BUILD"              => FileType::Build,
+    "BUILD.bazel"        => FileType::Build,
+    "build.gradle"       => FileType::Build,
+    "build.sbt"          => FileType::Build,
+    "build.xml"          => FileType::Build,
+    "Cargo.toml"         => FileType::Build,
+    "CMakeLists.txt"     => FileType::Build,
+    "composer.json"      => FileType::Build,
+    "configure"          => FileType::Build,
+    "Containerfile"      => FileType::Build,
+    "Dockerfile"         => FileType::Build,
+    "Earthfile"          => FileType::Build,
+    "flake.nix"          => FileType::Build,
+    "Gemfile"            => FileType::Build,
+    "GNUmakefile"        => FileType::Build,
+    "Gruntfile.coffee"   => FileType::Build,
+    "Gruntfile.js"       => FileType::Build,
+    "jsconfig.json"      => FileType::Build,
+    "Justfile"           => FileType::Build,
+    "justfile"           => FileType::Build,
+    "Makefile"           => FileType::Build,
+    "makefile"           => FileType::Build,
+    "meson.build"        => FileType::Build,
+    "mix.exs"            => FileType::Build,
+    "package.json"       => FileType::Build,
+    "Pipfile"            => FileType::Build,
+    "PKGBUILD"           => FileType::Build,
+    "Podfile"            => FileType::Build,
+    "pom.xml"            => FileType::Build,
+    "Procfile"           => FileType::Build,
+    "pyproject.toml"     => FileType::Build,
+    "Rakefile"           => FileType::Build,
+    "RoboFile.php"       => FileType::Build,
+    "SConstruct"         => FileType::Build,
+    "tsconfig.json"      => FileType::Build,
+    "Vagrantfile"        => FileType::Build,
+    "webpack.config.cjs" => FileType::Build,
+    "webpack.config.js"  => FileType::Build,
+    "WORKSPACE"          => FileType::Build,
     /* Cryptology files */
     "id_dsa"             => FileType::Crypto,
     "id_ecdsa"           => FileType::Crypto,
@@ -86,7 +84,7 @@ const FILENAME_TYPES: Map<&'static str, FileType> = phf_map! {
 /// extension is added also update the extension icon map.
 const EXTENSION_TYPES: Map<&'static str, FileType> = phf_map! {
     /* Immediate file - kick off the build of a project */
-    "ninja"      => FileType::Immediate,
+    "ninja"      => FileType::Build,
     /* Image files */
     "arw"        => FileType::Image,
     "avif"       => FileType::Image,
@@ -269,10 +267,10 @@ impl FileType {
     /// Lookup the file type based on the file's name, by the file name
     /// lowercase extension, or if the file could be compiled from related
     /// source code.
-    fn get_file_type(file: &File<'_>) -> Option<FileType> {
+    pub(crate) fn get_file_type(file: &File<'_>) -> Option<FileType> {
         // Case-insensitive readme is checked first for backwards compatibility.
         if file.name.to_lowercase().starts_with("readme") {
-            return Some(Self::Immediate)
+            return Some(Self::Build)
         }
         if let Some(file_type) = FILENAME_TYPES.get(&file.name) {
             return Some(file_type.clone())
@@ -291,27 +289,3 @@ impl FileType {
         None
     }
 }
-
-#[derive(Debug)]
-pub struct FileTypeColor;
-
-impl FileColours for FileTypeColor {
-    /// Map from the file type to the display style/color for the file.
-    fn colour_file(&self, file: &File<'_>) -> Option<Style> {
-        use ansiterm::Colour::*;
-
-        match FileType::get_file_type(file) {
-            Some(FileType::Compiled)   => Some(Yellow.normal()),
-            Some(FileType::Compressed) => Some(Red.normal()),
-            Some(FileType::Crypto)     => Some(Green.bold()),
-            Some(FileType::Document)   => Some(Green.normal()),
-            Some(FileType::Image)      => Some(Purple.normal()),
-            Some(FileType::Immediate)  => Some(Yellow.bold().underline()),
-            Some(FileType::Lossless)   => Some(Cyan.bold()),
-            Some(FileType::Music)      => Some(Cyan.normal()),
-            Some(FileType::Temp)       => Some(White.normal()),
-            Some(FileType::Video)      => Some(Purple.bold()),
-            _                          => None
-        }
-    }
-}

+ 13 - 0
src/theme/default_theme.rs

@@ -78,6 +78,19 @@ impl UiStyles {
                 },
             },
 
+            file_type: FileType {
+                image: Purple.normal(),
+                video: Purple.bold(),
+                music: Cyan.normal(),
+                lossless: Cyan.bold(),
+                crypto: Green.bold(),
+                document: Green.normal(),
+                compressed: Red.normal(),
+                temp: White.normal(),
+                compiled: Yellow.normal(),
+                build: Yellow.bold().underline()
+            },
+
             punctuation:  DarkGray.bold(),
             date:         Blue.normal(),
             inode:        Purple.normal(),

+ 65 - 33
src/theme/mod.rs

@@ -1,6 +1,7 @@
 use ansiterm::Style;
 
 use crate::fs::File;
+use crate::info::filetype::FileType;
 use crate::output::file_name::Colours as FileNameColours;
 use crate::output::render;
 
@@ -58,18 +59,16 @@ pub struct Definitions {
 
 pub struct Theme {
     pub ui: UiStyles,
-    pub exts: Box<dyn FileColours>,
+    pub exts: Box<dyn FileStyle>,
 }
 
 impl Options {
 
     #[allow(trivial_casts)]   // the `as Box<_>` stuff below warns about this for some reason
     pub fn to_theme(&self, isatty: bool) -> Theme {
-        use crate::info::filetype::FileTypeColor;
-
         if self.use_colours == UseColours::Never || (self.use_colours == UseColours::Automatic && ! isatty) {
             let ui = UiStyles::plain();
-            let exts = Box::new(NoFileColours);
+            let exts = Box::new(NoFileStyle);
             return Theme { ui, exts };
         }
 
@@ -79,10 +78,10 @@ impl Options {
 
         // Use between 0 and 2 file name highlighters
         let exts = match (exts.is_non_empty(), use_default_filetypes) {
-            (false, false)  => Box::new(NoFileColours)         as Box<_>,
-            (false,  true)  => Box::new(FileTypeColor)         as Box<_>,
-            ( true, false)  => Box::new(exts)                  as Box<_>,
-            ( true,  true)  => Box::new((exts, FileTypeColor)) as Box<_>,
+            (false, false)  => Box::new(NoFileStyle)     as Box<_>,
+            (false,  true)  => Box::new(FileTypes)         as Box<_>,
+            ( true, false)  => Box::new(exts)              as Box<_>,
+            ( true,  true)  => Box::new((exts, FileTypes)) as Box<_>,
         };
 
         Theme { ui, exts }
@@ -144,14 +143,18 @@ impl Definitions {
 }
 
 
-pub trait FileColours: std::marker::Sync {
-    fn colour_file(&self, file: &File<'_>) -> Option<Style>;
+/// Determine the style to paint the text for the filename part of the output.
+pub trait FileStyle: Sync {
+    /// Return the style to paint the filename text for `file` from the given
+    /// `theme`.
+    fn get_style(&self, file: &File<'_>, theme: &Theme) -> Option<Style>;
 }
 
 #[derive(PartialEq, Debug)]
-struct NoFileColours;
-impl FileColours for NoFileColours {
-    fn colour_file(&self, _file: &File<'_>) -> Option<Style> {
+struct NoFileStyle;
+
+impl FileStyle for NoFileStyle {
+    fn get_style(&self, _file: &File<'_>, _theme: &Theme) -> Option<Style> {
         None
     }
 }
@@ -160,13 +163,13 @@ impl FileColours for NoFileColours {
 // first one then try the second one. This lets the user provide their own
 // file type associations, while falling back to the default set if not set
 // explicitly.
-impl<A, B> FileColours for (A, B)
-where A: FileColours,
-      B: FileColours,
+impl<A, B> FileStyle for (A, B)
+where A: FileStyle,
+      B: FileStyle,
 {
-    fn colour_file(&self, file: &File<'_>) -> Option<Style> {
-        self.0.colour_file(file)
-            .or_else(|| self.1.colour_file(file))
+    fn get_style(&self, file: &File<'_>, theme: &Theme) -> Option<Style> {
+        self.0.get_style(file, theme)
+            .or_else(|| self.1.get_style(file, theme))
     }
 }
 
@@ -176,17 +179,6 @@ struct ExtensionMappings {
     mappings: Vec<(glob::Pattern, Style)>,
 }
 
-// Loop through backwards so that colours specified later in the list override
-// colours specified earlier, like we do with options and strict mode
-
-impl FileColours for ExtensionMappings {
-    fn colour_file(&self, file: &File<'_>) -> Option<Style> {
-        self.mappings.iter().rev()
-            .find(|t| t.0.matches(&file.name))
-            .map (|t| t.1)
-    }
-}
-
 impl ExtensionMappings {
     fn is_non_empty(&self) -> bool {
         ! self.mappings.is_empty()
@@ -197,8 +189,37 @@ impl ExtensionMappings {
     }
 }
 
+// Loop through backwards so that colours specified later in the list override
+// colours specified earlier, like we do with options and strict mode
 
+impl FileStyle for ExtensionMappings {
+    fn get_style(&self, file: &File<'_>, _theme: &Theme) -> Option<Style> {
+        self.mappings.iter().rev()
+            .find(|t| t.0.matches(&file.name))
+            .map (|t| t.1)
+    }
+}
 
+#[derive(Debug)]
+struct FileTypes;
+
+impl FileStyle for FileTypes {
+    fn get_style(&self, file: &File<'_>, theme: &Theme) -> Option<Style> {
+        match FileType::get_file_type(file) {
+            Some(FileType::Image)      => Some(theme.ui.file_type.image),
+            Some(FileType::Video)      => Some(theme.ui.file_type.video),
+            Some(FileType::Music)      => Some(theme.ui.file_type.music),
+            Some(FileType::Lossless)   => Some(theme.ui.file_type.lossless),
+            Some(FileType::Crypto)     => Some(theme.ui.file_type.crypto),
+            Some(FileType::Document)   => Some(theme.ui.file_type.document),
+            Some(FileType::Compressed) => Some(theme.ui.file_type.compressed),
+            Some(FileType::Temp)       => Some(theme.ui.file_type.temp),
+            Some(FileType::Compiled)   => Some(theme.ui.file_type.compiled),
+            Some(FileType::Build)      => Some(theme.ui.file_type.build),
+            None                       => None
+        }
+    }
+}
 
 #[cfg(unix)]
 impl render::BlocksColours for Theme {
@@ -320,17 +341,17 @@ impl render::UserColours for Theme {
 }
 
 impl FileNameColours for Theme {
+    fn symlink_path(&self)        -> Style { self.ui.symlink_path }
     fn normal_arrow(&self)        -> Style { self.ui.punctuation }
     fn broken_symlink(&self)      -> Style { self.ui.broken_symlink }
     fn broken_filename(&self)     -> Style { apply_overlay(self.ui.broken_symlink, self.ui.broken_path_overlay) }
-    fn broken_control_char(&self) -> Style { apply_overlay(self.ui.control_char,   self.ui.broken_path_overlay) }
     fn control_char(&self)        -> Style { self.ui.control_char }
-    fn symlink_path(&self)        -> Style { self.ui.symlink_path }
+    fn broken_control_char(&self) -> Style { apply_overlay(self.ui.control_char,   self.ui.broken_path_overlay) }
     fn executable_file(&self)     -> Style { self.ui.filekinds.executable }
     fn mount_point(&self)         -> Style { self.ui.filekinds.mount_point }
 
     fn colour_file(&self, file: &File<'_>) -> Style {
-        self.exts.colour_file(file).unwrap_or(self.ui.filekinds.normal)
+        self.exts.get_style(file, self).unwrap_or(self.ui.filekinds.normal)
     }
 }
 
@@ -537,6 +558,17 @@ mod customs_test {
 
     test!(exa_mp:  ls "", exa "mp=1;34;4"    =>  colours c -> { c.filekinds.mount_point     = Blue.bold().underline(); });
 
+    test!(exa_im:  ls "", exa "im=38;5;128"  =>  colours c -> { c.file_type.image           = Fixed(128).normal(); });
+    test!(exa_vi:  ls "", exa "vi=38;5;129"  =>  colours c -> { c.file_type.video           = Fixed(129).normal(); });
+    test!(exa_mu:  ls "", exa "mu=38;5;130"  =>  colours c -> { c.file_type.music           = Fixed(130).normal(); });
+    test!(exa_lo:  ls "", exa "lo=38;5;131"  =>  colours c -> { c.file_type.lossless        = Fixed(131).normal(); });
+    test!(exa_cr:  ls "", exa "cr=38;5;132"  =>  colours c -> { c.file_type.crypto          = Fixed(132).normal(); });
+    test!(exa_do:  ls "", exa "do=38;5;133"  =>  colours c -> { c.file_type.document        = Fixed(133).normal(); });
+    test!(exa_co:  ls "", exa "co=38;5;134"  =>  colours c -> { c.file_type.compressed      = Fixed(134).normal(); });
+    test!(exa_tm:  ls "", exa "tm=38;5;135"  =>  colours c -> { c.file_type.temp            = Fixed(135).normal(); });
+    test!(exa_cm:  ls "", exa "cm=38;5;136"  =>  colours c -> { c.file_type.compiled        = Fixed(136).normal(); });
+    test!(exa_ie:  ls "", exa "bu=38;5;137"  =>  colours c -> { c.file_type.build           = Fixed(137).normal(); });
+
     // All the while, LS_COLORS treats them as filenames:
     test!(ls_uu:   ls "uu=38;5;117", exa ""  =>  exts [ ("uu", Fixed(117).normal()) ]);
     test!(ls_un:   ls "un=38;5;118", exa ""  =>  exts [ ("un", Fixed(118).normal()) ]);

+ 27 - 0
src/theme/ui_styles.rs

@@ -14,6 +14,7 @@ pub struct UiStyles {
     pub links:            Links,
     pub git:              Git,
     pub security_context: SecurityContext,
+    pub file_type:        FileType,
 
     pub punctuation:  Style,
     pub date:         Style,
@@ -121,6 +122,21 @@ pub struct SecurityContext {
     pub selinux: SELinuxContext,
 }
 
+/// Drawing styles based on the type of file (video, image, compressed, etc)
+#[derive(Clone, Copy, Debug, Default, PartialEq)]
+pub struct FileType {
+    pub image: Style,       // im - image file
+    pub video: Style,       // vi - video file
+    pub music: Style,       // mu - lossy music
+    pub lossless: Style,    // lo - lossless music
+    pub crypto: Style,      // cr - related to cryptography
+    pub document: Style,    // do - document file
+    pub compressed: Style,  // co - compressed file
+    pub temp: Style,        // tm - temporary file
+    pub compiled: Style,    // cm - compilation artifact
+    pub build: Style,       // bu - file that is used to build a project
+}
+
 impl UiStyles {
     pub fn plain() -> Self {
         Self::default()
@@ -213,6 +229,17 @@ impl UiStyles {
 
             "mp" => self.filekinds.mount_point    = pair.to_style(),
 
+            "im" => self.file_type.image          = pair.to_style(),
+            "vi" => self.file_type.video          = pair.to_style(),
+            "mu" => self.file_type.music          = pair.to_style(),
+            "lo" => self.file_type.lossless       = pair.to_style(),
+            "cr" => self.file_type.crypto         = pair.to_style(),
+            "do" => self.file_type.document       = pair.to_style(),
+            "co" => self.file_type.compressed     = pair.to_style(),
+            "tm" => self.file_type.temp           = pair.to_style(),
+            "cm" => self.file_type.compiled       = pair.to_style(),
+            "bu" => self.file_type.build          = pair.to_style(),
+
              _   => return false,
         }