Просмотр исходного кода

feat: add `--color-scale`

This is based on a series of commits done by aashirabadb in #526.

This introduces the `--color-scale` flag both for file age and size, as
well as `--color-scale-mode` which allows "fixed" and gradient".

The reason for reapplying from the diff of the PR instead of the patch
is that the commit history was messy, and introduced "breaking changes"
that were only locally breaking to the PR, that is, not the users.

Further, the PR had seemingly gone stale, and the rather long and
complicated commit history interwoven with merges made it seem more
efficient to just work from scratch.

Again, the work here is done by aashirbadb, my contribution is just
ensuring the quality of the code they've written.

Co-authored-by: aashirbadb <aashirbadbhandari@gmail.com>
Signed-off-by: Christina Sørensen <christina@cafkafk.com>

Refs: #486
Christina Sørensen 2 лет назад
Родитель
Сommit
288696db10

+ 45 - 0
Cargo.lock

@@ -32,12 +32,19 @@ version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
 checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
 
 
+[[package]]
+name = "ansi_colours"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a1558bd2075d341b9ca698ec8eb6fcc55a746b1fc4255585aad5b141d918a80"
+
 [[package]]
 [[package]]
 name = "ansiterm"
 name = "ansiterm"
 version = "0.12.2"
 version = "0.12.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4ab587f5395da16dd2e6939adf53dede583221b320cadfb94e02b5b7b9bf24cc"
 checksum = "4ab587f5395da16dd2e6939adf53dede583221b320cadfb94e02b5b7b9bf24cc"
 dependencies = [
 dependencies = [
+ "ansi_colours",
  "winapi",
  "winapi",
 ]
 ]
 
 
@@ -89,6 +96,15 @@ dependencies = [
  "windows-sys",
  "windows-sys",
 ]
 ]
 
 
+[[package]]
+name = "approx"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
+dependencies = [
+ "num-traits",
+]
+
 [[package]]
 [[package]]
 name = "autocfg"
 name = "autocfg"
 version = "1.1.0"
 version = "1.1.0"
@@ -370,6 +386,7 @@ dependencies = [
  "num_cpus",
  "num_cpus",
  "number_prefix",
  "number_prefix",
  "once_cell",
  "once_cell",
+ "palette",
  "percent-encoding",
  "percent-encoding",
  "phf",
  "phf",
  "proc-mounts",
  "proc-mounts",
@@ -384,6 +401,12 @@ dependencies = [
  "zoneinfo_compiled",
  "zoneinfo_compiled",
 ]
 ]
 
 
+[[package]]
+name = "fast-srgb8"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
+
 [[package]]
 [[package]]
 name = "fastrand"
 name = "fastrand"
 version = "2.0.0"
 version = "2.0.0"
@@ -709,6 +732,28 @@ dependencies = [
  "windows-sys",
  "windows-sys",
 ]
 ]
 
 
+[[package]]
+name = "palette"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2e2f34147767aa758aa649415b50a69eeb46a67f9dc7db8011eeb3d84b351dc"
+dependencies = [
+ "approx",
+ "fast-srgb8",
+ "palette_derive",
+]
+
+[[package]]
+name = "palette_derive"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7db010ec5ff3d4385e4f133916faacd9dad0f6a09394c92d825b3aed310fa0a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 [[package]]
 name = "partition-identity"
 name = "partition-identity"
 version = "0.3.0"
 version = "0.3.0"

+ 2 - 1
Cargo.toml

@@ -71,7 +71,7 @@ name = "eza"
 
 
 
 
 [dependencies]
 [dependencies]
-ansiterm = "0.12.2"
+ansiterm = { version = "0.12.2", features = ["ansi_colours"] }
 chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
 chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
 glob = "0.3"
 glob = "0.3"
 libc = "0.2"
 libc = "0.2"
@@ -80,6 +80,7 @@ log = "0.4"
 natord = "1.0"
 natord = "1.0"
 num_cpus = "1.16"
 num_cpus = "1.16"
 number_prefix = "0.4"
 number_prefix = "0.4"
+palette = { version = "0.7.3", default-features = false, features = ["std"] }
 once_cell = "1.18.0"
 once_cell = "1.18.0"
 percent-encoding = "2.3.0"
 percent-encoding = "2.3.0"
 phf = { version = "0.11.2", features = ["macros"] }
 phf = { version = "0.11.2", features = ["macros"] }

+ 2 - 1
README.md

@@ -90,7 +90,8 @@ eza’s options are almost, but not quite, entirely unlike `ls`’s.
 - **-x**, **--across**: sort the grid across, rather than downwards
 - **-x**, **--across**: sort the grid across, rather than downwards
 - **-F**, **--classify**: display type indicator by file names
 - **-F**, **--classify**: display type indicator by file names
 - **--colo[u]r=(when)**: when to use terminal colours (always, auto, never)
 - **--colo[u]r=(when)**: when to use terminal colours (always, auto, never)
-- **--colo[u]r-scale**: highlight levels of file sizes distinctly
+- **--colo[u]r-scale=(field)**: highlight levels of `field` distinctly(all, age, size)
+- **--color-scale-mode=(mode)**: use gradient or fixed colors in --color-scale. valid options are `fixed` or `gradient`
 - **--icons=(when)**: when to display icons (always, auto, never)
 - **--icons=(when)**: when to display icons (always, auto, never)
 - **--hyperlink**: display entries as hyperlinks
 - **--hyperlink**: display entries as hyperlinks
 - **-w**, **--width=(columns)**: set screen width in columns
 - **-w**, **--width=(columns)**: set screen width in columns

+ 10 - 0
completions/bash/eza

@@ -37,6 +37,16 @@ _eza() {
             mapfile -t COMPREPLY < <(compgen -W 'default iso long-iso full-iso relative --' -- "$cur")
             mapfile -t COMPREPLY < <(compgen -W 'default iso long-iso full-iso relative --' -- "$cur")
             return
             return
             ;;
             ;;
+
+        --color-scale)
+            mapfile -t COMPREPLY < <(compgen -W 'all age size --' -- "$cur")
+            return
+            ;;
+
+        --color-scale-mode)
+            mapfile -t COMPREPLY < <(compgen -W 'fixed gradient --' -- "$cur")
+            return
+            ;;
     esac
     esac
 
 
     case "$cur" in
     case "$cur" in

+ 10 - 1
completions/fish/eza.fish

@@ -19,7 +19,16 @@ complete -c eza -l color \
     never\t'Never use colour'
     never\t'Never use colour'
 "
 "
 complete -c eza -l color-scale \
 complete -c eza -l color-scale \
-    -l colour-scale -d "Highlight levels of file sizes distinctly"
+    -l colour-scale -d "Highlight levels 'field' distinctly" -x -a "
+    all\t''
+    age\t''
+    size\t''
+"
+complete -c eza -l color-scale-mode \
+    -d "Use gradient or fixed colors in --color-scale" -x -a "
+    fixed\t'Highlight based on fixed colors'
+    gradient\t'Highlight based \'field\' in relation to other files'
+"
 complete -c eza -l icons -d "When to display icons" -x -a "
 complete -c eza -l icons -d "When to display icons" -x -a "
   always\t'Always display icons'
   always\t'Always display icons'
   auto\t'Display icons if standard output is a terminal'
   auto\t'Display icons if standard output is a terminal'

+ 2 - 0
completions/nush/eza.nu

@@ -13,6 +13,8 @@ export extern "eza" [
     --colour                   # When to use terminal colours
     --colour                   # When to use terminal colours
     --color-scale              # Highlight levels of file sizes distinctly
     --color-scale              # Highlight levels of file sizes distinctly
     --colour-scale             # Highlight levels of file sizes distinctly
     --colour-scale             # Highlight levels of file sizes distinctly
+    --color-scale-mode         # Use gradient or fixed colors in --color-scale
+    --colour-scale-mode        # Use gradient or fixed colors in --colour-scale
     --icons                    # When to display icons
     --icons                    # When to display icons
     --no-quotes                # Don't quote file names with spaces
     --no-quotes                # Don't quote file names with spaces
     --hyperlink                # Display entries as hyperlinks
     --hyperlink                # Display entries as hyperlinks

+ 2 - 1
completions/zsh/_eza

@@ -21,7 +21,8 @@ __eza() {
         {-X,--dereference}"[Dereference symbolic links when displaying information]" \
         {-X,--dereference}"[Dereference symbolic links when displaying information]" \
         {-F,--classify}"[Display type indicator by file names]" \
         {-F,--classify}"[Display type indicator by file names]" \
         --colo{,u}r="[When to use terminal colours]:(when):(always auto automatic never)" \
         --colo{,u}r="[When to use terminal colours]:(when):(always auto automatic never)" \
-        --colo{,u}r-scale"[Highlight levels of file sizes distinctly]" \
+        --colo{,u}r-scale"[highlight levels of 'field' distinctly]:(fields):(all age size)" \
+        --colo{,u}r-scale-mode"[Use gradient or fixed colors in --color-scale]:(mode):(fixed gradient)" \
         --icons="[When to display icons]:(when):(always auto automatic never)" \
         --icons="[When to display icons]:(when):(always auto automatic never)" \
         --no-quotes"[Don't quote filenames with spaces]" \
         --no-quotes"[Don't quote filenames with spaces]" \
         --hyperlink"[Display entries as hyperlinks]" \
         --hyperlink"[Display entries as hyperlinks]" \

+ 17 - 2
man/eza.1.md

@@ -86,7 +86,14 @@ The default behavior (‘`automatic`’ or ‘`auto`’) is to colorize the outp
 Manually setting this option overrides `NO_COLOR` environment.
 Manually setting this option overrides `NO_COLOR` environment.
 
 
 `--color-scale`, `--colour-scale`
 `--color-scale`, `--colour-scale`
-: Colour file sizes on a scale.
+: highlight levels of `field` distinctly.
+Use comma(,) separated list of all, age, size
+
+`--color-scale-mode`, `--colour-scale-mode`
+: Use gradient or fixed colors in `--color-scale`.
+
+Valid options are `fixed` or `gradient`.
+The default value is `gradient`.
 
 
 `--icons=WHEN`
 `--icons=WHEN`
 : Display icons next to file names.
 : Display icons next to file names.
@@ -105,10 +112,15 @@ The default value is ‘`automatic`’.
 `-w`, `--width=COLS`
 `-w`, `--width=COLS`
 : Set screen width in columns.
 : Set screen width in columns.
 
 
+Valid options are `none`, `absolute` or `relative`.
+The default value is `none`
+
+`absolute` mode highlights based on file modification time relative to the past year.
+`relative` mode highlights based on file modification time in relation to other files. `none` disables highlighting.
+
 `--smart-group`
 `--smart-group`
 : Only show group if it has a different name from owner
 : Only show group if it has a different name from owner
 
 
-
 FILTERING AND SORTING OPTIONS
 FILTERING AND SORTING OPTIONS
 =============================
 =============================
 
 
@@ -302,6 +314,9 @@ For more information on the format of these environment variables, see the [eza_
 
 
 Overrides any `--git` or `--git-repos` argument
 Overrides any `--git` or `--git-repos` argument
 
 
+## `EZA_MIN_LUMINANCE`
+Specifies the minimum luminance to use when decay is active. It's value can be between -100 to 100.
+
 ## `EZA_ICONS_AUTO`
 ## `EZA_ICONS_AUTO`
 
 
 If set, automates the same behavior as using `--icons` or `--icons=auto`. Useful for if you always want to have icons enabled.
 If set, automates the same behavior as using `--icons` or `--icons=auto`. Useful for if you always want to have icons enabled.

+ 11 - 6
src/options/flags.rs

@@ -16,12 +16,16 @@ pub static DEREF_LINKS: Arg = Arg { short: Some(b'X'), long: "dereference", take
 pub static WIDTH:       Arg = Arg { short: Some(b'w'), long: "width",       takes_value: TakesValue::Necessary(None) };
 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 };
 pub static NO_QUOTES:   Arg = Arg { short: None,       long: "no-quotes",   takes_value: TakesValue::Forbidden };
 
 
-pub static COLOR:  Arg = Arg { short: None, long: "color",  takes_value: TakesValue::Optional(Some(WHEN)) };
-pub static COLOUR: Arg = Arg { short: None, long: "colour", takes_value: TakesValue::Optional(Some(WHEN)) };
+pub static COLOR:  Arg = Arg { short: None, long: "color",  takes_value: TakesValue::Optional(Some(WHEN), "auto") };
+pub static COLOUR: Arg = Arg { short: None, long: "colour", takes_value: TakesValue::Optional(Some(WHEN), "auto") };
 const WHEN: &[&str] = &["always", "auto", "never"];
 const WHEN: &[&str] = &["always", "auto", "never"];
 
 
-pub static COLOR_SCALE:  Arg = Arg { short: None, long: "color-scale",  takes_value: TakesValue::Forbidden };
-pub static COLOUR_SCALE: Arg = Arg { short: None, long: "colour-scale", takes_value: TakesValue::Forbidden };
+pub static COLOR_SCALE:  Arg = Arg { short: None, long: "color-scale",  takes_value: TakesValue::Necessary(Some(SCALES)) };
+pub static COLOUR_SCALE: Arg = Arg { short: None, long: "colour-scale", takes_value: TakesValue::Necessary(Some(SCALES)) };
+pub static COLOR_SCALE_MODE:  Arg = Arg { short: None, long: "color-scale-mode",  takes_value: TakesValue::Necessary(Some(COLOR_SCALE_MODES))};
+pub static COLOUR_SCALE_MODE: Arg = Arg { short: None, long: "colour-scale-mode", takes_value: TakesValue::Necessary(Some(COLOR_SCALE_MODES))};
+const SCALES: Values = &["all", "size", "age"];
+const COLOR_SCALE_MODES: Values = &["fixed", "gradient"];
 
 
 // filtering and sorting options
 // filtering and sorting options
 pub static ALL:         Arg = Arg { short: Some(b'a'), long: "all",         takes_value: TakesValue::Forbidden };
 pub static ALL:         Arg = Arg { short: Some(b'a'), long: "all",         takes_value: TakesValue::Forbidden };
@@ -45,7 +49,7 @@ pub static BYTES:       Arg = Arg { short: Some(b'B'), long: "bytes",       take
 pub static GROUP:       Arg = Arg { short: Some(b'g'), long: "group",       takes_value: TakesValue::Forbidden };
 pub static GROUP:       Arg = Arg { short: Some(b'g'), long: "group",       takes_value: TakesValue::Forbidden };
 pub static NUMERIC:     Arg = Arg { short: Some(b'n'), long: "numeric",     takes_value: TakesValue::Forbidden };
 pub static NUMERIC:     Arg = Arg { short: Some(b'n'), long: "numeric",     takes_value: TakesValue::Forbidden };
 pub static HEADER:      Arg = Arg { short: Some(b'h'), long: "header",      takes_value: TakesValue::Forbidden };
 pub static HEADER:      Arg = Arg { short: Some(b'h'), long: "header",      takes_value: TakesValue::Forbidden };
-pub static ICONS:       Arg = Arg { short: None,       long: "icons",       takes_value: TakesValue::Optional(Some(WHEN))};
+pub static ICONS:       Arg = Arg { short: None,       long: "icons",       takes_value: TakesValue::Optional(Some(WHEN), "auto")};
 pub static INODE:       Arg = Arg { short: Some(b'i'), long: "inode",       takes_value: TakesValue::Forbidden };
 pub static INODE:       Arg = Arg { short: Some(b'i'), long: "inode",       takes_value: TakesValue::Forbidden };
 pub static LINKS:       Arg = Arg { short: Some(b'H'), long: "links",       takes_value: TakesValue::Forbidden };
 pub static LINKS:       Arg = Arg { short: Some(b'H'), long: "links",       takes_value: TakesValue::Forbidden };
 pub static MODIFIED:    Arg = Arg { short: Some(b'm'), long: "modified",    takes_value: TakesValue::Forbidden };
 pub static MODIFIED:    Arg = Arg { short: Some(b'm'), long: "modified",    takes_value: TakesValue::Forbidden };
@@ -81,7 +85,8 @@ pub static ALL_ARGS: Args = Args(&[
     &VERSION, &HELP,
     &VERSION, &HELP,
 
 
     &ONE_LINE, &LONG, &GRID, &ACROSS, &RECURSE, &TREE, &CLASSIFY, &DEREF_LINKS,
     &ONE_LINE, &LONG, &GRID, &ACROSS, &RECURSE, &TREE, &CLASSIFY, &DEREF_LINKS,
-    &COLOR, &COLOUR, &COLOR_SCALE, &COLOUR_SCALE, &WIDTH, &NO_QUOTES,
+    &COLOR, &COLOUR, &COLOR_SCALE, &COLOUR_SCALE, &COLOR_SCALE_MODE, &COLOUR_SCALE_MODE,
+    &WIDTH, &NO_QUOTES,
 
 
     &ALL, &ALMOST_ALL, &LIST_DIRS, &LEVEL, &REVERSE, &SORT, &DIRS_FIRST,
     &ALL, &ALMOST_ALL, &LIST_DIRS, &LEVEL, &REVERSE, &SORT, &DIRS_FIRST,
     &IGNORE_GLOB, &GIT_IGNORE, &ONLY_DIRS, &ONLY_FILES,
     &IGNORE_GLOB, &GIT_IGNORE, &ONLY_DIRS, &ONLY_FILES,

+ 16 - 15
src/options/help.rs

@@ -12,21 +12,22 @@ META OPTIONS
   -v, --version      show version of eza
   -v, --version      show version of eza
 
 
 DISPLAY OPTIONS
 DISPLAY OPTIONS
-  -1, --oneline      display one entry per line
-  -l, --long         display extended file metadata as a table
-  -G, --grid         display entries as a grid (default)
-  -x, --across       sort the grid across, rather than downwards
-  -R, --recurse      recurse into directories
-  -T, --tree         recurse into directories as a tree
-  -X, --dereference  dereference symbolic links when displaying information
-  -F, --classify     display type indicator by file names
-  --colo[u]r=WHEN    when to use terminal colours (always, auto, never)
-  --colo[u]r-scale   highlight levels of file sizes distinctly
-  --icons=WHEN       when to display icons (always, auto, never)
-  --no-quotes        don't quote file names with spaces
-  --hyperlink        display entries as hyperlinks
-  -w, --width COLS   set screen width in columns
-  --smart-group      only show group if it has a different name from owner
+  -1, --oneline            display one entry per line
+  -l, --long               display extended file metadata as a table
+  -G, --grid               display entries as a grid (default)
+  -x, --across             sort the grid across, rather than downwards
+  -R, --recurse            recurse into directories
+  -T, --tree               recurse into directories as a tree
+  -X, --dereference        dereference symbolic links when displaying information
+  -F, --classify           display type indicator by file names
+  --colo[u]r=WHEN          when to use terminal colours (always, auto, never)
+  --colo[u]r-scale         highlight levels of 'field' distinctly(all, age, size)
+  --colo[u]r-scale-mode    use gradient or fixed colors in --color-scale (fixed, gradient)
+  --icons=WHEN             when to display icons (always, auto, never)
+  --no-quotes              don't quote file names with spaces
+  --hyperlink              display entries as hyperlinks
+  -w, --width COLS         set screen width in columns
+  --smart-group            only show group if it has a different name from owner
 
 
 
 
 FILTERING AND SORTING OPTIONS
 FILTERING AND SORTING OPTIONS

+ 57 - 17
src/options/parser.rs

@@ -99,7 +99,7 @@ pub enum TakesValue {
     Forbidden,
     Forbidden,
 
 
     /// This flag may be followed by a value to override its defaults
     /// This flag may be followed by a value to override its defaults
-    Optional(Option<Values>),
+    Optional(Option<Values>, &'static str),
 }
 }
 
 
 /// An **argument** can be matched by one of the user’s input strings.
 /// An **argument** can be matched by one of the user’s input strings.
@@ -176,7 +176,7 @@ impl Args {
                     let arg = self.lookup_long(before)?;
                     let arg = self.lookup_long(before)?;
                     let flag = Flag::Long(arg.long);
                     let flag = Flag::Long(arg.long);
                     match arg.takes_value {
                     match arg.takes_value {
-                        TakesValue::Necessary(_) | TakesValue::Optional(_) => {
+                        TakesValue::Necessary(_) | TakesValue::Optional(_, _) => {
                             result_flags.push((flag, Some(after)));
                             result_flags.push((flag, Some(after)));
                         }
                         }
                         TakesValue::Forbidden => return Err(ParseError::ForbiddenValue { flag }),
                         TakesValue::Forbidden => return Err(ParseError::ForbiddenValue { flag }),
@@ -198,11 +198,14 @@ impl Args {
                                 return Err(ParseError::NeedsValue { flag, values });
                                 return Err(ParseError::NeedsValue { flag, values });
                             }
                             }
                         }
                         }
-                        TakesValue::Optional(_) => match inputs.peek() {
-                            Some(next_arg) if is_optional_arg(next_arg) => {
+                        TakesValue::Optional(values, default) => match inputs.peek() {
+                            Some(next_arg) if is_optional_arg(next_arg, values) => {
                                 result_flags.push((flag, Some(inputs.next().unwrap())));
                                 result_flags.push((flag, Some(inputs.next().unwrap())));
                             }
                             }
-                            _ => result_flags.push((flag, None)),
+                            _ => {
+                                result_flags
+                                    .push((flag, Some(bytes_to_os_str(default.as_bytes()))));
+                            }
                         },
                         },
                     }
                     }
                 }
                 }
@@ -233,9 +236,13 @@ impl Args {
                         let arg = self.lookup_short(*byte)?;
                         let arg = self.lookup_short(*byte)?;
                         let flag = Flag::Short(*byte);
                         let flag = Flag::Short(*byte);
                         match arg.takes_value {
                         match arg.takes_value {
-                            TakesValue::Forbidden | TakesValue::Optional(_) => {
+                            TakesValue::Forbidden => {
                                 result_flags.push((flag, None));
                                 result_flags.push((flag, None));
                             }
                             }
+                            TakesValue::Optional(_, default) => {
+                                result_flags
+                                    .push((flag, Some(bytes_to_os_str(default.as_bytes()))));
+                            }
                             TakesValue::Necessary(values) => {
                             TakesValue::Necessary(values) => {
                                 return Err(ParseError::NeedsValue { flag, values });
                                 return Err(ParseError::NeedsValue { flag, values });
                             }
                             }
@@ -246,7 +253,7 @@ impl Args {
                     let arg = self.lookup_short(*arg_with_value)?;
                     let arg = self.lookup_short(*arg_with_value)?;
                     let flag = Flag::Short(arg.short.unwrap());
                     let flag = Flag::Short(arg.short.unwrap());
                     match arg.takes_value {
                     match arg.takes_value {
-                        TakesValue::Necessary(_) | TakesValue::Optional(_) => {
+                        TakesValue::Necessary(_) | TakesValue::Optional(_, _) => {
                             result_flags.push((flag, Some(after)));
                             result_flags.push((flag, Some(after)));
                         }
                         }
                         TakesValue::Forbidden => {
                         TakesValue::Forbidden => {
@@ -274,7 +281,7 @@ impl Args {
                             TakesValue::Forbidden => {
                             TakesValue::Forbidden => {
                                 result_flags.push((flag, None));
                                 result_flags.push((flag, None));
                             }
                             }
-                            TakesValue::Necessary(values) | TakesValue::Optional(values) => {
+                            TakesValue::Necessary(values) => {
                                 if index < bytes.len() - 1 {
                                 if index < bytes.len() - 1 {
                                     let remnants = &bytes[index + 1..];
                                     let remnants = &bytes[index + 1..];
                                     result_flags.push((flag, Some(bytes_to_os_str(remnants))));
                                     result_flags.push((flag, Some(bytes_to_os_str(remnants))));
@@ -283,14 +290,37 @@ impl Args {
                                     result_flags.push((flag, Some(next_arg)));
                                     result_flags.push((flag, Some(next_arg)));
                                 } else {
                                 } else {
                                     match arg.takes_value {
                                     match arg.takes_value {
-                                        TakesValue::Forbidden => {
+                                        TakesValue::Forbidden | TakesValue::Optional(_, _) => {
                                             unreachable!()
                                             unreachable!()
                                         }
                                         }
                                         TakesValue::Necessary(_) => {
                                         TakesValue::Necessary(_) => {
                                             return Err(ParseError::NeedsValue { flag, values });
                                             return Err(ParseError::NeedsValue { flag, values });
                                         }
                                         }
-                                        TakesValue::Optional(_) => {
-                                            result_flags.push((flag, None));
+                                    }
+                                }
+                            }
+                            TakesValue::Optional(values, default) => {
+                                if index < bytes.len() - 1 {
+                                    let remnants = bytes_to_os_str(&bytes[index + 1..]);
+                                    if is_optional_arg(remnants, values) {
+                                        result_flags.push((flag, Some(remnants)));
+                                    } else {
+                                        return Err(ParseError::ForbiddenValue { flag });
+                                    }
+                                    break;
+                                } else if let Some(next_arg) = inputs.peek() {
+                                    if is_optional_arg(next_arg, values) {
+                                        result_flags.push((flag, Some(inputs.next().unwrap())));
+                                    } else {
+                                        result_flags.push((flag, Some(OsStr::new(default))));
+                                    }
+                                } else {
+                                    match arg.takes_value {
+                                        TakesValue::Forbidden | TakesValue::Necessary(_) => {
+                                            unreachable!()
+                                        }
+                                        TakesValue::Optional(_, default) => {
+                                            result_flags.push((flag, Some(OsStr::new(default))));
                                         }
                                         }
                                     }
                                     }
                                 }
                                 }
@@ -331,11 +361,9 @@ impl Args {
     }
     }
 }
 }
 
 
-fn is_optional_arg(arg: &OsStr) -> bool {
-    let bytes = os_str_to_bytes(arg);
-    match bytes {
-        // The only optional arguments allowed
-        b"always" | b"auto" | b"automatic" | b"never" => true,
+fn is_optional_arg(value: &OsStr, values: Option<&[&str]>) -> bool {
+    match (values, value.to_str()) {
+        (Some(values), Some(value)) => values.contains(&value),
         _ => false,
         _ => false,
     }
     }
 }
 }
@@ -639,7 +667,8 @@ mod parse_test {
         &Arg { short: Some(b'l'), long: "long",     takes_value: TakesValue::Forbidden },
         &Arg { short: Some(b'l'), long: "long",     takes_value: TakesValue::Forbidden },
         &Arg { short: Some(b'v'), long: "verbose",  takes_value: TakesValue::Forbidden },
         &Arg { short: Some(b'v'), long: "verbose",  takes_value: TakesValue::Forbidden },
         &Arg { short: Some(b'c'), long: "count",    takes_value: TakesValue::Necessary(None) },
         &Arg { short: Some(b'c'), long: "count",    takes_value: TakesValue::Necessary(None) },
-        &Arg { short: Some(b't'), long: "type",     takes_value: TakesValue::Necessary(Some(SUGGESTIONS)) }
+        &Arg { short: Some(b't'), long: "type",     takes_value: TakesValue::Necessary(Some(SUGGESTIONS))},
+        &Arg { short: Some(b'o'), long: "optional", takes_value: TakesValue::Optional(Some(&["all", "some", "none"]), "all")} 
     ];
     ];
 
 
     // Just filenames
     // Just filenames
@@ -697,6 +726,17 @@ mod parse_test {
     test!(unknown_short_2nd:     ["-lq"]          => error UnknownShortArgument { attempt: b'q' });
     test!(unknown_short_2nd:     ["-lq"]          => error UnknownShortArgument { attempt: b'q' });
     test!(unknown_short_eq:      ["-q=shhh"]      => error UnknownShortArgument { attempt: b'q' });
     test!(unknown_short_eq:      ["-q=shhh"]      => error UnknownShortArgument { attempt: b'q' });
     test!(unknown_short_2nd_eq:  ["-lq=shhh"]     => error UnknownShortArgument { attempt: b'q' });
     test!(unknown_short_2nd_eq:  ["-lq=shhh"]     => error UnknownShortArgument { attempt: b'q' });
+
+    // Optional args
+    test!(optional:         ["--optional"]         => frees: [], flags: [(Flag::Long("optional"), Some(OsStr::new("all")))]);
+    test!(optional_2:       ["--optional", "-l"]   => frees: [], flags: [ (Flag::Long("optional"), Some(OsStr::new("all"))), (Flag::Short(b'l'), None)]);
+    test!(optional_3:       ["--optional", "path"] => frees: ["path"], flags: [(Flag::Long("optional"), Some(OsStr::new("all")))]);
+    test!(optional_with_eq: ["--optional=none"]    => frees: [], flags: [(Flag::Long("optional"), Some(OsStr::new("none")))]);
+    test!(optional_wo_eq:   ["--optional", "none"] => frees: [], flags: [(Flag::Long("optional"), Some(OsStr::new("none")))]);
+    test!(short_opt:        ["-o"]                 => frees: [], flags: [(Flag::Short(b'o'), Some(OsStr::new("all")))]);
+    test!(short_opt_value:  ["-onone"]             => frees: [], flags: [(Flag::Short(b'o'), Some(OsStr::new("none")))]);
+    test!(short_forbidden:  ["-opath"]             => error ForbiddenValue  { flag: Flag::Short(b'o') });
+    test!(short_allowed:    ["-o","path"]          => frees: ["path"], flags: [(Flag::Short(b'o'), Some(OsStr::new("all")))]);
 }
 }
 
 
 #[cfg(test)]
 #[cfg(test)]

+ 11 - 23
src/options/theme.rs

@@ -1,11 +1,12 @@
 use crate::options::parser::MatchedFlags;
 use crate::options::parser::MatchedFlags;
 use crate::options::{flags, vars, OptionsError, Vars};
 use crate::options::{flags, vars, OptionsError, Vars};
-use crate::theme::{ColourScale, Definitions, Options, UseColours};
+use crate::output::decay::ColorScaleOptions;
+use crate::theme::{Definitions, Options, UseColours};
 
 
 impl Options {
 impl Options {
     pub fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
     pub fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
         let use_colours = UseColours::deduce(matches, vars)?;
         let use_colours = UseColours::deduce(matches, vars)?;
-        let colour_scale = ColourScale::deduce(matches)?;
+        let colour_scale = ColorScaleOptions::deduce(matches, vars)?;
 
 
         let definitions = if use_colours == UseColours::Never {
         let definitions = if use_colours == UseColours::Never {
             Definitions::default()
             Definitions::default()
@@ -46,19 +47,6 @@ impl UseColours {
     }
     }
 }
 }
 
 
-impl ColourScale {
-    fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
-        if matches
-            .has_where(|f| f.matches(&flags::COLOR_SCALE) || f.matches(&flags::COLOUR_SCALE))?
-            .is_some()
-        {
-            Ok(Self::Gradient)
-        } else {
-            Ok(Self::Fixed)
-        }
-    }
-}
-
 impl Definitions {
 impl Definitions {
     fn deduce<V: Vars>(vars: &V) -> Self {
     fn deduce<V: Vars>(vars: &V) -> Self {
         let ls = vars
         let ls = vars
@@ -204,13 +192,13 @@ mod terminal_test {
     test!(overridden_7:  UseColours <- ["--colour=auto", "--color=never"], MockVars::empty();   Complain => err OptionsError::Duplicate(Flag::Long("colour"), Flag::Long("color")));
     test!(overridden_7:  UseColours <- ["--colour=auto", "--color=never"], MockVars::empty();   Complain => err OptionsError::Duplicate(Flag::Long("colour"), Flag::Long("color")));
     test!(overridden_8:  UseColours <- ["--color=auto",  "--color=never"], MockVars::empty();   Complain => err OptionsError::Duplicate(Flag::Long("color"),  Flag::Long("color")));
     test!(overridden_8:  UseColours <- ["--color=auto",  "--color=never"], MockVars::empty();   Complain => err OptionsError::Duplicate(Flag::Long("color"),  Flag::Long("color")));
 
 
-    test!(scale_1:  ColourScale <- ["--color-scale", "--colour-scale"];   Last => Ok(ColourScale::Gradient));
-    test!(scale_2:  ColourScale <- ["--color-scale",                 ];   Last => Ok(ColourScale::Gradient));
-    test!(scale_3:  ColourScale <- [                 "--colour-scale"];   Last => Ok(ColourScale::Gradient));
-    test!(scale_4:  ColourScale <- [                                 ];   Last => Ok(ColourScale::Fixed));
+    // test!(scale_1:  ColourScale <- ["--color-scale", "--colour-scale"];   Last => Ok(ColourScale::Gradient));
+    // test!(scale_2:  ColourScale <- ["--color-scale",                 ];   Last => Ok(ColourScale::Gradient));
+    // test!(scale_3:  ColourScale <- [                 "--colour-scale"];   Last => Ok(ColourScale::Gradient));
+    // test!(scale_4:  ColourScale <- [                                 ];   Last => Ok(ColourScale::Fixed));
 
 
-    test!(scale_5:  ColourScale <- ["--color-scale", "--colour-scale"];   Complain => err OptionsError::Duplicate(Flag::Long("color-scale"),  Flag::Long("colour-scale")));
-    test!(scale_6:  ColourScale <- ["--color-scale",                 ];   Complain => Ok(ColourScale::Gradient));
-    test!(scale_7:  ColourScale <- [                 "--colour-scale"];   Complain => Ok(ColourScale::Gradient));
-    test!(scale_8:  ColourScale <- [                                 ];   Complain => Ok(ColourScale::Fixed));
+    // test!(scale_5:  ColourScale <- ["--color-scale", "--colour-scale"];   Complain => err OptionsError::Duplicate(Flag::Long("color-scale"),  Flag::Long("colour-scale")));
+    // test!(scale_6:  ColourScale <- ["--color-scale",                 ];   Complain => Ok(ColourScale::Gradient));
+    // test!(scale_7:  ColourScale <- [                 "--colour-scale"];   Complain => Ok(ColourScale::Gradient));
+    // test!(scale_8:  ColourScale <- [                                 ];   Complain => Ok(ColourScale::Fixed));
 }
 }

+ 5 - 0
src/options/vars.rs

@@ -55,6 +55,11 @@ pub static EZA_ICON_SPACING: &str = "EZA_ICON_SPACING";
 pub static EXA_OVERRIDE_GIT: &str = "EXA_OVERRIDE_GIT";
 pub static EXA_OVERRIDE_GIT: &str = "EXA_OVERRIDE_GIT";
 pub static EZA_OVERRIDE_GIT: &str = "EZA_OVERRIDE_GIT";
 pub static EZA_OVERRIDE_GIT: &str = "EZA_OVERRIDE_GIT";
 
 
+/// Enviroment variable used to set the minimum luminance in decay. It's value
+/// can be between -100 and 100
+pub static EXA_MIN_LUMINANCE: &str = "EXA_MIN_LUMINANCE";
+pub static EZA_MIN_LUMINANCE: &str = "EZA_MIN_LUMINANCE";
+
 /// Environment variable used to automate the same behavior as `--icons=auto` if set.
 /// Environment variable used to automate the same behavior as `--icons=auto` if set.
 /// Any explicit use of `--icons=WHEN` overrides this behavior.
 /// Any explicit use of `--icons=WHEN` overrides this behavior.
 pub static EZA_ICONS_AUTO: &str = "EZA_ICONS_AUTO";
 pub static EZA_ICONS_AUTO: &str = "EZA_ICONS_AUTO";

+ 70 - 9
src/options/view.rs

@@ -1,6 +1,9 @@
+use std::ffi::OsString;
+
 use crate::fs::feature::xattr;
 use crate::fs::feature::xattr;
 use crate::options::parser::MatchedFlags;
 use crate::options::parser::MatchedFlags;
-use crate::options::{flags, NumberSource, OptionsError, Vars};
+use crate::options::{flags, vars, NumberSource, OptionsError, Vars};
+use crate::output::decay::{ColorScaleMode, ColorScaleOptions};
 use crate::output::file_name::Options as FileStyle;
 use crate::output::file_name::Options as FileStyle;
 use crate::output::grid_details::{self, RowThreshold};
 use crate::output::grid_details::{self, RowThreshold};
 use crate::output::table::{
 use crate::output::table::{
@@ -79,7 +82,7 @@ impl Mode {
 
 
         if flag.matches(&flags::TREE) {
         if flag.matches(&flags::TREE) {
             let _ = matches.has(&flags::TREE)?;
             let _ = matches.has(&flags::TREE)?;
-            let details = details::Options::deduce_tree(matches)?;
+            let details = details::Options::deduce_tree(matches, vars)?;
             return Ok(Self::Details(details));
             return Ok(Self::Details(details));
         }
         }
 
 
@@ -142,13 +145,14 @@ impl grid::Options {
 }
 }
 
 
 impl details::Options {
 impl details::Options {
-    fn deduce_tree(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
+    fn deduce_tree<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
         let details = details::Options {
         let details = details::Options {
             table: None,
             table: None,
             header: false,
             header: false,
             xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
             xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
             secattr: xattr::ENABLED && matches.has(&flags::SECURITY_CONTEXT)?,
             secattr: xattr::ENABLED && matches.has(&flags::SECURITY_CONTEXT)?,
             mounts: matches.has(&flags::MOUNTS)?,
             mounts: matches.has(&flags::MOUNTS)?,
+            color_scale: ColorScaleOptions::deduce(matches, vars)?,
         };
         };
 
 
         Ok(details)
         Ok(details)
@@ -169,14 +173,13 @@ impl details::Options {
             xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
             xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
             secattr: xattr::ENABLED && matches.has(&flags::SECURITY_CONTEXT)?,
             secattr: xattr::ENABLED && matches.has(&flags::SECURITY_CONTEXT)?,
             mounts: matches.has(&flags::MOUNTS)?,
             mounts: matches.has(&flags::MOUNTS)?,
+            color_scale: ColorScaleOptions::deduce(matches, vars)?,
         })
         })
     }
     }
 }
 }
 
 
 impl TerminalWidth {
 impl TerminalWidth {
     fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
     fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
-        use crate::options::vars;
-
         if let Some(width) = matches.get(&flags::WIDTH)? {
         if let Some(width) = matches.get(&flags::WIDTH)? {
             let arg_str = width.to_string_lossy();
             let arg_str = width.to_string_lossy();
             match arg_str.parse() {
             match arg_str.parse() {
@@ -208,8 +211,6 @@ impl TerminalWidth {
 
 
 impl RowThreshold {
 impl RowThreshold {
     fn deduce<V: Vars>(vars: &V) -> Result<Self, OptionsError> {
     fn deduce<V: Vars>(vars: &V) -> Result<Self, OptionsError> {
-        use crate::options::vars;
-
         if let Some(columns) = vars
         if let Some(columns) = vars
             .get_with_fallback(vars::EZA_GRID_ROWS, vars::EXA_GRID_ROWS)
             .get_with_fallback(vars::EZA_GRID_ROWS, vars::EXA_GRID_ROWS)
             .and_then(|s| s.into_string().ok())
             .and_then(|s| s.into_string().ok())
@@ -249,7 +250,6 @@ impl TableOptions {
 
 
 impl Columns {
 impl Columns {
     fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
     fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
-        use crate::options::vars;
         let time_types = TimeTypes::deduce(matches)?;
         let time_types = TimeTypes::deduce(matches)?;
 
 
         let no_git_env = vars
         let no_git_env = vars
@@ -319,7 +319,6 @@ impl TimeFormat {
         let word = if let Some(w) = matches.get(&flags::TIME_STYLE)? {
         let word = if let Some(w) = matches.get(&flags::TIME_STYLE)? {
             w.to_os_string()
             w.to_os_string()
         } else {
         } else {
-            use crate::options::vars;
             match vars.get(vars::TIME_STYLE) {
             match vars.get(vars::TIME_STYLE) {
                 Some(ref t) if !t.is_empty() => t.clone(),
                 Some(ref t) if !t.is_empty() => t.clone(),
                 _ => return Ok(Self::DefaultFormat),
                 _ => return Ok(Self::DefaultFormat),
@@ -417,6 +416,68 @@ impl TimeTypes {
     }
     }
 }
 }
 
 
+impl ColorScaleOptions {
+    pub fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
+        let min_luminance =
+            match vars.get_with_fallback(vars::EZA_MIN_LUMINANCE, vars::EXA_MIN_LUMINANCE) {
+                Some(var) => match var.to_string_lossy().parse() {
+                    Ok(luminance) if (-100..=100).contains(&luminance) => luminance,
+                    _ => 40,
+                },
+                None => 40,
+            };
+
+        let mode = if let Some(w) = matches
+            .get(&flags::COLOR_SCALE_MODE)?
+            .or(matches.get(&flags::COLOUR_SCALE_MODE)?)
+        {
+            match w.to_str() {
+                Some("fixed") => ColorScaleMode::Fixed,
+                Some("gradient") => ColorScaleMode::Gradient,
+                _ => Err(OptionsError::BadArgument(
+                    &flags::COLOR_SCALE_MODE,
+                    w.to_os_string(),
+                ))?,
+            }
+        } else {
+            ColorScaleMode::Gradient
+        };
+
+        let mut options = ColorScaleOptions {
+            mode,
+            min_luminance,
+            size: false,
+            age: false,
+        };
+
+        let words = if let Some(w) = matches
+            .get(&flags::COLOR_SCALE)?
+            .or(matches.get(&flags::COLOUR_SCALE)?)
+        {
+            w.to_os_string()
+        } else {
+            return Ok(options);
+        };
+
+        for word in words.to_string_lossy().split(',') {
+            match word {
+                "all" => {
+                    options.size = true;
+                    options.age = true;
+                }
+                "age" => options.age = true,
+                "size" => options.size = true,
+                _ => Err(OptionsError::BadArgument(
+                    &flags::COLOR_SCALE,
+                    OsString::from(word),
+                ))?,
+            };
+        }
+
+        Ok(options)
+    }
+}
+
 #[cfg(test)]
 #[cfg(test)]
 mod test {
 mod test {
     use super::*;
     use super::*;

+ 206 - 0
src/output/decay.rs

@@ -0,0 +1,206 @@
+use ansiterm::{Colour, Style};
+use palette::{FromColor, Oklab, Srgb};
+
+use crate::{
+    fs::{dir_action::RecurseOptions, feature::git::GitCache, fields::Size, DotFilter, File},
+    output::{table::TimeType, tree::TreeDepth},
+};
+
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
+pub struct ColorScaleOptions {
+    pub mode: ColorScaleMode,
+    pub min_luminance: isize,
+
+    pub size: bool,
+    pub age: bool,
+}
+
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
+pub enum ColorScaleMode {
+    Fixed,
+    Gradient,
+}
+
+#[derive(Copy, Clone, Debug)]
+pub struct ColorScaleInformation {
+    pub options: ColorScaleOptions,
+
+    pub accessed: Option<Extremes>,
+    pub changed: Option<Extremes>,
+    pub created: Option<Extremes>,
+    pub modified: Option<Extremes>,
+
+    pub size: Option<Extremes>,
+}
+
+impl ColorScaleInformation {
+    pub fn from_color_scale(
+        color_scale: ColorScaleOptions,
+        files: &[File<'_>],
+        dot_filter: DotFilter,
+        git: Option<&GitCache>,
+        git_ignoring: bool,
+        r: Option<RecurseOptions>,
+    ) -> Option<Self> {
+        if color_scale.mode == ColorScaleMode::Fixed {
+            None
+        } else {
+            let mut information = Self {
+                options: color_scale,
+                accessed: None,
+                changed: None,
+                created: None,
+                modified: None,
+                size: None,
+            };
+
+            update_information_recursively(
+                &mut information,
+                files,
+                dot_filter,
+                git,
+                git_ignoring,
+                TreeDepth::root(),
+                r,
+            );
+
+            Some(information)
+        }
+    }
+
+    pub fn adjust_style(&self, mut style: Style, value: f32, range: Option<Extremes>) -> Style {
+        if let (Some(fg), Some(range)) = (style.foreground, range) {
+            let mut ratio = ((value - range.min) / (range.max - range.min)).clamp(0.0, 1.0);
+            if ratio.is_nan() {
+                ratio = 1.0;
+            }
+
+            style.foreground = Some(adjust_luminance(
+                fg,
+                ratio,
+                self.options.min_luminance as f32 / 100.0,
+            ));
+        }
+
+        style
+    }
+
+    pub fn apply_time_gradient(&self, style: Style, file: &File<'_>, time_type: TimeType) -> Style {
+        let range = match time_type {
+            TimeType::Modified => self.modified,
+            TimeType::Changed => self.changed,
+            TimeType::Accessed => self.accessed,
+            TimeType::Created => self.created,
+        };
+
+        if let Some(file_time) = time_type.get_corresponding_time(file) {
+            self.adjust_style(style, file_time.timestamp_millis() as f32, range)
+        } else {
+            style
+        }
+    }
+}
+
+fn update_information_recursively(
+    information: &mut ColorScaleInformation,
+    files: &[File<'_>],
+    dot_filter: DotFilter,
+    git: Option<&GitCache>,
+    git_ignoring: bool,
+    depth: TreeDepth,
+    r: Option<RecurseOptions>,
+) {
+    for file in files {
+        if information.options.age {
+            Extremes::update(
+                file.created_time().map(|x| x.timestamp_millis() as f32),
+                &mut information.created,
+            );
+            Extremes::update(
+                file.modified_time().map(|x| x.timestamp_millis() as f32),
+                &mut information.modified,
+            );
+            Extremes::update(
+                file.accessed_time().map(|x| x.timestamp_millis() as f32),
+                &mut information.accessed,
+            );
+            Extremes::update(
+                file.changed_time().map(|x| x.timestamp_millis() as f32),
+                &mut information.changed,
+            );
+        }
+
+        if information.options.size {
+            let size = match file.size() {
+                Size::Some(size) => Some(size as f32),
+                _ => None,
+            };
+            Extremes::update(size, &mut information.size);
+        }
+
+        if file.is_directory() && r.is_some_and(|x| !x.is_too_deep(depth.0)) {
+            match file.to_dir() {
+                Ok(dir) => {
+                    let files: Vec<File<'_>> = dir
+                        .files(dot_filter, git, git_ignoring, false, false)
+                        .flatten()
+                        .collect();
+
+                    update_information_recursively(
+                        information,
+                        &files,
+                        dot_filter,
+                        git,
+                        git_ignoring,
+                        depth.deeper(),
+                        r,
+                    );
+                }
+                Err(_) => todo!(),
+            }
+        };
+    }
+}
+
+#[derive(Copy, Clone, Debug)]
+pub struct Extremes {
+    max: f32,
+    min: f32,
+}
+
+impl Extremes {
+    fn update(maybe_value: Option<f32>, maybe_range: &mut Option<Extremes>) {
+        match (maybe_value, maybe_range) {
+            (Some(value), Some(range)) => {
+                if value > range.max {
+                    range.max = value;
+                } else if value < range.min {
+                    range.min = value;
+                };
+            }
+            (Some(value), rel) => {
+                let _ = rel.insert({
+                    Extremes {
+                        max: value,
+                        min: value,
+                    }
+                });
+            }
+            _ => (),
+        };
+    }
+}
+
+fn adjust_luminance(color: Colour, x: f32, min_l: f32) -> Colour {
+    let color = Srgb::from_components(color.into_rgb()).into_linear();
+
+    let mut lab: Oklab = Oklab::from_color(color);
+    lab.l = (min_l + (1.0 - min_l) * (-4.0 * (1.0 - x)).exp()).clamp(0.0, 1.0);
+
+    let adjusted_rgb: Srgb<f32> = Srgb::from_color(lab);
+    Colour::RGB(
+        (adjusted_rgb.red * 255.0).round() as u8,
+        (adjusted_rgb.green * 255.0).round() as u8,
+        (adjusted_rgb.blue * 255.0).round() as u8,
+    )
+}

+ 26 - 4
src/output/details.rs

@@ -76,6 +76,7 @@ use crate::fs::fields::SecurityContextType;
 use crate::fs::filter::FileFilter;
 use crate::fs::filter::FileFilter;
 use crate::fs::{Dir, File};
 use crate::fs::{Dir, File};
 use crate::output::cell::TextCell;
 use crate::output::cell::TextCell;
+use crate::output::decay::{ColorScaleInformation, ColorScaleOptions};
 use crate::output::file_name::Options as FileStyle;
 use crate::output::file_name::Options as FileStyle;
 use crate::output::table::{Options as TableOptions, Row as TableRow, Table};
 use crate::output::table::{Options as TableOptions, Row as TableRow, Table};
 use crate::output::tree::{TreeDepth, TreeParams, TreeTrunk};
 use crate::output::tree::{TreeDepth, TreeParams, TreeTrunk};
@@ -113,6 +114,8 @@ pub struct Options {
 
 
     /// Whether to show a directory's mounted filesystem details
     /// Whether to show a directory's mounted filesystem details
     pub mounts: bool,
     pub mounts: bool,
+
+    pub color_scale: ColorScaleOptions,
 }
 }
 
 
 pub struct Render<'a> {
 pub struct Render<'a> {
@@ -160,6 +163,15 @@ impl<'a> Render<'a> {
         let mut pool = Pool::new(n_cpus);
         let mut pool = Pool::new(n_cpus);
         let mut rows = Vec::new();
         let mut rows = Vec::new();
 
 
+        let color_scale_info = ColorScaleInformation::from_color_scale(
+            self.opts.color_scale,
+            &self.files,
+            self.filter.dot_filter,
+            self.git,
+            self.git_ignoring,
+            self.recurse,
+        );
+
         if let Some(ref table) = self.opts.table {
         if let Some(ref table) = self.opts.table {
             match (self.git, self.dir) {
             match (self.git, self.dir) {
                 (Some(g), Some(d)) => {
                 (Some(g), Some(d)) => {
@@ -192,6 +204,7 @@ impl<'a> Render<'a> {
                 &mut rows,
                 &mut rows,
                 &self.files,
                 &self.files,
                 TreeDepth::root(),
                 TreeDepth::root(),
+                color_scale_info,
             );
             );
 
 
             for row in self.iterate_with_table(table.unwrap(), rows) {
             for row in self.iterate_with_table(table.unwrap(), rows) {
@@ -204,6 +217,7 @@ impl<'a> Render<'a> {
                 &mut rows,
                 &mut rows,
                 &self.files,
                 &self.files,
                 TreeDepth::root(),
                 TreeDepth::root(),
+                color_scale_info,
             );
             );
 
 
             for row in self.iterate(rows) {
             for row in self.iterate(rows) {
@@ -236,6 +250,7 @@ impl<'a> Render<'a> {
         rows: &mut Vec<Row>,
         rows: &mut Vec<Row>,
         src: &[File<'dir>],
         src: &[File<'dir>],
         depth: TreeDepth,
         depth: TreeDepth,
+        color_scale_info: Option<ColorScaleInformation>,
     ) {
     ) {
         use crate::fs::feature::xattr;
         use crate::fs::feature::xattr;
         use std::sync::{Arc, Mutex};
         use std::sync::{Arc, Mutex};
@@ -282,9 +297,9 @@ impl<'a> Render<'a> {
                         &[]
                         &[]
                     };
                     };
 
 
-                    let table_row = table
-                        .as_ref()
-                        .map(|t| t.row_for_file(file, self.show_xattr_hint(file)));
+                    let table_row = table.as_ref().map(|t| {
+                        t.row_for_file(file, self.show_xattr_hint(file), color_scale_info)
+                    });
 
 
                     let mut dir = None;
                     let mut dir = None;
                     if let Some(r) = self.recurse {
                     if let Some(r) = self.recurse {
@@ -374,7 +389,14 @@ impl<'a> Render<'a> {
                         ));
                         ));
                     }
                     }
 
 
-                    self.add_files_to_table(pool, table, rows, &files, depth.deeper());
+                    self.add_files_to_table(
+                        pool,
+                        table,
+                        rows,
+                        &files,
+                        depth.deeper(),
+                        color_scale_info,
+                    );
                     continue;
                     continue;
                 }
                 }
             }
             }

+ 13 - 1
src/output/grid_details.rs

@@ -9,6 +9,7 @@ use crate::fs::feature::git::GitCache;
 use crate::fs::filter::FileFilter;
 use crate::fs::filter::FileFilter;
 use crate::fs::{Dir, File};
 use crate::fs::{Dir, File};
 use crate::output::cell::{DisplayWidth, TextCell};
 use crate::output::cell::{DisplayWidth, TextCell};
+use crate::output::decay::ColorScaleInformation;
 use crate::output::details::{
 use crate::output::details::{
     Options as DetailsOptions, Render as DetailsRender, Row as DetailsRow,
     Options as DetailsOptions, Render as DetailsRender, Row as DetailsRow,
 };
 };
@@ -150,12 +151,23 @@ impl<'a> Render<'a> {
 
 
         let drender = self.details_for_column();
         let drender = self.details_for_column();
 
 
+        let color_scale_info = ColorScaleInformation::from_color_scale(
+            self.details.color_scale,
+            &self.files,
+            self.filter.dot_filter,
+            self.git,
+            self.git_ignoring,
+            None,
+        );
+
         let (first_table, _) = self.make_table(options, &drender);
         let (first_table, _) = self.make_table(options, &drender);
 
 
         let rows = self
         let rows = self
             .files
             .files
             .iter()
             .iter()
-            .map(|file| first_table.row_for_file(file, drender.show_xattr_hint(file)))
+            .map(|file| {
+                first_table.row_for_file(file, drender.show_xattr_hint(file), color_scale_info)
+            })
             .collect::<Vec<_>>();
             .collect::<Vec<_>>();
 
 
         let file_names = self
         let file_names = self

+ 1 - 0
src/output/mod.rs

@@ -1,6 +1,7 @@
 pub use self::cell::{DisplayWidth, TextCell, TextCellContents};
 pub use self::cell::{DisplayWidth, TextCell, TextCellContents};
 pub use self::escape::escape;
 pub use self::escape::escape;
 
 
+pub mod decay;
 pub mod details;
 pub mod details;
 pub mod file_name;
 pub mod file_name;
 pub mod grid;
 pub mod grid;

+ 52 - 14
src/output/render/size.rs

@@ -4,6 +4,7 @@ use number_prefix::Prefix;
 
 
 use crate::fs::fields as f;
 use crate::fs::fields as f;
 use crate::output::cell::{DisplayWidth, TextCell};
 use crate::output::cell::{DisplayWidth, TextCell};
+use crate::output::decay::{ColorScaleInformation, ColorScaleMode};
 use crate::output::table::SizeFormat;
 use crate::output::table::SizeFormat;
 
 
 impl f::Size {
 impl f::Size {
@@ -12,6 +13,7 @@ impl f::Size {
         colours: &C,
         colours: &C,
         size_format: SizeFormat,
         size_format: SizeFormat,
         numerics: &NumericLocale,
         numerics: &NumericLocale,
+        color_scale_info: Option<ColorScaleInformation>,
     ) -> TextCell {
     ) -> TextCell {
         use number_prefix::NumberPrefix;
         use number_prefix::NumberPrefix;
 
 
@@ -21,28 +23,49 @@ impl f::Size {
             Self::DeviceIDs(ref ids) => return ids.render(colours),
             Self::DeviceIDs(ref ids) => return ids.render(colours),
         };
         };
 
 
+        let gradient_style = colours.major();
+        let is_gradient_mode =
+            color_scale_info.is_some_and(|csi| csi.options.mode == ColorScaleMode::Gradient);
+
         #[rustfmt::skip]
         #[rustfmt::skip]
         let result = match size_format {
         let result = match size_format {
             SizeFormat::DecimalBytes  => NumberPrefix::decimal(size as f64),
             SizeFormat::DecimalBytes  => NumberPrefix::decimal(size as f64),
             SizeFormat::BinaryBytes   => NumberPrefix::binary(size as f64),
             SizeFormat::BinaryBytes   => NumberPrefix::binary(size as f64),
             SizeFormat::JustBytes     => {
             SizeFormat::JustBytes     => {
-
                 // Use the binary prefix to select a style.
                 // Use the binary prefix to select a style.
                 let prefix = match NumberPrefix::binary(size as f64) {
                 let prefix = match NumberPrefix::binary(size as f64) {
-                    NumberPrefix::Standalone(_)   => None,
-                    NumberPrefix::Prefixed(p, _)  => Some(p),
+                    NumberPrefix::Standalone(_) => None,
+                    NumberPrefix::Prefixed(p, _) => Some(p),
                 };
                 };
 
 
                 // But format the number directly using the locale.
                 // But format the number directly using the locale.
                 let string = numerics.format_int(size);
                 let string = numerics.format_int(size);
 
 
-                return TextCell::paint(colours.size(prefix), string);
+                return if is_gradient_mode {
+                    let csi = color_scale_info.unwrap();
+                    TextCell::paint(
+                        csi.adjust_style(gradient_style, size as f32, csi.size),
+                        string,
+                    )
+                } else {
+                    TextCell::paint(colours.size(prefix), string)
+                }
             }
             }
         };
         };
 
 
         #[rustfmt::skip]
         #[rustfmt::skip]
         let (prefix, n) = match result {
         let (prefix, n) = match result {
-            NumberPrefix::Standalone(b)   => return TextCell::paint(colours.size(None), numerics.format_int(b)),
+            NumberPrefix::Standalone(b) => {
+                return if is_gradient_mode {
+                    let csi = color_scale_info.unwrap();
+                    TextCell::paint(
+                        csi.adjust_style(gradient_style, size as f32, csi.size),
+                        numerics.format_int(b),
+                    )
+                } else {
+                    TextCell::paint(colours.size(None), numerics.format_int(b))
+                }
+            }
             NumberPrefix::Prefixed(p, n)  => (p, n),
             NumberPrefix::Prefixed(p, n)  => (p, n),
         };
         };
 
 
@@ -56,10 +79,20 @@ impl f::Size {
         TextCell {
         TextCell {
             // symbol is guaranteed to be ASCII since unit prefixes are hardcoded.
             // symbol is guaranteed to be ASCII since unit prefixes are hardcoded.
             width: DisplayWidth::from(&*number) + symbol.len(),
             width: DisplayWidth::from(&*number) + symbol.len(),
-            contents: vec![
-                colours.size(Some(prefix)).paint(number),
-                colours.unit(Some(prefix)).paint(symbol),
-            ]
+            contents: if is_gradient_mode {
+                let csi = color_scale_info.unwrap();
+                vec![
+                    csi.adjust_style(gradient_style, size as f32, csi.size)
+                        .paint(number),
+                    csi.adjust_style(gradient_style, size as f32, csi.size)
+                        .paint(symbol),
+                ]
+            } else {
+                vec![
+                    colours.size(Some(prefix)).paint(number),
+                    colours.unit(Some(prefix)).paint(symbol),
+                ]
+            }
             .into(),
             .into(),
         }
         }
     }
     }
@@ -126,7 +159,8 @@ pub mod test {
             directory.render(
             directory.render(
                 &TestColours,
                 &TestColours,
                 SizeFormat::JustBytes,
                 SizeFormat::JustBytes,
-                &NumericLocale::english()
+                &NumericLocale::english(),
+                None
             )
             )
         )
         )
     }
     }
@@ -144,7 +178,8 @@ pub mod test {
             directory.render(
             directory.render(
                 &TestColours,
                 &TestColours,
                 SizeFormat::DecimalBytes,
                 SizeFormat::DecimalBytes,
-                &NumericLocale::english()
+                &NumericLocale::english(),
+                None
             )
             )
         )
         )
     }
     }
@@ -162,7 +197,8 @@ pub mod test {
             directory.render(
             directory.render(
                 &TestColours,
                 &TestColours,
                 SizeFormat::BinaryBytes,
                 SizeFormat::BinaryBytes,
-                &NumericLocale::english()
+                &NumericLocale::english(),
+                None
             )
             )
         )
         )
     }
     }
@@ -180,7 +216,8 @@ pub mod test {
             directory.render(
             directory.render(
                 &TestColours,
                 &TestColours,
                 SizeFormat::JustBytes,
                 SizeFormat::JustBytes,
-                &NumericLocale::english()
+                &NumericLocale::english(),
+                None
             )
             )
         )
         )
     }
     }
@@ -206,7 +243,8 @@ pub mod test {
             directory.render(
             directory.render(
                 &TestColours,
                 &TestColours,
                 SizeFormat::JustBytes,
                 SizeFormat::JustBytes,
-                &NumericLocale::english()
+                &NumericLocale::english(),
+                None
             )
             )
         )
         )
     }
     }

+ 44 - 23
src/output/table.rs

@@ -13,12 +13,15 @@ use uzers::UsersCache;
 use crate::fs::feature::git::GitCache;
 use crate::fs::feature::git::GitCache;
 use crate::fs::{fields as f, File};
 use crate::fs::{fields as f, File};
 use crate::output::cell::TextCell;
 use crate::output::cell::TextCell;
+use crate::output::decay::ColorScaleInformation;
 #[cfg(unix)]
 #[cfg(unix)]
 use crate::output::render::{GroupRender, OctalPermissionsRender, UserRender};
 use crate::output::render::{GroupRender, OctalPermissionsRender, UserRender};
 use crate::output::render::{PermissionsPlusRender, TimeRender};
 use crate::output::render::{PermissionsPlusRender, TimeRender};
 use crate::output::time::TimeFormat;
 use crate::output::time::TimeFormat;
 use crate::theme::Theme;
 use crate::theme::Theme;
 
 
+use super::decay::ColorScaleMode;
+
 /// Options for displaying a table.
 /// Options for displaying a table.
 #[derive(PartialEq, Eq, Debug)]
 #[derive(PartialEq, Eq, Debug)]
 pub struct Options {
 pub struct Options {
@@ -282,6 +285,16 @@ impl TimeType {
             Self::Created => "Date Created",
             Self::Created => "Date Created",
         }
         }
     }
     }
+
+    /// Returns the corresponding time from [File]
+    pub fn get_corresponding_time(self, file: &File<'_>) -> Option<NaiveDateTime> {
+        match self {
+            TimeType::Modified => file.modified_time(),
+            TimeType::Changed => file.changed_time(),
+            TimeType::Accessed => file.accessed_time(),
+            TimeType::Created => file.created_time(),
+        }
+    }
 }
 }
 
 
 /// Fields for which of a file’s time fields should be displayed in the
 /// Fields for which of a file’s time fields should be displayed in the
@@ -408,11 +421,16 @@ impl<'a> Table<'a> {
         Row { cells }
         Row { cells }
     }
     }
 
 
-    pub fn row_for_file(&self, file: &File<'_>, xattrs: bool) -> Row {
+    pub fn row_for_file(
+        &self,
+        file: &File<'_>,
+        xattrs: bool,
+        color_scale_info: Option<ColorScaleInformation>,
+    ) -> Row {
         let cells = self
         let cells = self
             .columns
             .columns
             .iter()
             .iter()
-            .map(|c| self.display(file, *c, xattrs))
+            .map(|c| self.display(file, *c, xattrs, color_scale_info))
             .collect();
             .collect();
 
 
         Row { cells }
         Row { cells }
@@ -448,12 +466,21 @@ impl<'a> Table<'a> {
             .map(|p| f::OctalPermissions { permissions: p })
             .map(|p| f::OctalPermissions { permissions: p })
     }
     }
 
 
-    fn display(&self, file: &File<'_>, column: Column, xattrs: bool) -> TextCell {
+    fn display(
+        &self,
+        file: &File<'_>,
+        column: Column,
+        xattrs: bool,
+        color_scale_info: Option<ColorScaleInformation>,
+    ) -> TextCell {
         match column {
         match column {
             Column::Permissions => self.permissions_plus(file, xattrs).render(self.theme),
             Column::Permissions => self.permissions_plus(file, xattrs).render(self.theme),
-            Column::FileSize => file
-                .size()
-                .render(self.theme, self.size_format, &self.env.numeric),
+            Column::FileSize => file.size().render(
+                self.theme,
+                self.size_format,
+                &self.env.numeric,
+                color_scale_info,
+            ),
             #[cfg(unix)]
             #[cfg(unix)]
             Column::HardLinks => file.links().render(self.theme, &self.env.numeric),
             Column::HardLinks => file.links().render(self.theme, &self.env.numeric),
             #[cfg(unix)]
             #[cfg(unix)]
@@ -483,23 +510,17 @@ impl<'a> Table<'a> {
             #[cfg(unix)]
             #[cfg(unix)]
             Column::Octal => self.octal_permissions(file).render(self.theme.ui.octal),
             Column::Octal => self.octal_permissions(file).render(self.theme.ui.octal),
 
 
-            Column::Timestamp(TimeType::Modified) => file.modified_time().render(
-                self.theme.ui.date,
-                self.env.time_offset,
-                self.time_format.clone(),
-            ),
-            Column::Timestamp(TimeType::Changed) => file.changed_time().render(
-                self.theme.ui.date,
-                self.env.time_offset,
-                self.time_format.clone(),
-            ),
-            Column::Timestamp(TimeType::Created) => file.created_time().render(
-                self.theme.ui.date,
-                self.env.time_offset,
-                self.time_format.clone(),
-            ),
-            Column::Timestamp(TimeType::Accessed) => file.accessed_time().render(
-                self.theme.ui.date,
+            Column::Timestamp(time_type) => time_type.get_corresponding_time(file).render(
+                if color_scale_info.is_some_and(|csi| csi.options.mode == ColorScaleMode::Gradient)
+                {
+                    color_scale_info.unwrap().apply_time_gradient(
+                        self.theme.ui.date,
+                        file,
+                        time_type,
+                    )
+                } else {
+                    self.theme.ui.date
+                },
                 self.env.time_offset,
                 self.env.time_offset,
                 self.time_format.clone(),
                 self.time_format.clone(),
             ),
             ),

+ 7 - 6
src/theme/default_theme.rs

@@ -1,11 +1,11 @@
 use ansiterm::Colour::*;
 use ansiterm::Colour::*;
 use ansiterm::Style;
 use ansiterm::Style;
 
 
+use crate::output::decay::ColorScaleOptions;
 use crate::theme::ui_styles::*;
 use crate::theme::ui_styles::*;
-use crate::theme::ColourScale;
 
 
 impl UiStyles {
 impl UiStyles {
-    pub fn default_theme(scale: ColourScale) -> Self {
+    pub fn default_theme(scale: ColorScaleOptions) -> Self {
         Self {
         Self {
             colourful: true,
             colourful: true,
 
 
@@ -123,10 +123,11 @@ impl UiStyles {
 }
 }
 
 
 impl Size {
 impl Size {
-    pub fn colourful(scale: ColourScale) -> Self {
-        match scale {
-            ColourScale::Gradient => Self::colourful_gradient(),
-            ColourScale::Fixed => Self::colourful_fixed(),
+    pub fn colourful(scale: ColorScaleOptions) -> Self {
+        if scale.size {
+            Self::colourful_gradient()
+        } else {
+            Self::colourful_fixed()
         }
         }
     }
     }
 
 

+ 2 - 7
src/theme/mod.rs

@@ -2,6 +2,7 @@ use ansiterm::Style;
 
 
 use crate::fs::File;
 use crate::fs::File;
 use crate::info::filetype::FileType;
 use crate::info::filetype::FileType;
+use crate::output::decay::ColorScaleOptions;
 use crate::output::file_name::Colours as FileNameColours;
 use crate::output::file_name::Colours as FileNameColours;
 use crate::output::render;
 use crate::output::render;
 
 
@@ -17,7 +18,7 @@ mod default_theme;
 pub struct Options {
 pub struct Options {
     pub use_colours: UseColours,
     pub use_colours: UseColours,
 
 
-    pub colour_scale: ColourScale,
+    pub colour_scale: ColorScaleOptions,
 
 
     pub definitions: Definitions,
     pub definitions: Definitions,
 }
 }
@@ -41,12 +42,6 @@ pub enum UseColours {
     Never,
     Never,
 }
 }
 
 
-#[derive(PartialEq, Eq, Debug, Copy, Clone)]
-pub enum ColourScale {
-    Fixed,
-    Gradient,
-}
-
 #[derive(PartialEq, Eq, Debug, Default)]
 #[derive(PartialEq, Eq, Debug, Default)]
 pub struct Definitions {
 pub struct Definitions {
     pub ls: Option<String>,
     pub ls: Option<String>,