ソースを参照

feat: theme file configuration base

PThorpe92 2 年 前
コミット
8715840d3a
7 ファイル変更184 行追加90 行削除
  1. 6 4
      man/eza.1.md
  2. 134 0
      src/options/config.rs
  3. 2 1
      src/options/flags.rs
  4. 1 2
      src/options/mod.rs
  5. 4 8
      src/options/theme.rs
  6. 26 21
      src/theme/mod.rs
  7. 11 54
      src/theme/ui_styles.rs

+ 6 - 4
man/eza.1.md

@@ -47,6 +47,11 @@ META OPTIONS
 `-v`, `--version`
 : Show version of eza.
 
+`--write-theme=DIR`
+: Write _eza_ default theme.yml file to the directory passed as argument, or defaults to the current working directory.
+
+`--config` [if eza was built with config support]
+: Specify a custom path to configuration file.
 
 DISPLAY OPTIONS
 ===============
@@ -257,9 +262,6 @@ Alternatively, `<FORMAT>` can be a two line string, the first line will be used
 `--stdin`
 : When you wish to pipe directories to eza/read from stdin. Separate one per line or define custom separation char in `EZA_STDIN_SEPARATOR` env variable.
 
-`--write-theme=DIR`
-: Write _eza_ default theme.yml file to the directory passed as argument, or defaults to the current working directory.
-
 `-@`, `--extended`
 : List each file’s extended attributes and sizes.
 
@@ -350,7 +352,7 @@ Any explicit use of the `--icons=WHEN` flag overrides this behavior.
 
 Specifies the separator to use when file names are piped from stdin. Defaults to newline.
 
-## EZA_CONFIG_DIR
+## `EZA_CONFIG_DIR`
 
 Specifies the directory where eza will look for its configuration and theme files. Defaults to `$XDG_CONFIG_HOME/eza` or `$HOME/.config/eza` if `XDG_CONFIG_HOME` is not set.
 

+ 134 - 0
src/options/config.rs

@@ -0,0 +1,134 @@
+use crate::options::{MatchedFlags, Vars};
+use crate::output::color_scale::ColorScaleOptions;
+use crate::theme::UiStyles;
+use dirs;
+use serde_yaml;
+use std::{ffi::OsStr, path::PathBuf};
+
+use super::{flags, OptionsError};
+
+#[derive(Debug, Default, Eq, PartialEq)]
+pub struct ThemeConfig {
+    pub location: ConfigLoc,
+    pub theme: UiStyles,
+}
+
+#[derive(Debug, Default, PartialEq, Eq)]
+pub enum ConfigLoc {
+    #[default]
+    Default, // $XDG_CONFIG_HOME/eza/config|theme.yml
+    Env(PathBuf), // $EZA_CONFIG_DIR
+    Arg(PathBuf), // --config path/to/config|theme.yml
+}
+
+impl ThemeConfig {
+    pub fn write_default_theme_file(path: Option<&OsStr>) -> std::io::Result<()> {
+        if path.is_some_and(|path| std::path::Path::new(path).is_dir()) {
+            let path = std::path::Path::new(path.unwrap());
+            let path = path.join("theme.yml");
+            let file = std::fs::File::create(path.clone())?;
+            println!("Writing default theme to {:?}", path);
+            serde_yaml::to_writer(file, &UiStyles::default())
+                .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
+        } else {
+            let default_path = std::env::var("EZA_CONFIG_DIR")
+                .map(|dir| PathBuf::from(&dir))
+                .unwrap_or(dirs::config_dir().unwrap_or_default().join("eza"));
+            if !default_path.exists() {
+                std::fs::create_dir_all(&default_path)?;
+            }
+            println!("Writing default theme to {:?}", default_path);
+            let default_file = default_path.join("theme.yml");
+            let file = std::fs::File::create(default_file)?;
+            let default = UiStyles::default();
+            serde_yaml::to_writer(file, &default)
+                .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
+        }
+    }
+
+    pub fn theme_from_yaml(file: Option<&str>) -> UiStyles {
+        if let Some(file) = file {
+            let file = std::fs::File::open(file);
+            if let Err(e) = file {
+                eprintln!("Could not open theme file: {e}");
+                return UiStyles::default();
+            }
+            let file = file.expect("Could not open theme file");
+            let theme: UiStyles = serde_yaml::from_reader(file).unwrap_or_else(|e| {
+                eprintln!("Could not parse theme file: {e}");
+                UiStyles::default()
+            });
+            theme
+        } else {
+            UiStyles::default()
+        }
+    }
+    pub fn deduce<V: Vars>(
+        matches: &MatchedFlags<'_>,
+        vars: &V,
+        opts: ColorScaleOptions,
+    ) -> Result<ThemeConfig, crate::options::OptionsError> {
+        println!("Deducing theme");
+        if matches.has(&flags::WRITE_THEME)? {
+            let path = matches.get(&flags::WRITE_THEME)?;
+            println!("Writing default theme to {:?}", path);
+            let err = Self::write_default_theme_file(path).map_err(|e| e.to_string());
+            if let Err(err) = err {
+                return Err(OptionsError::WriteTheme(err));
+            }
+        }
+        let theme_file = if matches.has(&flags::THEME)? {
+            let path = matches.get(&flags::THEME)?;
+            // passing --config will require a value as we will check default location
+            if path.is_none() {
+                return Err(OptionsError::BadArgument(&flags::THEME, "no value".into()));
+            }
+            path.map(|p| p.to_string_lossy().to_string())
+        } else {
+            None
+        };
+        Ok(Self::find_with_fallback(theme_file, vars, opts))
+    }
+
+    pub fn find_with_fallback<V: Vars>(
+        path: Option<String>,
+        vars: &V,
+        opts: ColorScaleOptions,
+    ) -> Self {
+        if let Some(path) = path {
+            let path = std::path::PathBuf::from(path);
+            if path.is_dir() && path.exists() {
+                let path = path
+                    .join("theme.yml")
+                    .exists()
+                    .then(|| path.join("theme.yml"));
+                match path {
+                    Some(path) => {
+                        let file = std::fs::read_to_string(&path).unwrap_or_default();
+                        let uistyles: Option<UiStyles> = serde_yaml::from_str(&file).ok();
+                        return Self {
+                            location: ConfigLoc::Arg(path),
+                            theme: uistyles.unwrap_or(UiStyles::default_theme(opts)),
+                        };
+                    }
+                    None => return Self::default(),
+                }
+            }
+        } else if vars.get("EZA_CONFIG_DIR").is_some() {
+            let path = std::path::PathBuf::from(&format!(
+                "{}/theme.yml",
+                vars.get("EZA_CONFIG_DIR").unwrap().to_string_lossy()
+            ));
+            if path.exists() {
+                let file = std::fs::read_to_string(&path).unwrap_or_default();
+                let uistyles: Option<UiStyles> = serde_yaml::from_str(&file).ok();
+                return Self {
+                    location: ConfigLoc::Env(path),
+                    theme: uistyles.unwrap_or(UiStyles::default_theme(opts)),
+                };
+            }
+            return Self::default();
+        };
+        Self::default()
+    }
+}

+ 2 - 1
src/options/flags.rs

@@ -87,6 +87,7 @@ pub static SECURITY_CONTEXT:  Arg = Arg { short: Some(b'Z'), long: "context",
 pub static STDIN:             Arg = Arg { short: None,       long: "stdin",                takes_value: TakesValue::Forbidden };
 pub static FILE_FLAGS:        Arg = Arg { short: Some(b'O'), long: "flags",                takes_value: TakesValue::Forbidden };
 pub static WRITE_THEME:       Arg = Arg { short: None,       long: "write-theme",          takes_value: TakesValue::Optional(None, ".")};
+pub static THEME:             Arg = Arg { short: None,       long: "theme",                takes_value: TakesValue::Optional(None, ".")};
 pub static ALL_ARGS: Args = Args(&[
     &VERSION, &HELP,
 
@@ -102,5 +103,5 @@ pub static ALL_ARGS: Args = Args(&[
     &NO_PERMISSIONS, &NO_FILESIZE, &NO_USER, &NO_TIME, &SMART_GROUP, &NO_SYMLINKS, &SHOW_SYMLINKS,
 
     &GIT, &NO_GIT, &GIT_REPOS, &GIT_REPOS_NO_STAT,
-    &EXTENDED, &OCTAL, &SECURITY_CONTEXT, &STDIN, &FILE_FLAGS, &WRITE_THEME
+    &EXTENDED, &OCTAL, &SECURITY_CONTEXT, &STDIN, &FILE_FLAGS, &WRITE_THEME, &THEME
 ]);

+ 1 - 2
src/options/mod.rs

@@ -95,7 +95,7 @@ use self::parser::MatchedFlags;
 
 pub mod vars;
 pub use self::vars::Vars;
-
+pub mod config;
 pub mod stdin;
 mod version;
 
@@ -200,7 +200,6 @@ impl Options {
                 "Options --git and --git-ignore can't be used because `git` feature was disabled in this build of exa"
             )));
         }
-
         let view = View::deduce(matches, vars)?;
         let dir_action = DirAction::deduce(matches, matches!(view.mode, Mode::Details(_)))?;
         let filter = FileFilter::deduce(matches)?;

+ 4 - 8
src/options/theme.rs

@@ -1,20 +1,15 @@
 use crate::options::parser::MatchedFlags;
 use crate::options::{flags, vars, OptionsError, Vars};
 use crate::output::color_scale::ColorScaleOptions;
-use crate::theme::UiStyles;
 use crate::theme::{Definitions, Options, UseColours};
 
+use super::config::ThemeConfig;
+
 impl Options {
     pub fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
         let use_colours = UseColours::deduce(matches, vars)?;
         let colour_scale = ColorScaleOptions::deduce(matches, vars)?;
-        if matches.has(&flags::WRITE_THEME)? {
-            let path = matches.get(&flags::WRITE_THEME)?;
-            let err = UiStyles::write_default_theme_file(path).map_err(|e| e.to_string());
-            if let Err(err) = err {
-                return Err(OptionsError::WriteTheme(err));
-            }
-        }
+        let theme_config = ThemeConfig::deduce(matches, vars, colour_scale)?;
         let definitions = if use_colours == UseColours::Never {
             Definitions::default()
         } else {
@@ -25,6 +20,7 @@ impl Options {
             use_colours,
             colour_scale,
             definitions,
+            theme_config,
         })
     }
 }

+ 26 - 21
src/theme/mod.rs

@@ -2,6 +2,7 @@ use nu_ansi_term::Style;
 
 use crate::fs::File;
 use crate::info::filetype::FileType;
+use crate::options::config::ThemeConfig;
 use crate::output::color_scale::ColorScaleOptions;
 use crate::output::file_name::Colours as FileNameColours;
 use crate::output::render;
@@ -21,6 +22,8 @@ pub struct Options {
     pub colour_scale: ColorScaleOptions,
 
     pub definitions: Definitions,
+
+    pub theme_config: ThemeConfig,
 }
 
 /// Under what circumstances we should display coloured, rather than plain,
@@ -55,27 +58,29 @@ pub struct Theme {
 
 impl Options {
     pub fn to_theme(&self, isatty: bool) -> Theme {
+        // If the user has explicitly turned off colours, or if we’re not
+        // outputting to a terminal, then we don’t want to use them.
         if self.use_colours == UseColours::Never
             || (self.use_colours == UseColours::Automatic && !isatty)
         {
             let ui = UiStyles::plain();
             let exts = Box::new(NoFileStyle);
-            return Theme { ui, exts };
+            Theme { ui, exts }
+        } else {
+            Theme {
+                ui: self.theme_config.theme.clone(),
+                exts: Box::new(FileTypes),
+            }
         }
-
-        // Parse the environment variables into colours and extension mappings
-        let mut ui = UiStyles::default_theme(self.colour_scale);
-        let (exts, use_default_filetypes) = self.definitions.parse_color_vars(&mut ui);
-
-        // Use between 0 and 2 file name highlighters
-        let exts: Box<dyn FileStyle> = match (exts.is_non_empty(), use_default_filetypes) {
-            (false, false) => Box::new(NoFileStyle),
-            (false, true) => Box::new(FileTypes),
-            (true, false) => Box::new(exts),
-            (true, true) => Box::new((exts, FileTypes)),
-        };
-
-        Theme { ui, exts }
+        //     // Use between 0 and 2 file name highlighters
+        //     let exts: Box<dyn FileStyle> = match (exts.is_non_empty(), use_default_filetypes) {
+        //         (false, false) => Box::new(NoFileStyle),
+        //         (false, true) => Box::new(FileTypes),
+        //         (true, false) => Box::new(exts),
+        //         (true, true) => Box::new((exts, FileTypes)),
+        //     };
+        //
+        //     Theme { ui, exts }
     }
 }
 
@@ -264,7 +269,7 @@ impl render::FiletypeColours for Theme {
 
 #[rustfmt::skip]
 impl render::GitColours for Theme {
-    fn not_modified(&self)  -> Style { self.ui.punctuation.unwrap_or_default() }
+    fn not_modified(&self)  -> Style { self.ui.punctuation() }
     #[allow(clippy::new_ret_no_self)]
     fn new(&self)           -> Style { self.ui.git.unwrap_or_default().new() }
     fn modified(&self)      -> Style { self.ui.git.unwrap_or_default().modified() }
@@ -277,11 +282,11 @@ impl render::GitColours for Theme {
 
 #[rustfmt::skip]
 impl render::GitRepoColours for Theme {
-    fn branch_main(&self)  -> Style { self.ui.git_repo.unwrap_or_default().branch_main.unwrap_or_default() }
-    fn branch_other(&self) -> Style { self.ui.git_repo.unwrap_or_default().branch_other.unwrap_or_default() }
-    fn no_repo(&self)      -> Style { self.ui.punctuation.unwrap_or_default() }
-    fn git_clean(&self)    -> Style { self.ui.git_repo.unwrap_or_default().git_clean.unwrap_or_default() }
-    fn git_dirty(&self)    -> Style { self.ui.git_repo.unwrap_or_default().git_dirty.unwrap_or_default() }
+    fn branch_main(&self)  -> Style { self.ui.git_repo.unwrap_or_default().branch_main() }
+    fn branch_other(&self) -> Style { self.ui.git_repo.unwrap_or_default().branch_other() }
+    fn no_repo(&self)      -> Style { self.ui.punctuation() }
+    fn git_clean(&self)    -> Style { self.ui.git_repo.unwrap_or_default().git_clean() }
+    fn git_dirty(&self)    -> Style { self.ui.git_repo.unwrap_or_default().git_dirty() }
 }
 
 #[rustfmt::skip]

+ 11 - 54
src/theme/ui_styles.rs

@@ -3,11 +3,9 @@ use nu_ansi_term::Color::*;
 use nu_ansi_term::Style;
 use serde::{Deserialize, Serialize};
 use std::default::Default;
-use std::ffi::OsStr;
-use std::path::PathBuf;
 
 #[rustfmt::skip]
-#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
+#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
 pub struct UiStyles {
     pub colourful: Option<bool>,
 
@@ -36,7 +34,6 @@ pub struct UiStyles {
     pub broken_path_overlay:  Option<Style>,  // bO
 }
 // Macro to generate .unwrap_or_default getters for each field to cut down boilerplate
-#[allow(clippy::new_without_default)]
 macro_rules! field_accessors {
     ($struct_name:ident, $($field_name:ident: Option<$type:ty>),*) => {
         impl $struct_name {
@@ -73,7 +70,7 @@ field_accessors!(UiStyles, punctuation: Option<Style>, date: Option<Style>, inod
     control_char: Option<Style>, broken_symlink: Option<Style>, broken_path_overlay: Option<Style>);
 
 #[rustfmt::skip]
-#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
+#[derive(Clone, Eq, Copy, Debug, PartialEq, Serialize, Deserialize)]
 pub struct FileKinds {
     pub normal: Option<Style>,        // fi
     pub directory: Option<Style>,     // di
@@ -106,7 +103,7 @@ impl Default for FileKinds {
 field_accessors!(FileKinds, normal: Option<Style>, directory: Option<Style>, symlink: Option<Style>, pipe: Option<Style>, block_device: Option<Style>, char_device: Option<Style>, socket: Option<Style>, special: Option<Style>, executable: Option<Style>, mount_point: Option<Style>);
 
 #[rustfmt::skip]
-#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
+#[derive(Clone, Copy,Eq, Debug, Default, PartialEq, Serialize, Deserialize)]
 pub struct Permissions {
     pub user_read:         Option<Style>,  // ur
     pub user_write:         Option<Style>,  // uw
@@ -129,7 +126,7 @@ pub struct Permissions {
 field_accessors!(Permissions, user_read: Option<Style>, user_write: Option<Style>, user_execute_file: Option<Style>, user_execute_other: Option<Style>, group_read: Option<Style>, group_write: Option<Style>, group_execute: Option<Style>, other_read: Option<Style>, other_write: Option<Style>, other_execute: Option<Style>, special_user_file: Option<Style>, special_other: Option<Style>, attribute: Option<Style>);
 
 #[rustfmt::skip]
-#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
+#[derive(Clone, Copy, Eq, Debug, Default, PartialEq, Serialize, Deserialize)]
 pub struct Size {
     pub major: Option<Style>,        // df
     pub minor: Option<Style>,        // ds
@@ -149,7 +146,7 @@ pub struct Size {
 field_accessors!(Size, major: Option<Style>, minor: Option<Style>, number_byte: Option<Style>, number_kilo: Option<Style>, number_mega: Option<Style>, number_giga: Option<Style>, number_huge: Option<Style>, unit_byte: Option<Style>, unit_kilo: Option<Style>, unit_mega: Option<Style>, unit_giga: Option<Style>, unit_huge: Option<Style>);
 
 #[rustfmt::skip]
-#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
+#[derive(Clone, Copy, Debug,Eq, Default, PartialEq, Serialize, Deserialize)]
 pub struct Users {
     pub user_you: Option<Style>,           // uu
     pub user_root: Option<Style>,          // uR
@@ -161,7 +158,7 @@ pub struct Users {
 field_accessors!(Users, user_you: Option<Style>, user_root: Option<Style>, user_other: Option<Style>, group_yours: Option<Style>, group_other: Option<Style>, group_root: Option<Style>);
 
 #[rustfmt::skip]
-#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
+#[derive(Clone, Copy, Debug, Eq, Default, PartialEq, Serialize, Deserialize)]
 pub struct Links {
     pub normal: Option<Style>,           // lc
     pub multi_link_file: Option<Style>,  // lm
@@ -169,7 +166,7 @@ pub struct Links {
 field_accessors!(Links, normal: Option<Style>, multi_link_file: Option<Style>);
 
 #[rustfmt::skip]
-#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
+#[derive(Clone, Copy, Debug,Eq, PartialEq, Serialize, Deserialize)]
 pub struct Git {
     pub new: Option<Style>,         // ga
     pub modified: Option<Style>,    // gm
@@ -196,7 +193,7 @@ impl Default for Git {
 }
 
 #[rustfmt::skip]
-#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
 pub struct GitRepo {
     pub branch_main: Option<Style>,  //Gm
     pub branch_other: Option<Style>, //Go
@@ -215,7 +212,7 @@ impl Default for GitRepo {
     }
 }
 
-#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
+#[derive(Clone, Copy, Debug, Eq, Default, PartialEq, Serialize, Deserialize)]
 pub struct SELinuxContext {
     pub colon: Option<Style>,
     pub user: Option<Style>,  // Su
@@ -226,7 +223,7 @@ pub struct SELinuxContext {
 field_accessors!(SELinuxContext, colon: Option<Style>, user: Option<Style>, role: Option<Style>, typ: Option<Style>, range: Option<Style>);
 
 #[rustfmt::skip]
-#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
+#[derive(Clone, Eq, Copy, Debug, PartialEq, Serialize, Deserialize)]
 pub struct SecurityContext {
     pub none:    Option<Style>, // Sn
     pub selinux: Option<SELinuxContext>,
@@ -250,7 +247,7 @@ impl Default for SecurityContext {
 
 /// Drawing styles based on the type of file (video, image, compressed, etc)
 #[rustfmt::skip]
-#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
+#[derive(Clone, Copy, Debug, Eq, Default, PartialEq, Serialize, Deserialize)]
 pub struct FileType {
     pub image: Option<Style>,       // im - image file
     pub video: Option<Style>,       // vi - video file
@@ -270,46 +267,6 @@ impl UiStyles {
         Self::default()
     }
 
-    pub fn write_default_theme_file(path: Option<&OsStr>) -> std::io::Result<()> {
-        let default_path = std::env::var("EZA_CONFIG_DIR").map(|dir| PathBuf::from(&dir)).unwrap_or({
-             dirs::config_dir().unwrap_or_default().join("eza")
-        });
-        if let Ok(dir) = std::env::var("EZA_CONFIG_DIR") {
-            let dir = std::path::PathBuf::from(&dir);
-            if !dir.exists() {
-            std::fs::create_dir_all(dir)?;
-            }
-        }
-        if path.is_some_and(|path| std::path::PathBuf::from(path).is_dir()) {
-            let path = PathBuf::from(path.unwrap());
-            let path = path.join(PathBuf::from("default-theme.yml")); 
-            let file = std::fs::File::create(path)?;
-            serde_yaml::to_writer(file, &Self::default()).map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
-        } else {
-            let default_file = default_path.join("default-theme.yml");
-            let file = std::fs::File::create(default_file)?;
-            let default = Self::default();
-            serde_yaml::to_writer(file, &default).map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
-        }
-    }
-
-    pub fn from_yaml(file: Option<&str>) -> Self {
-        if let Some(file) = file {
-            let file = std::fs::File::open(file);
-            if let Err(e) = file {
-                eprintln!("Could not open theme file: {e}");
-                return Self::default();
-            }
-            let file = file.expect("Could not open theme file");
-            let theme: UiStyles = serde_yaml::from_reader(file).unwrap_or_else(|e| {
-                eprintln!("Could not parse theme file: {e}");
-                Self::default()
-            });
-            theme
-        } else {
-            Self::default()
-        }
-    }
 }
 
 impl UiStyles {