Sfoglia il codice sorgente

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 anni fa
parent
commit
288696db10

+ 45 - 0
Cargo.lock

@@ -32,12 +32,19 @@ version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
 
+[[package]]
+name = "ansi_colours"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a1558bd2075d341b9ca698ec8eb6fcc55a746b1fc4255585aad5b141d918a80"
+
 [[package]]
 name = "ansiterm"
 version = "0.12.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4ab587f5395da16dd2e6939adf53dede583221b320cadfb94e02b5b7b9bf24cc"
 dependencies = [
+ "ansi_colours",
  "winapi",
 ]
 
@@ -89,6 +96,15 @@ dependencies = [
  "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]]
 name = "autocfg"
 version = "1.1.0"
@@ -370,6 +386,7 @@ dependencies = [
  "num_cpus",
  "number_prefix",
  "once_cell",
+ "palette",
  "percent-encoding",
  "phf",
  "proc-mounts",
@@ -384,6 +401,12 @@ dependencies = [
  "zoneinfo_compiled",
 ]
 
+[[package]]
+name = "fast-srgb8"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
+
 [[package]]
 name = "fastrand"
 version = "2.0.0"
@@ -709,6 +732,28 @@ dependencies = [
  "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]]
 name = "partition-identity"
 version = "0.3.0"

+ 2 - 1
Cargo.toml

@@ -71,7 +71,7 @@ name = "eza"
 
 
 [dependencies]
-ansiterm = "0.12.2"
+ansiterm = { version = "0.12.2", features = ["ansi_colours"] }
 chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
 glob = "0.3"
 libc = "0.2"
@@ -80,6 +80,7 @@ log = "0.4"
 natord = "1.0"
 num_cpus = "1.16"
 number_prefix = "0.4"
+palette = { version = "0.7.3", default-features = false, features = ["std"] }
 once_cell = "1.18.0"
 percent-encoding = "2.3.0"
 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
 - **-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
+- **--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)
 - **--hyperlink**: display entries as hyperlinks
 - **-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")
             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
 
     case "$cur" in

+ 10 - 1
completions/fish/eza.fish

@@ -19,7 +19,16 @@ complete -c eza -l color \
     never\t'Never use colour'
 "
 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 "
   always\t'Always display icons'
   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
     --color-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
     --no-quotes                # Don't quote file names with spaces
     --hyperlink                # Display entries as hyperlinks

+ 2 - 1
completions/zsh/_eza

@@ -21,7 +21,8 @@ __eza() {
         {-X,--dereference}"[Dereference symbolic links when displaying information]" \
         {-F,--classify}"[Display type indicator by file names]" \
         --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)" \
         --no-quotes"[Don't quote filenames with spaces]" \
         --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.
 
 `--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`
 : Display icons next to file names.
@@ -105,10 +112,15 @@ The default value is ‘`automatic`’.
 `-w`, `--width=COLS`
 : 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`
 : Only show group if it has a different name from owner
 
-
 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
 
+## `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`
 
 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 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"];
 
-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
 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 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 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 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 };
@@ -81,7 +85,8 @@ pub static ALL_ARGS: Args = Args(&[
     &VERSION, &HELP,
 
     &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,
     &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
 
 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

+ 57 - 17
src/options/parser.rs

@@ -99,7 +99,7 @@ pub enum TakesValue {
     Forbidden,
 
     /// 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.
@@ -176,7 +176,7 @@ impl Args {
                     let arg = self.lookup_long(before)?;
                     let flag = Flag::Long(arg.long);
                     match arg.takes_value {
-                        TakesValue::Necessary(_) | TakesValue::Optional(_) => {
+                        TakesValue::Necessary(_) | TakesValue::Optional(_, _) => {
                             result_flags.push((flag, Some(after)));
                         }
                         TakesValue::Forbidden => return Err(ParseError::ForbiddenValue { flag }),
@@ -198,11 +198,14 @@ impl Args {
                                 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, 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 flag = Flag::Short(*byte);
                         match arg.takes_value {
-                            TakesValue::Forbidden | TakesValue::Optional(_) => {
+                            TakesValue::Forbidden => {
                                 result_flags.push((flag, None));
                             }
+                            TakesValue::Optional(_, default) => {
+                                result_flags
+                                    .push((flag, Some(bytes_to_os_str(default.as_bytes()))));
+                            }
                             TakesValue::Necessary(values) => {
                                 return Err(ParseError::NeedsValue { flag, values });
                             }
@@ -246,7 +253,7 @@ impl Args {
                     let arg = self.lookup_short(*arg_with_value)?;
                     let flag = Flag::Short(arg.short.unwrap());
                     match arg.takes_value {
-                        TakesValue::Necessary(_) | TakesValue::Optional(_) => {
+                        TakesValue::Necessary(_) | TakesValue::Optional(_, _) => {
                             result_flags.push((flag, Some(after)));
                         }
                         TakesValue::Forbidden => {
@@ -274,7 +281,7 @@ impl Args {
                             TakesValue::Forbidden => {
                                 result_flags.push((flag, None));
                             }
-                            TakesValue::Necessary(values) | TakesValue::Optional(values) => {
+                            TakesValue::Necessary(values) => {
                                 if index < bytes.len() - 1 {
                                     let remnants = &bytes[index + 1..];
                                     result_flags.push((flag, Some(bytes_to_os_str(remnants))));
@@ -283,14 +290,37 @@ impl Args {
                                     result_flags.push((flag, Some(next_arg)));
                                 } else {
                                     match arg.takes_value {
-                                        TakesValue::Forbidden => {
+                                        TakesValue::Forbidden | TakesValue::Optional(_, _) => {
                                             unreachable!()
                                         }
                                         TakesValue::Necessary(_) => {
                                             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,
     }
 }
@@ -639,7 +667,8 @@ mod parse_test {
         &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'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
@@ -697,6 +726,17 @@ mod parse_test {
     test!(unknown_short_2nd:     ["-lq"]          => 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' });
+
+    // 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)]

+ 11 - 23
src/options/theme.rs

@@ -1,11 +1,12 @@
 use crate::options::parser::MatchedFlags;
 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 {
     pub fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
         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 {
             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 {
     fn deduce<V: Vars>(vars: &V) -> Self {
         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_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 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.
 /// Any explicit use of `--icons=WHEN` overrides this behavior.
 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::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::grid_details::{self, RowThreshold};
 use crate::output::table::{
@@ -79,7 +82,7 @@ impl Mode {
 
         if flag.matches(&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));
         }
 
@@ -142,13 +145,14 @@ impl grid::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 {
             table: None,
             header: false,
             xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
             secattr: xattr::ENABLED && matches.has(&flags::SECURITY_CONTEXT)?,
             mounts: matches.has(&flags::MOUNTS)?,
+            color_scale: ColorScaleOptions::deduce(matches, vars)?,
         };
 
         Ok(details)
@@ -169,14 +173,13 @@ impl details::Options {
             xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
             secattr: xattr::ENABLED && matches.has(&flags::SECURITY_CONTEXT)?,
             mounts: matches.has(&flags::MOUNTS)?,
+            color_scale: ColorScaleOptions::deduce(matches, vars)?,
         })
     }
 }
 
 impl TerminalWidth {
     fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
-        use crate::options::vars;
-
         if let Some(width) = matches.get(&flags::WIDTH)? {
             let arg_str = width.to_string_lossy();
             match arg_str.parse() {
@@ -208,8 +211,6 @@ impl TerminalWidth {
 
 impl RowThreshold {
     fn deduce<V: Vars>(vars: &V) -> Result<Self, OptionsError> {
-        use crate::options::vars;
-
         if let Some(columns) = vars
             .get_with_fallback(vars::EZA_GRID_ROWS, vars::EXA_GRID_ROWS)
             .and_then(|s| s.into_string().ok())
@@ -249,7 +250,6 @@ impl TableOptions {
 
 impl Columns {
     fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
-        use crate::options::vars;
         let time_types = TimeTypes::deduce(matches)?;
 
         let no_git_env = vars
@@ -319,7 +319,6 @@ impl TimeFormat {
         let word = if let Some(w) = matches.get(&flags::TIME_STYLE)? {
             w.to_os_string()
         } else {
-            use crate::options::vars;
             match vars.get(vars::TIME_STYLE) {
                 Some(ref t) if !t.is_empty() => t.clone(),
                 _ => 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)]
 mod test {
     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::{Dir, File};
 use crate::output::cell::TextCell;
+use crate::output::decay::{ColorScaleInformation, ColorScaleOptions};
 use crate::output::file_name::Options as FileStyle;
 use crate::output::table::{Options as TableOptions, Row as TableRow, Table};
 use crate::output::tree::{TreeDepth, TreeParams, TreeTrunk};
@@ -113,6 +114,8 @@ pub struct Options {
 
     /// Whether to show a directory's mounted filesystem details
     pub mounts: bool,
+
+    pub color_scale: ColorScaleOptions,
 }
 
 pub struct Render<'a> {
@@ -160,6 +163,15 @@ impl<'a> Render<'a> {
         let mut pool = Pool::new(n_cpus);
         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 {
             match (self.git, self.dir) {
                 (Some(g), Some(d)) => {
@@ -192,6 +204,7 @@ impl<'a> Render<'a> {
                 &mut rows,
                 &self.files,
                 TreeDepth::root(),
+                color_scale_info,
             );
 
             for row in self.iterate_with_table(table.unwrap(), rows) {
@@ -204,6 +217,7 @@ impl<'a> Render<'a> {
                 &mut rows,
                 &self.files,
                 TreeDepth::root(),
+                color_scale_info,
             );
 
             for row in self.iterate(rows) {
@@ -236,6 +250,7 @@ impl<'a> Render<'a> {
         rows: &mut Vec<Row>,
         src: &[File<'dir>],
         depth: TreeDepth,
+        color_scale_info: Option<ColorScaleInformation>,
     ) {
         use crate::fs::feature::xattr;
         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;
                     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;
                 }
             }

+ 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::{Dir, File};
 use crate::output::cell::{DisplayWidth, TextCell};
+use crate::output::decay::ColorScaleInformation;
 use crate::output::details::{
     Options as DetailsOptions, Render as DetailsRender, Row as DetailsRow,
 };
@@ -150,12 +151,23 @@ impl<'a> Render<'a> {
 
         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 rows = self
             .files
             .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<_>>();
 
         let file_names = self

+ 1 - 0
src/output/mod.rs

@@ -1,6 +1,7 @@
 pub use self::cell::{DisplayWidth, TextCell, TextCellContents};
 pub use self::escape::escape;
 
+pub mod decay;
 pub mod details;
 pub mod file_name;
 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::output::cell::{DisplayWidth, TextCell};
+use crate::output::decay::{ColorScaleInformation, ColorScaleMode};
 use crate::output::table::SizeFormat;
 
 impl f::Size {
@@ -12,6 +13,7 @@ impl f::Size {
         colours: &C,
         size_format: SizeFormat,
         numerics: &NumericLocale,
+        color_scale_info: Option<ColorScaleInformation>,
     ) -> TextCell {
         use number_prefix::NumberPrefix;
 
@@ -21,28 +23,49 @@ impl f::Size {
             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]
         let result = match size_format {
             SizeFormat::DecimalBytes  => NumberPrefix::decimal(size as f64),
             SizeFormat::BinaryBytes   => NumberPrefix::binary(size as f64),
             SizeFormat::JustBytes     => {
-
                 // Use the binary prefix to select a style.
                 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.
                 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]
         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),
         };
 
@@ -56,10 +79,20 @@ impl f::Size {
         TextCell {
             // symbol is guaranteed to be ASCII since unit prefixes are hardcoded.
             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(),
         }
     }
@@ -126,7 +159,8 @@ pub mod test {
             directory.render(
                 &TestColours,
                 SizeFormat::JustBytes,
-                &NumericLocale::english()
+                &NumericLocale::english(),
+                None
             )
         )
     }
@@ -144,7 +178,8 @@ pub mod test {
             directory.render(
                 &TestColours,
                 SizeFormat::DecimalBytes,
-                &NumericLocale::english()
+                &NumericLocale::english(),
+                None
             )
         )
     }
@@ -162,7 +197,8 @@ pub mod test {
             directory.render(
                 &TestColours,
                 SizeFormat::BinaryBytes,
-                &NumericLocale::english()
+                &NumericLocale::english(),
+                None
             )
         )
     }
@@ -180,7 +216,8 @@ pub mod test {
             directory.render(
                 &TestColours,
                 SizeFormat::JustBytes,
-                &NumericLocale::english()
+                &NumericLocale::english(),
+                None
             )
         )
     }
@@ -206,7 +243,8 @@ pub mod test {
             directory.render(
                 &TestColours,
                 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::{fields as f, File};
 use crate::output::cell::TextCell;
+use crate::output::decay::ColorScaleInformation;
 #[cfg(unix)]
 use crate::output::render::{GroupRender, OctalPermissionsRender, UserRender};
 use crate::output::render::{PermissionsPlusRender, TimeRender};
 use crate::output::time::TimeFormat;
 use crate::theme::Theme;
 
+use super::decay::ColorScaleMode;
+
 /// Options for displaying a table.
 #[derive(PartialEq, Eq, Debug)]
 pub struct Options {
@@ -282,6 +285,16 @@ impl TimeType {
             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
@@ -408,11 +421,16 @@ impl<'a> Table<'a> {
         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
             .columns
             .iter()
-            .map(|c| self.display(file, *c, xattrs))
+            .map(|c| self.display(file, *c, xattrs, color_scale_info))
             .collect();
 
         Row { cells }
@@ -448,12 +466,21 @@ impl<'a> Table<'a> {
             .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 {
             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)]
             Column::HardLinks => file.links().render(self.theme, &self.env.numeric),
             #[cfg(unix)]
@@ -483,23 +510,17 @@ impl<'a> Table<'a> {
             #[cfg(unix)]
             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.time_format.clone(),
             ),

+ 7 - 6
src/theme/default_theme.rs

@@ -1,11 +1,11 @@
 use ansiterm::Colour::*;
 use ansiterm::Style;
 
+use crate::output::decay::ColorScaleOptions;
 use crate::theme::ui_styles::*;
-use crate::theme::ColourScale;
 
 impl UiStyles {
-    pub fn default_theme(scale: ColourScale) -> Self {
+    pub fn default_theme(scale: ColorScaleOptions) -> Self {
         Self {
             colourful: true,
 
@@ -123,10 +123,11 @@ impl UiStyles {
 }
 
 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::info::filetype::FileType;
+use crate::output::decay::ColorScaleOptions;
 use crate::output::file_name::Colours as FileNameColours;
 use crate::output::render;
 
@@ -17,7 +18,7 @@ mod default_theme;
 pub struct Options {
     pub use_colours: UseColours,
 
-    pub colour_scale: ColourScale,
+    pub colour_scale: ColorScaleOptions,
 
     pub definitions: Definitions,
 }
@@ -41,12 +42,6 @@ pub enum UseColours {
     Never,
 }
 
-#[derive(PartialEq, Eq, Debug, Copy, Clone)]
-pub enum ColourScale {
-    Fixed,
-    Gradient,
-}
-
 #[derive(PartialEq, Eq, Debug, Default)]
 pub struct Definitions {
     pub ls: Option<String>,