Преглед на файлове

feat: allow writing default theme.yml file for eventual config file implementation

PThorpe92 преди 2 години
родител
ревизия
0b34b758b3
променени са 16 файла, в които са добавени 495 реда и са изтрити 458 реда
  1. 76 0
      Cargo.lock
  2. 1 0
      Cargo.toml
  3. 1 0
      completions/fish/eza.fish
  4. 1 0
      completions/nush/eza.nu
  5. 1 0
      completions/zsh/_eza
  6. 7 0
      man/eza.1.md
  7. 0 133
      src/options/config.rs
  8. 4 0
      src/options/error.rs
  9. 3 3
      src/options/flags.rs
  10. 1 2
      src/options/mod.rs
  11. 8 1
      src/options/theme.rs
  12. 7 4
      src/output/details.rs
  13. 2 2
      src/output/render/groups.rs
  14. 10 3
      src/theme/default_theme.rs
  15. 200 198
      src/theme/mod.rs
  16. 173 112
      src/theme/ui_styles.rs

+ 76 - 0
Cargo.lock

@@ -346,6 +346,27 @@ dependencies = [
  "powerfmt",
 ]
 
+[[package]]
+name = "dirs"
+version = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.48.0",
+]
+
 [[package]]
 name = "dunce"
 version = "1.0.4"
@@ -392,6 +413,7 @@ dependencies = [
  "ansi-width",
  "chrono",
  "criterion",
+ "dirs",
  "git2",
  "glob",
  "libc",
@@ -454,6 +476,17 @@ dependencies = [
  "percent-encoding",
 ]
 
+[[package]]
+name = "getrandom"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
 [[package]]
 name = "git2"
 version = "0.19.0"
@@ -615,6 +648,17 @@ dependencies = [
  "pkg-config",
 ]
 
+[[package]]
+name = "libredox"
+version = "0.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8"
+dependencies = [
+ "bitflags 2.4.0",
+ "libc",
+ "redox_syscall 0.4.1",
+]
+
 [[package]]
 name = "libz-sys"
 version = "1.1.2"
@@ -747,6 +791,12 @@ dependencies = [
  "vcpkg",
 ]
 
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
 [[package]]
 name = "os_pipe"
 version = "1.1.4"
@@ -981,6 +1031,26 @@ dependencies = [
  "bitflags 1.3.2",
 ]
 
+[[package]]
+name = "redox_syscall"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4"
+dependencies = [
+ "getrandom",
+ "libredox",
+ "thiserror",
+]
+
 [[package]]
 name = "regex"
 version = "1.9.5"
@@ -1399,6 +1469,12 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
 [[package]]
 name = "wasm-bindgen"
 version = "0.2.87"

+ 1 - 0
Cargo.toml

@@ -97,6 +97,7 @@ zoneinfo_compiled = "0.5.1"
 ansi-width = "0.1.0"
 serde = { version = "1.0.193", features = ["derive"] }
 serde_yaml = "0.9.29"
+dirs = "5.0.1"
 [dependencies.git2]
 version = "0.19"
 optional = true

+ 1 - 0
completions/fish/eza.fish

@@ -125,5 +125,6 @@ complete -c eza -l git -d "List each file's Git status, if tracked"
 complete -c eza -l no-git -d "Suppress Git status"
 complete -c eza -l git-repos -d "List each git-repos status and branch name"
 complete -c eza -l git-repos-no-status -d "List each git-repos branch name (much faster)"
+complete -c eza -l write-theme -d "Write default theme.yml file to the specified path"
 complete -c eza -s '@' -l extended -d "List each file's extended attributes and sizes"
 complete -c eza -s Z -l context -d "List each file's security context"

+ 1 - 0
completions/nush/eza.nu

@@ -61,4 +61,5 @@ export extern "eza" [
     --context(-Z)              # List each file's security context
     --smart-group              # Only show group if it has a different name from owner
     --stdin                    # When piping to eza. Read file paths from stdin
+    --write-theme              # Write the default theme to some directory: default cwd 
 ]

+ 1 - 0
completions/zsh/_eza

@@ -65,6 +65,7 @@ __eza() {
         --no-git"[Suppress Git status]" \
         --git-repos"[List each git-repos status and branch name]" \
         --git-repos-no-status"[List each git-repos branch name (much faster)]" \
+        --write-theme="[Write the default theme.yml to a directory. default: cwd]" \
         {-@,--extended}"[List each file's extended attributes and sizes]" \
         {-Z,--context}"[List each file's security context]" \
         {-M,--mounts}"[Show mount details (long mode only)]" \

+ 7 - 0
man/eza.1.md

@@ -257,6 +257,9 @@ 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.
 
@@ -347,6 +350,10 @@ 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
+
+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.
+
 EXIT STATUSES
 =============
 

+ 0 - 133
src/options/config.rs

@@ -1,133 +0,0 @@
-use crate::options::theme::ui_styles::UiStyles;
-use nu_ansi_term::Color;
-use serde::{Deserialize, Serialize};
-use std::collections::HashMap;
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct Config {
-    display: Option<DisplayOptions>,
-    theme: Option<Theme>,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct DisplayOptions {
-    icons: Option<DisplayMode>,
-    color: Option<DisplayMode>,
-    hyperlinks: Option<DisplayMode>,
-    quotes: Option<DisplayMode>,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub enum DisplayMode {
-    Always,
-    Auto,
-    Never,
-}
-
-// This will be the base layout for the options in a "Theme" file, so we can separate the
-// verbose theme file from the config file.
-#[derive(Debug, Serialize, Deserialize)]
-pub struct ThemeMapping(pub HashMap<ThemeOption, Vec<Color>>);
-
-// This will represent the theme.yaml file itself
-#[derive(Debug, Serialize, Deserialize)]
-pub struct Theme {
-    pub name: String,
-    pub colors: ThemeMapping,
-}
-
-#[derive(Debug, Serialize, Deserialize, Hash, Eq, PartialEq)]
-pub enum ThemeOption {
-    Directories,
-    Executables,
-    RegularFiles,
-    NamedPipes,
-    Sockets,
-    BlockDevices,
-    CharacterDevices,
-    Symlinks,
-    Orphans,
-    OctalPermissions,
-    UserRead,
-    UserWrite,
-    UserExecuteRegular,
-    UserExecuteOther,
-    GroupRead,
-    GroupWrite,
-    GroupExecuteRegular,
-    GroupExecuteOther,
-    OthersRead,
-    OthersWrite,
-    OthersExecuteRegular,
-    OthersExecuteOther,
-    SetIdsStickyBitFiles,
-    SetIdsStickyBitOther,
-    ExtendedAttribute,
-    FileSizeNumbers(Vec<Option<FileSize>>),
-    FileSizeUnits(Vec<Option<FileSize>>),
-    DeviceMajorId,
-    DeviceMinorId,
-    UserYou,
-    UserRoot,
-    UserElse,
-    GroupYou,
-    GroupRoot,
-    GroupElse,
-    NumberHardLinks,
-    NumberHardLinksRegular,
-    GitNew,
-    GitModified,
-    GitDeleted,
-    GitRenamed,
-    GitMetadataModified,
-    GitIgnored,
-    GitConflicted,
-    GitMainBranch,
-    GitOtherBranch,
-    GitClean,
-    GitDirty,
-    Punctuation,
-    FileDate,
-    FileInode,
-    FileBlocks,
-    TableHeader,
-    SymlinkPath,
-    FilenameEscapedCharacter,
-    BrokenSymlinkOverlay,
-    Special,
-    MountPoint,
-    ImageFile,
-    VideoFile,
-    LossyMusicFile,
-    LosslessMusicFile,
-    CryptographyFile,
-    DocumentFile,
-    CompressedFile,
-    TemporaryFile,
-    CompilationArtifact,
-    BuildFile,
-    SourceCodeFile,
-    NoSecurityContext,
-    SelinuxUser,
-    SelinuxRole,
-    SelinuxType,
-    SelinuxLevel,
-    BsdFileFlags,
-}
-
-#[derive(Debug, Serialize, Deserialize, Hash, Eq, PartialEq)]
-pub enum FileSize {
-    Bytes,
-    Kilobytes,
-    Megabytes,
-    Gigabytes,
-    Terabytes,
-}
-impl Default for Theme {
-    fn default() -> Self {
-        Self {
-            name: "default".to_string(),
-            colors: ThemeMapping::default(),
-        }
-    }
-}

+ 4 - 0
src/options/error.rs

@@ -39,6 +39,9 @@ pub enum OptionsError {
 
     /// A glob ignore was given that failed to be parsed as a pattern.
     FailedGlobPattern(String),
+
+    /// Error writing theme file to disk.
+    WriteTheme(String),
 }
 
 /// The source of a string that failed to be parsed as a number.
@@ -96,6 +99,7 @@ impl fmt::Display for OptionsError {
             Self::TreeAllAll                 => write!(f, "Option --tree is useless given --all --all"),
             Self::FailedParse(s, n, e)       => write!(f, "Value {s:?} not valid for {n}: {e}"),
             Self::FailedGlobPattern(ref e)   => write!(f, "Failed to parse glob pattern: {e}"),
+            Self::WriteTheme(ref e)          => write!(f, "Failed to write theme file: {e}"),
         };
     }
 }

+ 3 - 3
src/options/flags.rs

@@ -11,7 +11,7 @@ pub static GRID:        Arg = Arg { short: Some(b'G'), long: "grid",        take
 pub static ACROSS:      Arg = Arg { short: Some(b'x'), long: "across",      takes_value: TakesValue::Forbidden };
 pub static RECURSE:     Arg = Arg { short: Some(b'R'), long: "recurse",     takes_value: TakesValue::Forbidden };
 pub static TREE:        Arg = Arg { short: Some(b'T'), long: "tree",        takes_value: TakesValue::Forbidden };
-pub static CLASSIFY:    Arg = Arg { short: Some(b'F'), long: "classify",    takes_value: TakesValue::Optional(Some(WHEN), "auto") };
+pub static CLASSIFY:    Arg = Arg { short: Some(b'F'), long: "classify",    takes_value: TakesValue::Forbidden };
 pub static DEREF_LINKS: Arg = Arg { short: Some(b'X'), long: "dereference", takes_value: TakesValue::Forbidden };
 pub static WIDTH:       Arg = Arg { short: Some(b'w'), long: "width",       takes_value: TakesValue::Necessary(None) };
 pub static NO_QUOTES:   Arg = Arg { short: None,       long: "no-quotes",   takes_value: TakesValue::Forbidden };
@@ -86,7 +86,7 @@ pub static OCTAL:             Arg = Arg { short: Some(b'o'), long: "octal-permis
 pub static SECURITY_CONTEXT:  Arg = Arg { short: Some(b'Z'), long: "context",              takes_value: TakesValue::Forbidden };
 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 ALL_ARGS: Args = Args(&[
     &VERSION, &HELP,
 
@@ -102,5 +102,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
+    &EXTENDED, &OCTAL, &SECURITY_CONTEXT, &STDIN, &FILE_FLAGS, &WRITE_THEME
 ]);

+ 1 - 2
src/options/mod.rs

@@ -81,7 +81,6 @@ mod file_name;
 mod filter;
 #[rustfmt::skip] // this module becomes unreadable with rustfmt
 mod flags;
-mod config;
 mod error;
 mod theme;
 mod view;
@@ -248,7 +247,7 @@ pub mod test {
     use crate::options::parser::{Arg, MatchedFlags};
     use std::ffi::OsStr;
 
-    #[derive(PartialEq, Eq, Debug)]
+    #[derive(PartialEq, Eq, Debug, Copy, Clone)]
     pub enum Strictnesses {
         Last,
         Complain,

+ 8 - 1
src/options/theme.rs

@@ -1,13 +1,20 @@
 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};
 
 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 definitions = if use_colours == UseColours::Never {
             Definitions::default()
         } else {

+ 7 - 4
src/output/details.rs

@@ -397,7 +397,7 @@ impl<'a> Render<'a> {
         Row {
             tree: TreeParams::new(TreeDepth::root(), false),
             cells: Some(header),
-            name: TextCell::paint_str(self.theme.ui.header, "Name"),
+            name: TextCell::paint_str(self.theme.ui.header.unwrap_or_default(), "Name"),
         }
     }
 
@@ -421,7 +421,10 @@ impl<'a> Render<'a> {
     }
 
     fn render_xattr(&self, xattr: &Attribute, tree: TreeParams) -> Row {
-        let name = TextCell::paint(self.theme.ui.perms.attribute, format!("{xattr}"));
+        let name = TextCell::paint(
+            self.theme.ui.perms.unwrap_or_default().attribute(),
+            format!("{xattr}"),
+        );
         Row {
             cells: None,
             name,
@@ -435,7 +438,7 @@ impl<'a> Render<'a> {
             total_width: table.widths().total(),
             table,
             inner: rows.into_iter(),
-            tree_style: self.theme.ui.punctuation,
+            tree_style: self.theme.ui.punctuation.unwrap_or_default(),
         }
     }
 
@@ -443,7 +446,7 @@ impl<'a> Render<'a> {
         Iter {
             tree_trunk: TreeTrunk::default(),
             inner: rows.into_iter(),
-            tree_style: self.theme.ui.punctuation,
+            tree_style: self.theme.ui.punctuation.unwrap_or_default(),
         }
     }
 }

+ 2 - 2
src/output/render/groups.rs

@@ -180,7 +180,7 @@ pub mod test {
                 GroupFormat::Regular,
                 file_user
             )
-        )
+        );
     }
 
     #[test]
@@ -203,7 +203,7 @@ pub mod test {
                 GroupFormat::Regular,
                 file_user
             )
-        )
+        );
     }
 
     #[test]

+ 10 - 3
src/theme/default_theme.rs

@@ -4,9 +4,16 @@ use std::default::Default;
 
 use crate::output::color_scale::{ColorScaleMode, ColorScaleOptions};
 use crate::theme::ui_styles::*;
-
+impl UiStyles {
+    pub fn default_theme(scale: ColorScaleOptions) -> Self {
+        Self {
+            size: Some(Size::colourful(scale)),
+            ..Self::default()
+        }
+    }
+}
 impl Default for UiStyles {
-    fn default(scale: Option<ColorScaleOptions>) -> Self {
+    fn default() -> Self {
         Self {
             colourful: Some(true),
 
@@ -45,7 +52,7 @@ impl Default for UiStyles {
                 attribute:           Some(Style::default()),
             }),
 
-            size: Some(Size::colourful(scale.unwrap_or_default())),
+            size: Some(Size::colourful(ColorScaleOptions::default())),
 
             #[rustfmt::skip]
             users:Some(Users {

+ 200 - 198
src/theme/mod.rs

@@ -64,7 +64,7 @@ impl Options {
         }
 
         // Parse the environment variables into colours and extension mappings
-        let mut ui = UiStyles::default(self.colour_scale);
+        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
@@ -199,19 +199,19 @@ impl FileStyle for FileTypes {
     fn get_style(&self, file: &File<'_>, theme: &Theme) -> Option<Style> {
         #[rustfmt::skip]
         return 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),
-            Some(FileType::Source)     => Some(theme.ui.file_type.source),
+            Some(FileType::Image)      => theme.ui.file_type.unwrap_or_default().image,
+            Some(FileType::Video)      => theme.ui.file_type.unwrap_or_default().video,
+            Some(FileType::Music)      => theme.ui.file_type.unwrap_or_default().music,
+            Some(FileType::Lossless)   => theme.ui.file_type.unwrap_or_default().lossless,
+            Some(FileType::Crypto)     => theme.ui.file_type.unwrap_or_default().crypto,
+            Some(FileType::Document)   => theme.ui.file_type.unwrap_or_default().document,
+            Some(FileType::Compressed) => theme.ui.file_type.unwrap_or_default().compressed,
+            Some(FileType::Temp)       => theme.ui.file_type.unwrap_or_default().temp,
+            Some(FileType::Compiled)   => theme.ui.file_type.unwrap_or_default().compiled,
+            Some(FileType::Build)      => theme.ui.file_type.unwrap_or_default().build,
+            Some(FileType::Source)     => theme.ui.file_type.unwrap_or_default().source,
             None                       => None
-        };
+    };
     }
 }
 
@@ -221,98 +221,100 @@ impl render::BlocksColours for Theme {
         use number_prefix::Prefix::*;
 
         #[rustfmt::skip]
-        return match prefix {
-            Some(Kilo | Kibi) => self.ui.size.number_kilo,
-            Some(Mega | Mebi) => self.ui.size.number_mega,
-            Some(Giga | Gibi) => self.ui.size.number_giga,
-            Some(_)           => self.ui.size.number_huge,
-            None              => self.ui.size.number_byte,
+        let style = match prefix {
+            Some(Kilo | Kibi) => self.ui.size.unwrap_or_default().number_kilo,
+            Some(Mega | Mebi) => self.ui.size.unwrap_or_default().number_mega,
+            Some(Giga | Gibi) => self.ui.size.unwrap_or_default().number_giga,
+            Some(_)           => self.ui.size.unwrap_or_default().number_huge,
+            None              => self.ui.size.unwrap_or_default().number_byte,
         };
+        style.unwrap_or_default()
     }
 
     fn unit(&self, prefix: Option<number_prefix::Prefix>) -> Style {
         use number_prefix::Prefix::*;
 
         #[rustfmt::skip]
-        return match prefix {
-            Some(Kilo | Kibi) => self.ui.size.unit_kilo,
-            Some(Mega | Mebi) => self.ui.size.unit_mega,
-            Some(Giga | Gibi) => self.ui.size.unit_giga,
-            Some(_)           => self.ui.size.unit_huge,
-            None              => self.ui.size.unit_byte,
+           let style = match prefix {
+            Some(Kilo | Kibi) => self.ui.size.unwrap_or_default().unit_kilo,
+            Some(Mega | Mebi) => self.ui.size.unwrap_or_default().unit_mega,
+            Some(Giga | Gibi) => self.ui.size.unwrap_or_default().unit_giga,
+            Some(_)           => self.ui.size.unwrap_or_default().unit_huge,
+            None              => self.ui.size.unwrap_or_default().unit_byte,
         };
+        style.unwrap_or_default()
     }
 
     fn no_blocksize(&self) -> Style {
-        self.ui.punctuation
+        self.ui.punctuation.unwrap_or_default()
     }
 }
 
 #[rustfmt::skip]
 impl render::FiletypeColours for Theme {
-    fn normal(&self)       -> Style { self.ui.filekinds.normal }
-    fn directory(&self)    -> Style { self.ui.filekinds.directory }
-    fn pipe(&self)         -> Style { self.ui.filekinds.pipe }
-    fn symlink(&self)      -> Style { self.ui.filekinds.symlink }
-    fn block_device(&self) -> Style { self.ui.filekinds.block_device }
-    fn char_device(&self)  -> Style { self.ui.filekinds.char_device }
-    fn socket(&self)       -> Style { self.ui.filekinds.socket }
-    fn special(&self)      -> Style { self.ui.filekinds.special }
+    fn normal(&self)       -> Style { self.ui.filekinds.unwrap_or_default().normal() }
+    fn directory(&self)    -> Style { self.ui.filekinds.unwrap_or_default().directory() }
+    fn pipe(&self)         -> Style { self.ui.filekinds.unwrap_or_default().pipe() }
+    fn symlink(&self)      -> Style { self.ui.filekinds.unwrap_or_default().symlink() }
+    fn block_device(&self) -> Style { self.ui.filekinds.unwrap_or_default().block_device() }
+    fn char_device(&self)  -> Style { self.ui.filekinds.unwrap_or_default().char_device() }
+    fn socket(&self)       -> Style { self.ui.filekinds.unwrap_or_default().socket() }
+    fn special(&self)      -> Style { self.ui.filekinds.unwrap_or_default().special() }
 }
 
 #[rustfmt::skip]
 impl render::GitColours for Theme {
-    fn not_modified(&self)  -> Style { self.ui.punctuation }
+    fn not_modified(&self)  -> Style { self.ui.punctuation.unwrap_or_default() }
     #[allow(clippy::new_ret_no_self)]
-    fn new(&self)           -> Style { self.ui.git.new }
-    fn modified(&self)      -> Style { self.ui.git.modified }
-    fn deleted(&self)       -> Style { self.ui.git.deleted }
-    fn renamed(&self)       -> Style { self.ui.git.renamed }
-    fn type_change(&self)   -> Style { self.ui.git.typechange }
-    fn ignored(&self)       -> Style { self.ui.git.ignored }
-    fn conflicted(&self)    -> Style { self.ui.git.conflicted }
+    fn new(&self)           -> Style { self.ui.git.unwrap_or_default().new() }
+    fn modified(&self)      -> Style { self.ui.git.unwrap_or_default().modified() }
+    fn deleted(&self)       -> Style { self.ui.git.unwrap_or_default().deleted() }
+    fn renamed(&self)       -> Style { self.ui.git.unwrap_or_default().renamed() }
+    fn type_change(&self)   -> Style { self.ui.git.unwrap_or_default().typechange() }
+    fn ignored(&self)       -> Style { self.ui.git.unwrap_or_default().ignored() }
+    fn conflicted(&self)    -> Style { self.ui.git.unwrap_or_default().conflicted() }
 }
 
 #[rustfmt::skip]
 impl render::GitRepoColours for Theme {
-    fn branch_main(&self)  -> Style { self.ui.git_repo.branch_main }
-    fn branch_other(&self) -> Style { self.ui.git_repo.branch_other }
-    fn no_repo(&self)      -> Style { self.ui.punctuation }
-    fn git_clean(&self)    -> Style { self.ui.git_repo.git_clean }
-    fn git_dirty(&self)    -> Style { self.ui.git_repo.git_dirty }
+    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() }
 }
 
 #[rustfmt::skip]
 #[cfg(unix)]
 impl render::GroupColours for Theme {
-    fn yours(&self)      -> Style { self.ui.users.group_yours }
-    fn not_yours(&self)  -> Style { self.ui.users.group_other }
-    fn root_group(&self) -> Style { self.ui.users.group_root }
-    fn no_group(&self)   -> Style { self.ui.punctuation }
+    fn yours(&self)      -> Style { self.ui.users.unwrap_or_default().group_yours() }
+    fn not_yours(&self)  -> Style { self.ui.users.unwrap_or_default().group_other() }
+    fn root_group(&self) -> Style { self.ui.users.unwrap_or_default().group_root() }
+    fn no_group(&self)   -> Style { self.ui.punctuation() }
 }
 
 #[rustfmt::skip]
 impl render::LinksColours for Theme {
-    fn normal(&self)           -> Style { self.ui.links.normal }
-    fn multi_link_file(&self)  -> Style { self.ui.links.multi_link_file }
+    fn normal(&self)           -> Style { self.ui.links.unwrap_or_default().normal() }
+    fn multi_link_file(&self)  -> Style { self.ui.links.unwrap_or_default().multi_link_file() }
 }
 
 #[rustfmt::skip]
 impl render::PermissionsColours for Theme {
-    fn dash(&self)               -> Style { self.ui.punctuation }
-    fn user_read(&self)          -> Style { self.ui.perms.user_read }
-    fn user_write(&self)         -> Style { self.ui.perms.user_write }
-    fn user_execute_file(&self)  -> Style { self.ui.perms.user_execute_file }
-    fn user_execute_other(&self) -> Style { self.ui.perms.user_execute_other }
-    fn group_read(&self)         -> Style { self.ui.perms.group_read }
-    fn group_write(&self)        -> Style { self.ui.perms.group_write }
-    fn group_execute(&self)      -> Style { self.ui.perms.group_execute }
-    fn other_read(&self)         -> Style { self.ui.perms.other_read }
-    fn other_write(&self)        -> Style { self.ui.perms.other_write }
-    fn other_execute(&self)      -> Style { self.ui.perms.other_execute }
-    fn special_user_file(&self)  -> Style { self.ui.perms.special_user_file }
-    fn special_other(&self)      -> Style { self.ui.perms.special_other }
-    fn attribute(&self)          -> Style { self.ui.perms.attribute }
+    fn dash(&self)               -> Style { self.ui.punctuation() }
+    fn user_read(&self)          -> Style { self.ui.perms.unwrap_or_default().user_read() }
+    fn user_write(&self)         -> Style { self.ui.perms.unwrap_or_default().user_write() }
+    fn user_execute_file(&self)  -> Style { self.ui.perms.unwrap_or_default().user_execute_file() }
+    fn user_execute_other(&self) -> Style { self.ui.perms.unwrap_or_default().user_execute_other() }
+    fn group_read(&self)         -> Style { self.ui.perms.unwrap_or_default().group_read() }
+    fn group_write(&self)        -> Style { self.ui.perms.unwrap_or_default().group_write() }
+    fn group_execute(&self)      -> Style { self.ui.perms.unwrap_or_default().group_execute() }
+    fn other_read(&self)         -> Style { self.ui.perms.unwrap_or_default().other_read() }
+    fn other_write(&self)        -> Style { self.ui.perms.unwrap_or_default().other_write() }
+    fn other_execute(&self)      -> Style { self.ui.perms.unwrap_or_default().other_execute() }
+    fn special_user_file(&self)  -> Style { self.ui.perms.unwrap_or_default().special_user_file() }
+    fn special_other(&self)      -> Style { self.ui.perms.unwrap_or_default().special_other() }
+    fn attribute(&self)          -> Style { self.ui.perms.unwrap_or_default().attribute() }
 }
 
 impl render::SizeColours for Theme {
@@ -321,11 +323,11 @@ impl render::SizeColours for Theme {
 
         #[rustfmt::skip]
         return match prefix {
-            Some(Kilo | Kibi) => self.ui.size.number_kilo,
-            Some(Mega | Mebi) => self.ui.size.number_mega,
-            Some(Giga | Gibi) => self.ui.size.number_giga,
-            Some(_)           => self.ui.size.number_huge,
-            None              => self.ui.size.number_byte,
+            Some(Kilo | Kibi) => self.ui.size.unwrap_or_default().number_kilo(),
+            Some(Mega | Mebi) => self.ui.size.unwrap_or_default().number_mega(),
+            Some(Giga | Gibi) => self.ui.size.unwrap_or_default().number_giga(),
+            Some(_)           => self.ui.size.unwrap_or_default().number_huge(),
+            None              => self.ui.size.unwrap_or_default().number_byte(),
         };
     }
 
@@ -334,61 +336,61 @@ impl render::SizeColours for Theme {
 
         #[rustfmt::skip]
         return match prefix {
-            Some(Kilo | Kibi) => self.ui.size.unit_kilo,
-            Some(Mega | Mebi) => self.ui.size.unit_mega,
-            Some(Giga | Gibi) => self.ui.size.unit_giga,
-            Some(_)           => self.ui.size.unit_huge,
-            None              => self.ui.size.unit_byte,
+            Some(Kilo | Kibi) => self.ui.size.unwrap_or_default().unit_kilo(),
+            Some(Mega | Mebi) => self.ui.size.unwrap_or_default().unit_mega(),
+            Some(Giga | Gibi) => self.ui.size.unwrap_or_default().unit_giga(),
+            Some(_)           => self.ui.size.unwrap_or_default().unit_huge(),
+            None              => self.ui.size.unwrap_or_default().unit_byte(),
         };
     }
 
     #[rustfmt::skip]
-    fn no_size(&self) -> Style { self.ui.punctuation }
+    fn no_size(&self) -> Style { self.ui.punctuation() }
     #[rustfmt::skip]
-    fn major(&self)   -> Style { self.ui.size.major }
+    fn major(&self)   -> Style { self.ui.size.unwrap_or_default().major() }
     #[rustfmt::skip]
-    fn comma(&self)   -> Style { self.ui.punctuation }
+    fn comma(&self)   -> Style { self.ui.punctuation() }
     #[rustfmt::skip]
-    fn minor(&self)   -> Style { self.ui.size.minor }
+    fn minor(&self)   -> Style { self.ui.size.unwrap_or_default().minor() }
 }
 
 #[rustfmt::skip]
 #[cfg(unix)]
 impl render::UserColours for Theme {
-    fn you(&self)           -> Style { self.ui.users.user_you }
-    fn other(&self)         -> Style { self.ui.users.user_other }
-    fn root(&self)          -> Style { self.ui.users.user_root }
-    fn no_user(&self)       -> Style { self.ui.punctuation }
+    fn you(&self)           -> Style { self.ui.users.unwrap_or_default().user_you() }
+    fn other(&self)         -> Style { self.ui.users.unwrap_or_default().user_other() }
+    fn root(&self)          -> Style { self.ui.users.unwrap_or_default().user_root() }
+    fn no_user(&self)       -> Style { self.ui.punctuation() }
 }
 
 #[rustfmt::skip]
 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 control_char(&self)        -> Style { self.ui.control_char }
-    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 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 control_char(&self)        -> Style { self.ui.control_char() }
+    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.unwrap_or_default().executable() }
+    fn mount_point(&self)         -> Style { self.ui.filekinds.unwrap_or_default().mount_point() }
 
     fn icon(&self)          -> Option<Style> { self.ui.icon }
 
     fn colour_file(&self, file: &File<'_>) -> Style {
         self.exts
             .get_style(file, self)
-            .unwrap_or(self.ui.filekinds.normal)
+            .unwrap_or(self.ui.filekinds.unwrap_or_default().normal())
     }
 }
 
 #[rustfmt::skip]
 impl render::SecurityCtxColours for Theme {
-    fn none(&self)          -> Style { self.ui.security_context.none }
-    fn selinux_colon(&self) -> Style { self.ui.security_context.selinux.colon }
-    fn selinux_user(&self)  -> Style { self.ui.security_context.selinux.user }
-    fn selinux_role(&self)  -> Style { self.ui.security_context.selinux.role }
-    fn selinux_type(&self)  -> Style { self.ui.security_context.selinux.typ }
-    fn selinux_range(&self) -> Style { self.ui.security_context.selinux.range }
+    fn none(&self)          -> Style { self.ui.security_context.unwrap_or_default().none() }
+    fn selinux_colon(&self) -> Style { self.ui.security_context.unwrap_or_default().selinux().colon() }
+    fn selinux_user(&self)  -> Style { self.ui.security_context.unwrap_or_default().selinux().user() }
+    fn selinux_role(&self)  -> Style { self.ui.security_context.unwrap_or_default().selinux().role() }
+    fn selinux_type(&self)  -> Style { self.ui.security_context.unwrap_or_default().selinux().typ() }
+    fn selinux_range(&self) -> Style { self.ui.security_context.unwrap_or_default().selinux().range() }
 }
 
 /// Some of the styles are **overlays**: although they have the same attribute
@@ -488,92 +490,92 @@ mod customs_test {
     }
 
     // LS_COLORS can affect all of these colours:
-    test!(ls_di:   ls "di=31", exa ""  =>  colours c -> { c.filekinds.directory    = Red.normal();    });
-    test!(ls_ex:   ls "ex=32", exa ""  =>  colours c -> { c.filekinds.executable   = Green.normal();  });
-    test!(ls_fi:   ls "fi=33", exa ""  =>  colours c -> { c.filekinds.normal       = Yellow.normal(); });
-    test!(ls_pi:   ls "pi=34", exa ""  =>  colours c -> { c.filekinds.pipe         = Blue.normal();   });
-    test!(ls_so:   ls "so=35", exa ""  =>  colours c -> { c.filekinds.socket       = Purple.normal(); });
-    test!(ls_bd:   ls "bd=36", exa ""  =>  colours c -> { c.filekinds.block_device = Cyan.normal();   });
-    test!(ls_cd:   ls "cd=35", exa ""  =>  colours c -> { c.filekinds.char_device  = Purple.normal(); });
-    test!(ls_ln:   ls "ln=34", exa ""  =>  colours c -> { c.filekinds.symlink      = Blue.normal();   });
+    test!(ls_di:   ls "di=31", exa ""  =>  colours c -> { c.filekinds().directory    = Some(Red.normal());    });
+    test!(ls_ex:   ls "ex=32", exa ""  =>  colours c -> { c.filekinds().executable   = Some(Green.normal());  });
+    test!(ls_fi:   ls "fi=33", exa ""  =>  colours c -> { c.filekinds().normal       = Some(Yellow.normal()); });
+    test!(ls_pi:   ls "pi=34", exa ""  =>  colours c -> { c.filekinds().pipe         = Some(Blue.normal());   });
+    test!(ls_so:   ls "so=35", exa ""  =>  colours c -> { c.filekinds().socket       = Some(Purple.normal()); });
+    test!(ls_bd:   ls "bd=36", exa ""  =>  colours c -> { c.filekinds().block_device = Some(Cyan.normal());   });
+    test!(ls_cd:   ls "cd=35", exa ""  =>  colours c -> { c.filekinds().char_device  = Some(Purple.normal()); });
+    test!(ls_ln:   ls "ln=34", exa ""  =>  colours c -> { c.filekinds().symlink      = Some(Blue.normal());   });
     test!(ls_or:   ls "or=33", exa ""  =>  colours c -> { c.broken_symlink         = Some(Yellow.normal()); });
 
     // EZA_COLORS can affect all those colours too:
-    test!(exa_di:  ls "", exa "di=32"  =>  colours c -> { c.filekinds.directory    = Green.normal();  });
-    test!(exa_ex:  ls "", exa "ex=33"  =>  colours c -> { c.filekinds.executable   = Yellow.normal(); });
-    test!(exa_fi:  ls "", exa "fi=34"  =>  colours c -> { c.filekinds.normal       = Blue.normal();   });
-    test!(exa_pi:  ls "", exa "pi=35"  =>  colours c -> { c.filekinds.pipe         = Purple.normal(); });
-    test!(exa_so:  ls "", exa "so=36"  =>  colours c -> { c.filekinds.socket       = Cyan.normal();   });
-    test!(exa_bd:  ls "", exa "bd=35"  =>  colours c -> { c.filekinds.block_device = Purple.normal(); });
-    test!(exa_cd:  ls "", exa "cd=34"  =>  colours c -> { c.filekinds.char_device  = Blue.normal();   });
-    test!(exa_ln:  ls "", exa "ln=33"  =>  colours c -> { c.filekinds.symlink      = Yellow.normal(); });
+    test!(exa_di:  ls "", exa "di=32"  =>  colours c -> { c.filekinds().directory    = Some(Green.normal());  });
+    test!(exa_ex:  ls "", exa "ex=33"  =>  colours c -> { c.filekinds().executable   = Some(Yellow.normal()); });
+    test!(exa_fi:  ls "", exa "fi=34"  =>  colours c -> { c.filekinds().normal       = Some(Blue.normal());   });
+    test!(exa_pi:  ls "", exa "pi=35"  =>  colours c -> { c.filekinds().pipe         = Some(Purple.normal()); });
+    test!(exa_so:  ls "", exa "so=36"  =>  colours c -> { c.filekinds().socket       = Some(Cyan.normal());   });
+    test!(exa_bd:  ls "", exa "bd=35"  =>  colours c -> { c.filekinds().block_device = Some(Purple.normal()); });
+    test!(exa_cd:  ls "", exa "cd=34"  =>  colours c -> { c.filekinds().char_device  = Some(Blue.normal());   });
+    test!(exa_ln:  ls "", exa "ln=33"  =>  colours c -> { c.filekinds().symlink      = Some(Yellow.normal()); });
     test!(exa_or:  ls "", exa "or=32"  =>  colours c -> { c.broken_symlink         = Some(Green.normal());  });
 
     // EZA_COLORS will even override options from LS_COLORS:
-    test!(ls_exa_di: ls "di=31", exa "di=32"  =>  colours c -> { c.filekinds.directory  = Green.normal();  });
-    test!(ls_exa_ex: ls "ex=32", exa "ex=33"  =>  colours c -> { c.filekinds.executable = Yellow.normal(); });
-    test!(ls_exa_fi: ls "fi=33", exa "fi=34"  =>  colours c -> { c.filekinds.normal     = Blue.normal();   });
+    test!(ls_exa_di: ls "di=31", exa "di=32"  =>  colours c -> { c.filekinds().directory  = Some(Green.normal());  });
+    test!(ls_exa_ex: ls "ex=32", exa "ex=33"  =>  colours c -> { c.filekinds().executable = Some(Yellow.normal()); });
+    test!(ls_exa_fi: ls "fi=33", exa "fi=34"  =>  colours c -> { c.filekinds().normal     = Some(Blue.normal());   });
 
     // But more importantly, EZA_COLORS has its own, special list of colours:
-    test!(exa_ur:  ls "", exa "ur=38;5;100"  =>  colours c -> { c.perms.user_read           = Fixed(100).normal(); });
-    test!(exa_uw:  ls "", exa "uw=38;5;101"  =>  colours c -> { c.perms.user_write          = Fixed(101).normal(); });
-    test!(exa_ux:  ls "", exa "ux=38;5;102"  =>  colours c -> { c.perms.user_execute_file   = Fixed(102).normal(); });
-    test!(exa_ue:  ls "", exa "ue=38;5;103"  =>  colours c -> { c.perms.user_execute_other  = Fixed(103).normal(); });
-    test!(exa_gr:  ls "", exa "gr=38;5;104"  =>  colours c -> { c.perms.group_read          = Fixed(104).normal(); });
-    test!(exa_gw:  ls "", exa "gw=38;5;105"  =>  colours c -> { c.perms.group_write         = Fixed(105).normal(); });
-    test!(exa_gx:  ls "", exa "gx=38;5;106"  =>  colours c -> { c.perms.group_execute       = Fixed(106).normal(); });
-    test!(exa_tr:  ls "", exa "tr=38;5;107"  =>  colours c -> { c.perms.other_read          = Fixed(107).normal(); });
-    test!(exa_tw:  ls "", exa "tw=38;5;108"  =>  colours c -> { c.perms.other_write         = Fixed(108).normal(); });
-    test!(exa_tx:  ls "", exa "tx=38;5;109"  =>  colours c -> { c.perms.other_execute       = Fixed(109).normal(); });
-    test!(exa_su:  ls "", exa "su=38;5;110"  =>  colours c -> { c.perms.special_user_file   = Fixed(110).normal(); });
-    test!(exa_sf:  ls "", exa "sf=38;5;111"  =>  colours c -> { c.perms.special_other       = Fixed(111).normal(); });
-    test!(exa_xa:  ls "", exa "xa=38;5;112"  =>  colours c -> { c.perms.attribute           = Fixed(112).normal(); });
+    test!(exa_ur:  ls "", exa "ur=38;5;100"  =>  colours c -> { c.perms().user_read           = Some(Fixed(100).normal()); });
+    test!(exa_uw:  ls "", exa "uw=38;5;101"  =>  colours c -> { c.perms().user_write          = Some(Fixed(101).normal()); });
+    test!(exa_ux:  ls "", exa "ux=38;5;102"  =>  colours c -> { c.perms().user_execute_file   = Some(Fixed(102).normal()); });
+    test!(exa_ue:  ls "", exa "ue=38;5;103"  =>  colours c -> { c.perms().user_execute_other  = Some(Fixed(103).normal()); });
+    test!(exa_gr:  ls "", exa "gr=38;5;104"  =>  colours c -> { c.perms().group_read          = Some(Fixed(104).normal()); });
+    test!(exa_gw:  ls "", exa "gw=38;5;105"  =>  colours c -> { c.perms().group_write         = Some(Fixed(105).normal()); });
+    test!(exa_gx:  ls "", exa "gx=38;5;106"  =>  colours c -> { c.perms().group_execute       = Some(Fixed(106).normal()); });
+    test!(exa_tr:  ls "", exa "tr=38;5;107"  =>  colours c -> { c.perms().other_read          = Some(Fixed(107).normal()); });
+    test!(exa_tw:  ls "", exa "tw=38;5;108"  =>  colours c -> { c.perms().other_write         = Some(Fixed(108).normal()); });
+    test!(exa_tx:  ls "", exa "tx=38;5;109"  =>  colours c -> { c.perms().other_execute       = Some(Fixed(109).normal()); });
+    test!(exa_su:  ls "", exa "su=38;5;110"  =>  colours c -> { c.perms().special_user_file   = Some(Fixed(110).normal()); });
+    test!(exa_sf:  ls "", exa "sf=38;5;111"  =>  colours c -> { c.perms().special_other       = Some(Fixed(111).normal()); });
+    test!(exa_xa:  ls "", exa "xa=38;5;112"  =>  colours c -> { c.perms().attribute           = Some(Fixed(112).normal()); });
 
     test!(exa_sn:  ls "", exa "sn=38;5;113" => colours c -> {
-        c.size.number_byte = Fixed(113).normal();
-        c.size.number_kilo = Fixed(113).normal();
-        c.size.number_mega = Fixed(113).normal();
-        c.size.number_giga = Fixed(113).normal();
-        c.size.number_huge = Fixed(113).normal();
+        c.size().number_byte = Some(Fixed(113).normal());
+        c.size().number_kilo = Some(Fixed(113).normal());
+        c.size().number_mega = Some(Fixed(113).normal());
+        c.size().number_giga = Some(Fixed(113).normal());
+        c.size().number_huge = Some(Fixed(113).normal());
     });
     test!(exa_sb:  ls "", exa "sb=38;5;114" => colours c -> {
-        c.size.unit_byte = Fixed(114).normal();
-        c.size.unit_kilo = Fixed(114).normal();
-        c.size.unit_mega = Fixed(114).normal();
-        c.size.unit_giga = Fixed(114).normal();
-        c.size.unit_huge = Fixed(114).normal();
+        c.size().unit_byte = Some(Fixed(114).normal());
+        c.size().unit_kilo = Some(Fixed(114).normal());
+        c.size().unit_mega = Some(Fixed(114).normal());
+        c.size().unit_giga = Some(Fixed(114).normal());
+        c.size().unit_huge = Some(Fixed(114).normal());
     });
 
-    test!(exa_nb:  ls "", exa "nb=38;5;115"  =>  colours c -> { c.size.number_byte                      = Fixed(115).normal(); });
-    test!(exa_nk:  ls "", exa "nk=38;5;116"  =>  colours c -> { c.size.number_kilo                      = Fixed(116).normal(); });
-    test!(exa_nm:  ls "", exa "nm=38;5;117"  =>  colours c -> { c.size.number_mega                      = Fixed(117).normal(); });
-    test!(exa_ng:  ls "", exa "ng=38;5;118"  =>  colours c -> { c.size.number_giga                      = Fixed(118).normal(); });
-    test!(exa_nt:  ls "", exa "nt=38;5;119"  =>  colours c -> { c.size.number_huge                      = Fixed(119).normal(); });
-
-    test!(exa_ub:  ls "", exa "ub=38;5;115"  =>  colours c -> { c.size.unit_byte                        = Fixed(115).normal(); });
-    test!(exa_uk:  ls "", exa "uk=38;5;116"  =>  colours c -> { c.size.unit_kilo                        = Fixed(116).normal(); });
-    test!(exa_um:  ls "", exa "um=38;5;117"  =>  colours c -> { c.size.unit_mega                        = Fixed(117).normal(); });
-    test!(exa_ug:  ls "", exa "ug=38;5;118"  =>  colours c -> { c.size.unit_giga                        = Fixed(118).normal(); });
-    test!(exa_ut:  ls "", exa "ut=38;5;119"  =>  colours c -> { c.size.unit_huge                        = Fixed(119).normal(); });
-
-    test!(exa_df:  ls "", exa "df=38;5;115"  =>  colours c -> { c.size.major                            = Fixed(115).normal(); });
-    test!(exa_ds:  ls "", exa "ds=38;5;116"  =>  colours c -> { c.size.minor                            = Fixed(116).normal(); });
-
-    test!(exa_uu:  ls "", exa "uu=38;5;117"  =>  colours c -> { c.users.user_you                        = Fixed(117).normal(); });
-    test!(exa_un:  ls "", exa "un=38;5;118"  =>  colours c -> { c.users.user_other                      = Fixed(118).normal(); });
-    test!(exa_gu:  ls "", exa "gu=38;5;119"  =>  colours c -> { c.users.group_yours                     = Fixed(119).normal(); });
-    test!(exa_gn:  ls "", exa "gn=38;5;120"  =>  colours c -> { c.users.group_other                     = Fixed(120).normal(); });
-
-    test!(exa_lc:  ls "", exa "lc=38;5;121"  =>  colours c -> { c.links.normal                          = Fixed(121).normal(); });
-    test!(exa_lm:  ls "", exa "lm=38;5;122"  =>  colours c -> { c.links.multi_link_file                 = Fixed(122).normal(); });
-
-    test!(exa_ga:  ls "", exa "ga=38;5;123"  =>  colours c -> { c.git.new                               = Fixed(123).normal(); });
-    test!(exa_gm:  ls "", exa "gm=38;5;124"  =>  colours c -> { c.git.modified                          = Fixed(124).normal(); });
-    test!(exa_gd:  ls "", exa "gd=38;5;125"  =>  colours c -> { c.git.deleted                           = Fixed(125).normal(); });
-    test!(exa_gv:  ls "", exa "gv=38;5;126"  =>  colours c -> { c.git.renamed                           = Fixed(126).normal(); });
-    test!(exa_gt:  ls "", exa "gt=38;5;127"  =>  colours c -> { c.git.typechange                        = Fixed(127).normal(); });
-    test!(exa_gi:  ls "", exa "gi=38;5;128"  =>  colours c -> { c.git.ignored                           = Fixed(128).normal(); });
-    test!(exa_gc:  ls "", exa "gc=38;5;129"  =>  colours c -> { c.git.conflicted                        = Fixed(129).normal(); });
+    test!(exa_nb:  ls "", exa "nb=38;5;115"  =>  colours c -> { c.size().number_byte                      = Some(Fixed(115).normal()); });
+    test!(exa_nk:  ls "", exa "nk=38;5;116"  =>  colours c -> { c.size().number_kilo                      = Some(Fixed(116).normal()); });
+    test!(exa_nm:  ls "", exa "nm=38;5;117"  =>  colours c -> { c.size().number_mega                      = Some(Fixed(117).normal()); });
+    test!(exa_ng:  ls "", exa "ng=38;5;118"  =>  colours c -> { c.size().number_giga                      = Some(Fixed(118).normal()); });
+    test!(exa_nt:  ls "", exa "nt=38;5;119"  =>  colours c -> { c.size().number_huge                      = Some(Fixed(119).normal()); });
+
+    test!(exa_ub:  ls "", exa "ub=38;5;115"  =>  colours c -> { c.size().unit_byte                        = Some(Fixed(115).normal()); });
+    test!(exa_uk:  ls "", exa "uk=38;5;116"  =>  colours c -> { c.size().unit_kilo                        = Some(Fixed(116).normal()); });
+    test!(exa_um:  ls "", exa "um=38;5;117"  =>  colours c -> { c.size().unit_mega                        = Some(Fixed(117).normal()); });
+    test!(exa_ug:  ls "", exa "ug=38;5;118"  =>  colours c -> { c.size().unit_giga                        = Some(Fixed(118).normal()); });
+    test!(exa_ut:  ls "", exa "ut=38;5;119"  =>  colours c -> { c.size().unit_huge                        = Some(Fixed(119).normal()); });
+
+    test!(exa_df:  ls "", exa "df=38;5;115"  =>  colours c -> { c.size().major                            = Some(Fixed(115).normal()); });
+    test!(exa_ds:  ls "", exa "ds=38;5;116"  =>  colours c -> { c.size().minor                            = Some(Fixed(116).normal()); });
+
+    test!(exa_uu:  ls "", exa "uu=38;5;117"  =>  colours c -> { c.users().user_you                        = Some(Fixed(117).normal()); });
+    test!(exa_un:  ls "", exa "un=38;5;118"  =>  colours c -> { c.users().user_other                      = Some(Fixed(118).normal()); });
+    test!(exa_gu:  ls "", exa "gu=38;5;119"  =>  colours c -> { c.users().group_yours                     = Some(Fixed(119).normal()); });
+    test!(exa_gn:  ls "", exa "gn=38;5;120"  =>  colours c -> { c.users().group_other                     = Some(Fixed(120).normal()); });
+
+    test!(exa_lc:  ls "", exa "lc=38;5;121"  =>  colours c -> { c.links().normal                          = Some(Fixed(121).normal()); });
+    test!(exa_lm:  ls "", exa "lm=38;5;122"  =>  colours c -> { c.links().multi_link_file                 = Some(Fixed(122).normal()); });
+
+    test!(exa_ga:  ls "", exa "ga=38;5;123"  =>  colours c -> { c.git().new                               = Some(Fixed(123).normal()); });
+    test!(exa_gm:  ls "", exa "gm=38;5;124"  =>  colours c -> { c.git().modified                          = Some(Fixed(124).normal()); });
+    test!(exa_gd:  ls "", exa "gd=38;5;125"  =>  colours c -> { c.git().deleted                           = Some(Fixed(125).normal()); });
+    test!(exa_gv:  ls "", exa "gv=38;5;126"  =>  colours c -> { c.git().renamed                           = Some(Fixed(126).normal()); });
+    test!(exa_gt:  ls "", exa "gt=38;5;127"  =>  colours c -> { c.git().typechange                        = Some(Fixed(127).normal()); });
+    test!(exa_gi:  ls "", exa "gi=38;5;128"  =>  colours c -> { c.git().ignored                           = Some(Fixed(128).normal()); });
+    test!(exa_gc:  ls "", exa "gc=38;5;129"  =>  colours c -> { c.git().conflicted                        = Some(Fixed(129).normal()); });
 
     test!(exa_xx:  ls "", exa "xx=38;5;128"  =>  colours c -> { c.punctuation                           = Some(Fixed(128).normal()); });
     test!(exa_da:  ls "", exa "da=38;5;129"  =>  colours c -> { c.date                                  = Some(Fixed(129).normal()); });
@@ -586,27 +588,27 @@ mod customs_test {
     test!(exa_ff:  ls "", exa "ff=38;5;136"  =>  colours c -> { c.flags                                 = Some(Fixed(136).normal()); });
     test!(exa_bo:  ls "", exa "bO=4"         =>  colours c -> { c.broken_path_overlay                   = Some(Style::default().underline()); });
 
-    test!(exa_mp:  ls "", exa "mp=1;34;4"    =>  colours c -> { c.filekinds.mount_point                 = Blue.bold().underline(); });
-    test!(exa_sp:  ls "", exa "sp=1;35;4"    =>  colours c -> { c.filekinds.special                     = Purple.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(); });
-    test!(exa_bu:  ls "", exa "bu=38;5;137"  =>  colours c -> { c.file_type.build                       = Fixed(137).normal(); });
-    test!(exa_sc:  ls "", exa "sc=38;5;138"  =>  colours c -> { c.file_type.source                      = Fixed(138).normal(); });
-
-    test!(exa_Sn:  ls "", exa "Sn=38;5;128"  =>  colours c -> { c.security_context.none                 = Fixed(128).normal(); });
-    test!(exa_Su:  ls "", exa "Su=38;5;129"  =>  colours c -> { c.security_context.selinux.user         = Fixed(129).normal(); });
-    test!(exa_Sr:  ls "", exa "Sr=38;5;130"  =>  colours c -> { c.security_context.selinux.role         = Fixed(130).normal(); });
-    test!(exa_St:  ls "", exa "St=38;5;131"  =>  colours c -> { c.security_context.selinux.typ          = Fixed(131).normal(); });
-    test!(exa_Sl:  ls "", exa "Sl=38;5;132"  =>  colours c -> { c.security_context.selinux.range        = Fixed(132).normal(); });
+    test!(exa_mp:  ls "", exa "mp=1;34;4"    =>  colours c -> { c.filekinds().mount_point                 = Some(Blue.bold().underline()); });
+    test!(exa_sp:  ls "", exa "sp=1;35;4"    =>  colours c -> { c.filekinds().special                     = Some(Purple.bold().underline()); });
+
+    test!(exa_im:  ls "", exa "im=38;5;128"  =>  colours c -> { c.file_type().image                       = Some(Fixed(128).normal()); });
+    test!(exa_vi:  ls "", exa "vi=38;5;129"  =>  colours c -> { c.file_type().video                       = Some(Fixed(129).normal()); });
+    test!(exa_mu:  ls "", exa "mu=38;5;130"  =>  colours c -> { c.file_type().music                       = Some(Fixed(130).normal()); });
+    test!(exa_lo:  ls "", exa "lo=38;5;131"  =>  colours c -> { c.file_type().lossless                    = Some(Fixed(131).normal()); });
+    test!(exa_cr:  ls "", exa "cr=38;5;132"  =>  colours c -> { c.file_type().crypto                      = Some(Fixed(132).normal()); });
+    test!(exa_do:  ls "", exa "do=38;5;133"  =>  colours c -> { c.file_type().document                    = Some(Fixed(133).normal()); });
+    test!(exa_co:  ls "", exa "co=38;5;134"  =>  colours c -> { c.file_type().compressed                  = Some(Fixed(134).normal()); });
+    test!(exa_tm:  ls "", exa "tm=38;5;135"  =>  colours c -> { c.file_type().temp                        = Some(Fixed(135).normal()); });
+    test!(exa_cm:  ls "", exa "cm=38;5;136"  =>  colours c -> { c.file_type().compiled                    = Some(Fixed(136).normal()); });
+    test!(exa_ie:  ls "", exa "bu=38;5;137"  =>  colours c -> { c.file_type().build                       = Some(Fixed(137).normal()); });
+    test!(exa_bu:  ls "", exa "bu=38;5;137"  =>  colours c -> { c.file_type().build                       = Some(Fixed(137).normal()); });
+    test!(exa_sc:  ls "", exa "sc=38;5;138"  =>  colours c -> { c.file_type().source                      = Some(Fixed(138).normal()); });
+
+    test!(exa_Sn:  ls "", exa "Sn=38;5;128"  =>  colours c -> { c.security_context().none                   = Some(Fixed(128).normal()); });
+    test!(exa_Su:  ls "", exa "Su=38;5;129"  =>  colours c -> { c.security_context().selinux().user         = Some(Fixed(129).normal()); });
+    test!(exa_Sr:  ls "", exa "Sr=38;5;130"  =>  colours c -> { c.security_context().selinux().role         = Some(Fixed(130).normal()); });
+    test!(exa_St:  ls "", exa "St=38;5;131"  =>  colours c -> { c.security_context().selinux().typ          = Some(Fixed(131).normal()); });
+    test!(exa_Sl:  ls "", exa "Sl=38;5;132"  =>  colours c -> { c.security_context().selinux().range        = Some(Fixed(132).normal()); });
 
     // All the while, LS_COLORS treats them as filenames:
     test!(ls_uu:   ls "uu=38;5;117", exa ""  =>  exts [ ("uu", Fixed(117).normal()) ]);
@@ -635,12 +637,12 @@ mod customs_test {
     ]);
 
     // Finally, colours get applied right-to-left:
-    test!(ls_overwrite:  ls "pi=31:pi=32:pi=33", exa ""  =>  colours c -> { c.filekinds.pipe = Yellow.normal(); });
+    test!(ls_overwrite:  ls "pi=31:pi=32:pi=33", exa ""  =>  colours c -> { c.filekinds().pipe = Some(Yellow.normal()); });
     test!(exa_overwrite: ls "", exa "da=36:da=35:da=34"  =>  colours c -> { c.date = Some(Blue.normal()); });
 
     // Parse keys and extensions
-    test!(ls_fi_ls_txt:   ls "fi=33:*.txt=31", exa "" => colours c -> { c.filekinds.normal = Yellow.normal(); }, exts [ ("*.txt", Red.normal()) ]);
-    test!(ls_fi_exa_txt:  ls "fi=33", exa "*.txt=31"  => colours c -> { c.filekinds.normal = Yellow.normal(); }, exts [ ("*.txt", Red.normal()) ]);
-    test!(ls_txt_exa_fi:  ls "*.txt=31", exa "fi=33"  => colours c -> { c.filekinds.normal = Yellow.normal(); }, exts [ ("*.txt", Red.normal()) ]);
-    test!(eza_fi_exa_txt: ls "", exa "fi=33:*.txt=31" => colours c -> { c.filekinds.normal = Yellow.normal(); }, exts [ ("*.txt", Red.normal()) ]);
+    test!(ls_fi_ls_txt:   ls "fi=33:*.txt=31", exa "" => colours c -> { c.filekinds().normal = Some(Yellow.normal()); }, exts [ ("*.txt", Red.normal()) ]);
+    test!(ls_fi_exa_txt:  ls "fi=33", exa "*.txt=31"  => colours c -> { c.filekinds().normal = Some(Yellow.normal()); }, exts [ ("*.txt", Red.normal()) ]);
+    test!(ls_txt_exa_fi:  ls "*.txt=31", exa "fi=33"  => colours c -> { c.filekinds().normal = Some(Yellow.normal()); }, exts [ ("*.txt", Red.normal()) ]);
+    test!(eza_fi_exa_txt: ls "", exa "fi=33:*.txt=31" => colours c -> { c.filekinds().normal = Some(Yellow.normal()); }, exts [ ("*.txt", Red.normal()) ]);
 }

+ 173 - 112
src/theme/ui_styles.rs

@@ -1,10 +1,10 @@
-use crate::output::color_scale::ColorScaleOptions;
 use crate::theme::lsc::Pair;
-use libc::SYS_socketpair;
-use nu_ansi_term::Color::{self, *};
+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)]
@@ -15,7 +15,7 @@ pub struct UiStyles {
     pub perms:            Option<Permissions>,
     pub size:             Option<Size>,
     pub users:            Option<Users>,
-    pub links:           Option<Links>,
+    pub links:            Option<Links>,
     pub git:              Option<Git>,
     pub git_repo:         Option<GitRepo>,
     pub security_context: Option<SecurityContext>,
@@ -35,6 +35,42 @@ pub struct UiStyles {
     pub broken_symlink:       Option<Style>,  // or
     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 {
+            $(
+                pub fn $field_name(&self) -> $type {
+                    self.$field_name.unwrap_or_default()
+                }
+            )*
+        }
+    };
+}
+// Macro to generate method that returns a mut ref to each field or creates a default one if it's None
+macro_rules! update_field_accessors {
+    ($struct_name:ident, $($field_name:ident: Option<$type:ty>),*) => {
+        impl $struct_name {
+            $(
+                pub fn $field_name(&mut self) -> &mut $type {
+                    if self.$field_name.is_none() {
+                        self.$field_name = Some(Default::default());
+                    }
+                    // It is safe to unwrap here because we just ensured it's not None
+                    self.$field_name.as_mut().unwrap()
+                }
+            )*
+        }
+    };
+}
+
+update_field_accessors!(UiStyles, colourful: Option<bool>,filekinds: Option<FileKinds>, perms: Option<Permissions>, size: Option<Size>,
+    file_type: Option<FileType>, security_context: Option<SecurityContext>, users: Option<Users>, links: Option<Links>, git: Option<Git>, git_repo: Option<GitRepo>);
+
+field_accessors!(UiStyles, punctuation: Option<Style>, date: Option<Style>, inode: Option<Style>, blocks: Option<Style>,
+    header: Option<Style>, octal: Option<Style>, flags: Option<Style>, symlink_path: Option<Style>,
+    control_char: Option<Style>, broken_symlink: Option<Style>, broken_path_overlay: Option<Style>);
 
 #[rustfmt::skip]
 #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
@@ -67,23 +103,7 @@ impl Default for FileKinds {
         }
     }
 }
-
-impl FileKinds {
-    pub fn colourful(scale: ColorScaleOptions) -> Self {
-        Self {
-            normal: Some(Style::default()),
-            directory: Some(scale.blue.bold()),
-            symlink: Some(scale.cyan.normal()),
-            pipe: Some(scale.yellow.normal()),
-            block_device: Some(scale.yellow.bold()),
-            char_device: Some(scale.yellow.bold()),
-            socket: Some(scale.red.bold()),
-            special: Some(scale.yellow.normal()),
-            executable: Some(scale.green.bold()),
-            mount_point: Some(scale.blue.bold().underline()),
-        }
-    }
-}
+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)]
@@ -106,6 +126,7 @@ pub struct Permissions {
 
     pub attribute: Option<Style>,           // xa
 }
+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)]
@@ -125,6 +146,7 @@ pub struct Size {
     pub unit_giga: Option<Style>,    // sb ug
     pub unit_huge: Option<Style>,    // sb ut
 }
+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)]
@@ -136,6 +158,7 @@ pub struct Users {
     pub group_other: Option<Style>,        // gn
     pub group_root: Option<Style>,         // gR
 }
+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)]
@@ -143,6 +166,7 @@ pub struct Links {
     pub normal: Option<Style>,           // lc
     pub multi_link_file: Option<Style>,  // lm
 }
+field_accessors!(Links, normal: Option<Style>, multi_link_file: Option<Style>);
 
 #[rustfmt::skip]
 #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
@@ -156,6 +180,7 @@ pub struct Git {
     pub conflicted: Option<Style>,  // gc
 }
 
+field_accessors!(Git, new: Option<Style>, modified: Option<Style>, deleted: Option<Style>, renamed: Option<Style>, typechange: Option<Style>, ignored: Option<Style>, conflicted: Option<Style>);
 impl Default for Git {
     fn default() -> Self {
         Git {
@@ -178,7 +203,7 @@ pub struct GitRepo {
     pub git_clean: Option<Style>,    //Gc
     pub git_dirty: Option<Style>,    //Gd
 }
-
+field_accessors!(GitRepo, branch_main: Option<Style>, branch_other: Option<Style>, git_clean: Option<Style>, git_dirty: Option<Style>);
 impl Default for GitRepo {
     fn default() -> Self {
         Self {
@@ -198,13 +223,15 @@ pub struct SELinuxContext {
     pub typ: Option<Style>,   // St
     pub range: Option<Style>, // Sl
 }
+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)]
+#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
 pub struct SecurityContext {
     pub none:    Option<Style>, // Sn
     pub selinux: Option<SELinuxContext>,
 }
+field_accessors!(SecurityContext, none: Option<Style>, selinux: Option<SELinuxContext>);
 
 impl Default for SecurityContext {
     fn default() -> Self {
@@ -243,16 +270,39 @@ 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);
+                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);
+                eprintln!("Could not parse theme file: {e}");
                 Self::default()
             });
             theme
@@ -269,14 +319,14 @@ impl UiStyles {
     pub fn set_ls(&mut self, pair: &Pair<'_>) -> bool {
         #[rustfmt::skip]
         match pair.key {
-            "di" => self.filekinds.directory    = Some(pair.to_style()),  // DIR
-            "ex" => self.filekinds.executable   = Some(pair.to_style()),  // EXEC
-            "fi" => self.filekinds.normal       = Some(pair.to_style()),  // FILE
-            "pi" => self.filekinds.pipe         = Some(pair.to_style()),  // FIFO
-            "so" => self.filekinds.socket       = Some(pair.to_style()),  // SOCK
-            "bd" => self.filekinds.block_device = Some(pair.to_style()),  // BLK
-            "cd" => self.filekinds.char_device  = Some(pair.to_style()),  // CHR
-            "ln" => self.filekinds.symlink      = Some(pair.to_style()),  // LINK
+            "di" => self.filekinds().directory    = Some(pair.to_style()),  // DIR
+            "ex" => self.filekinds().executable   = Some(pair.to_style()),  // EXEC
+            "fi" => self.filekinds().normal       = Some(pair.to_style()),  // FILE
+            "pi" => self.filekinds().pipe         = Some(pair.to_style()),  // FIFO
+            "so" => self.filekinds().socket       = Some(pair.to_style()),  // SOCK
+            "bd" => self.filekinds().block_device = Some(pair.to_style()),  // BLK
+            "cd" => self.filekinds().char_device  = Some(pair.to_style()),  // CHR
+            "ln" => self.filekinds().symlink      = Some(pair.to_style()),  // LINK
             "or" => self.broken_symlink         = Some(pair.to_style()),  // ORPHAN
              _   => return false,
              // Codes we don’t do anything with:
@@ -293,57 +343,68 @@ impl UiStyles {
     pub fn set_exa(&mut self, pair: &Pair<'_>) -> bool {
         #[rustfmt::skip]
         match pair.key {
-            "ur" => self.perms.user_read                = Some(pair.to_style()),
-            "uw" => self.perms.user_write               = Some(pair.to_style()),
-            "ux" => self.perms.user_execute_file        = Some(pair.to_style()),
-            "ue" => self.perms.user_execute_other       = Some(pair.to_style()),
-            "gr" => self.perms.group_read               = Some(pair.to_style()),
-            "gw" => self.perms.group_write              = Some(pair.to_style()),
-            "gx" => self.perms.group_execute            = Some(pair.to_style()),
-            "tr" => self.perms.other_read               = Some(pair.to_style()),
-            "tw" => self.perms.other_write              = Some(pair.to_style()),
-            "tx" => self.perms.other_execute            = Some(pair.to_style()),
-            "su" => self.perms.special_user_file        = Some(pair.to_style()),
-            "sf" => self.perms.special_other            = Some(pair.to_style()),
-            "xa" => self.perms.attribute                = Some(pair.to_style()),
+            "ur" => self.perms().user_read                = Some(pair.to_style()),
+            "uw" => self.perms().user_write               = Some(pair.to_style()),
+            "ux" => self.perms().user_execute_file        = Some(pair.to_style()),
+            "ue" => self.perms().user_execute_other       = Some(pair.to_style()),
+            "gr" => self.perms().group_read               = Some(pair.to_style()),
+            "gw" => self.perms().group_write              = Some(pair.to_style()),
+            "gx" => self.perms().group_execute            = Some(pair.to_style()),
+            "tr" => self.perms().other_read               = Some(pair.to_style()),
+            "tw" => self.perms().other_write              = Some(pair.to_style()),
+            "tx" => self.perms().other_execute            = Some(pair.to_style()),
+            "su" => self.perms().special_user_file        = Some(pair.to_style()),
+            "sf" => self.perms().special_other            = Some(pair.to_style()),
+            "xa" => self.perms().attribute                = Some(pair.to_style()),
 
             "sn" => self.set_number_style(pair.to_style()),
             "sb" => self.set_unit_style(pair.to_style()),
-            "nb" => self.size.number_byte               = Some(pair.to_style()),
-            "nk" => self.size.number_kilo               = Some(pair.to_style()),
-            "nm" => self.size.number_mega               = Some(pair.to_style()),
-            "ng" => self.size.number_giga               = Some(pair.to_style()),
-            "nt" => self.size.number_huge               = Some(pair.to_style()),
-            "ub" => self.size.unit_byte                 = Some(pair.to_style()),
-            "uk" => self.size.unit_kilo                 = Some(pair.to_style()),
-            "um" => self.size.unit_mega                 = Some(pair.to_style()),
-            "ug" => self.size.unit_giga                 = Some(pair.to_style()),
-            "ut" => self.size.unit_huge                 = Some(pair.to_style()),
-            "df" => self.size.major                     = Some(pair.to_style()),
-            "ds" => self.size.minor                     = Some(pair.to_style()),
-
-            "uu" => self.users.user_you                 = Some(pair.to_style()),
-            "un" => self.users.user_other               = Some(pair.to_style()),
-            "uR" => self.users.user_root                = Some(pair.to_style()),
-            "gu" => self.users.group_yours              = Some(pair.to_style()),
-            "gn" => self.users.group_other              = Some(pair.to_style()),
-            "gR" => self.users.group_root               = Some(pair.to_style()),
-
-            "lc" => self.links.normal                   = Some(pair.to_style()),
-            "lm" => self.links.multi_link_file          = Some(pair.to_style()),
-
-            "ga" => self.git.new                        = Some(pair.to_style()),
-            "gm" => self.git.modified                   = Some(pair.to_style()),
-            "gd" => self.git.deleted                    = Some(pair.to_style()),
-            "gv" => self.git.renamed                    = Some(pair.to_style()),
-            "gt" => self.git.typechange                 = Some(pair.to_style()),
-            "gi" => self.git.ignored                    = Some(pair.to_style()),
-            "gc" => self.git.conflicted                 = Some(pair.to_style()),
-
-            "Gm" => self.git_repo.branch_main           = Some(pair.to_style()),
-            "Go" => self.git_repo.branch_other          = Some(pair.to_style()),
-            "Gc" => self.git_repo.git_clean             = Some(pair.to_style()),
-            "Gd" => self.git_repo.git_dirty             = Some(pair.to_style()),
+            "nb" => self.size().number_byte               = Some(pair.to_style()),
+            "nk" => self.size().number_kilo               = Some(pair.to_style()),
+            "nm" => self.size().number_mega               = Some(pair.to_style()),
+            "ng" => self.size().number_giga               = Some(pair.to_style()),
+            "nt" => self.size().number_huge               = Some(pair.to_style()),
+            "ub" => self.size().unit_byte                 = Some(pair.to_style()),
+            "uk" => self.size().unit_kilo                 = Some(pair.to_style()),
+            "um" => self.size().unit_mega                 = Some(pair.to_style()),
+            "ug" => self.size().unit_giga                 = Some(pair.to_style()),
+            "ut" => self.size().unit_huge                 = Some(pair.to_style()),
+            "df" => self.size().major                     = Some(pair.to_style()),
+            "ds" => self.size().minor                     = Some(pair.to_style()),
+
+            "uu" => self.users().user_you                 = Some(pair.to_style()),
+            "un" => self.users().user_other               = Some(pair.to_style()),
+            "uR" => self.users().user_root                = Some(pair.to_style()),
+            "gu" => self.users().group_yours              = Some(pair.to_style()),
+            "gn" => self.users().group_other              = Some(pair.to_style()),
+            "gR" => self.users().group_root               = Some(pair.to_style()),
+
+            "lc" => self.links().normal                   = Some(pair.to_style()),
+            "lm" => self.links().multi_link_file          = Some(pair.to_style()),
+
+            "ga" => self.git().new                        = Some(pair.to_style()),
+            "gm" => self.git().modified                   = Some(pair.to_style()),
+            "gd" => self.git().deleted                    = Some(pair.to_style()),
+            "gv" => self.git().renamed                    = Some(pair.to_style()),
+            "gt" => self.git().typechange                 = Some(pair.to_style()),
+            "gi" => self.git().ignored                    = Some(pair.to_style()),
+            "gc" => self.git().conflicted                 = Some(pair.to_style()),
+
+            "Gm" => self.git_repo().branch_main           = Some(pair.to_style()),
+            "Go" => self.git_repo().branch_other          = Some(pair.to_style()),
+            "Gc" => self.git_repo().git_clean             = Some(pair.to_style()),
+            "Gd" => self.git_repo().git_dirty             = Some(pair.to_style()),
+                                 
+            "xx" => self.punctuation                     = Some(pair.to_style()),
+            "da" => self.date                            = Some(pair.to_style()),
+            "in" => self.inode                           = Some(pair.to_style()),
+            "bl" => self.blocks                          = Some(pair.to_style()),
+            "hd" => self.header                          = Some(pair.to_style()),
+            "oc" => self.octal                           = Some(pair.to_style()),
+            "ff" => self.flags                           = Some(pair.to_style()),
+            "lp" => self.symlink_path                    = Some(pair.to_style()),
+            "cc" => self.control_char                    = Some(pair.to_style()),
+            "bO" => self.broken_path_overlay             = Some(pair.to_style()),
 
             "ic" => self.icon                           = Some(pair.to_style()),
 
@@ -357,26 +418,26 @@ impl UiStyles {
             "lp" => self.symlink_path                   = Some(pair.to_style()),
             "cc" => self.control_char                   = Some(pair.to_style()),
             "bO" => self.broken_path_overlay            = Some(pair.to_style()),
-            "mp" => self.filekinds.mount_point          = Some(pair.to_style()),
-            "sp" => self.filekinds.special              = Some(pair.to_style()),  // Catch-all for unrecognized file kind
-
-            "im" => self.file_type.image                = Some(pair.to_style()),
-            "vi" => self.file_type.video                = Some(pair.to_style()),
-            "mu" => self.file_type.music                = Some(pair.to_style()),
-            "lo" => self.file_type.lossless             = Some(pair.to_style()),
-            "cr" => self.file_type.crypto               = Some(pair.to_style()),
-            "do" => self.file_type.document             = Some(pair.to_style()),
-            "co" => self.file_type.compressed           = Some(pair.to_style()),
-            "tm" => self.file_type.temp                 = Some(pair.to_style()),
-            "cm" => self.file_type.compiled             = Some(pair.to_style()),
-            "bu" => self.file_type.build                = Some(pair.to_style()),
-            "sc" => self.file_type.source               = Some(pair.to_style()),
-
-            "Sn" => self.security_context.none          = Some(pair.to_style()),
-            "Su" => self.security_context.selinux.user  = Some(pair.to_style()),
-            "Sr" => self.security_context.selinux.role  = Some(pair.to_style()),
-            "St" => self.security_context.selinux.typ   = Some(pair.to_style()),
-            "Sl" => self.security_context.selinux.range = Some(pair.to_style()),
+            "mp" => self.filekinds().mount_point          = Some(pair.to_style()),
+            "sp" => self.filekinds().special              = Some(pair.to_style()),  // Catch-all for unrecognized file kind
+
+            "im" => self.file_type().image                = Some(pair.to_style()),
+            "vi" => self.file_type().video                = Some(pair.to_style()),
+            "mu" => self.file_type().music                = Some(pair.to_style()),
+            "lo" => self.file_type().lossless             = Some(pair.to_style()),
+            "cr" => self.file_type().crypto               = Some(pair.to_style()),
+            "do" => self.file_type().document             = Some(pair.to_style()),
+            "co" => self.file_type().compressed           = Some(pair.to_style()),
+            "tm" => self.file_type().temp                 = Some(pair.to_style()),
+            "cm" => self.file_type().compiled             = Some(pair.to_style()),
+            "bu" => self.file_type().build                = Some(pair.to_style()),
+            "sc" => self.file_type().source               = Some(pair.to_style()),
+
+            "Sn" => self.security_context().none          = Some(pair.to_style()),
+            "Su" => self.security_context().selinux().user  = Some(pair.to_style()),
+            "Sr" => self.security_context().selinux().role  = Some(pair.to_style()),
+            "St" => self.security_context().selinux().typ   = Some(pair.to_style()),
+            "Sl" => self.security_context().selinux().range = Some(pair.to_style()),
 
              _   => return false,
         };
@@ -385,18 +446,18 @@ impl UiStyles {
     }
 
     pub fn set_number_style(&mut self, style: Style) {
-        self.size.number_byte = Some(style);
-        self.size.number_kilo = Some(style);
-        self.size.number_mega = Some(style);
-        self.size.number_giga = Some(style);
-        self.size.number_huge = Some(style);
-    }
+        self.size().number_byte = Some(style);
+        self.size().number_kilo = Some(style);
+        self.size().number_mega = Some(style);
+        self.size().number_giga = Some(style);
+        self.size().number_huge = Some(style);
+    }            
 
     pub fn set_unit_style(&mut self, style: Style) {
-        self.size.unit_byte = Some(style);
-        self.size.unit_kilo = Some(style);
-        self.size.unit_mega = Some(style);
-        self.size.unit_giga = Some(style);
-        self.size.unit_huge = Some(style);
+        self.size().unit_byte = Some(style);
+        self.size().unit_kilo = Some(style);
+        self.size().unit_mega = Some(style);
+        self.size().unit_giga = Some(style);
+        self.size().unit_huge = Some(style);
     }
 }