Repository: eza-community/eza (language: rust)

Issue:
feat: supporting time format strings
### How to reproduce

versions: `v0.11.1 [+git]`
run: `TIME_STYLE='+%d-%m-%Y %H:%M:%S %z' eza -l`

```bash
> eza -l
eza: Option --time-style has no "+%d-%m-%Y %H:%M:%S %z" setting (choices: default, long-iso, full-iso, iso, relative)
```
### Expected results

Since `eza` is using `TIME_STYLE` env var, I would expect it to handle valid strings as the `ls` command does. 

```bash
> ls -l
total 0
drwxr-xr-x 1 crazybyte crazybyte    0 04-03-2023 19:27:39 +0100 Documents
drwxr-xr-x 1 crazybyte crazybyte 3468 13-09-2023 18:00:05 +0200 Downloads
drwxr-xr-x 1 crazybyte crazybyte   50 11-07-2023 18:26:43 +0200 Games
drwxr-xr-x 1 crazybyte crazybyte  184 07-07-2023 09:38:23 +0200 Workspace
```

See also https://github.com/ogham/exa/issues/923 and https://github.com/ogham/exa/issues/875.


Discussion / hints:
We would be happy to accept a PR for this, as it is definitely something that would be nice to support :+1: 

Fix patch:
diff --git a/README.md b/README.md
index 683f4cedb..ad0d0756b 100644
--- a/README.md
+++ b/README.md
@@ -140,6 +140,7 @@ nix-env -i eza
 ```
 
 **Declarative Nix Installations**
+
 - Simple NixOS installation: [rfaulhaber/dotfiles](https://github.com/rfaulhaber/dotfiles/blob/a8d084d178efd0592b7ac02d34a450fb58913aca/nix/modules/programs/eza/default.nix#L15)
 - Using the flake via NixOS: [hallettj/home.nix](https://github.com/hallettj/home.nix/blob/a8388483e5d78e110be73c5af0e7f0e3ca8f8aa3/flake.nix#L19)
 - Using home-manager on NixOS: [Misterio77/nix-config](https://github.com/Misterio77/nix-config/blob/6867d66a2fe7899c608b9c8e5a8f9aee279d188b/home/misterio/features/cli/fish.nix#L6)
@@ -209,7 +210,6 @@ sudo port install eza
 
 [![Windows package](https://repology.org/badge/version-for-repo/winget/eza.svg)](https://repology.org/project/eza/versions)
 
-
 Eza is available on Winget.
 
 To install eza, run:
@@ -238,13 +238,13 @@ scoop install eza
 > Change `~/.zshrc` to your preferred zsh config file.
 
 ##### Clone the repository:
-   
+
 ```sh
 git clone https://github.com/eza-community/eza.git
 ```
 
 ##### Add the completion path to your zsh configuration:
-   
+
 Replace `<path_to_eza>` with the actual path where you cloned the `eza` repository.
 
 ```sh
@@ -252,7 +252,7 @@ echo 'export FPATH="<path_to_eza>/completions/zsh:$FPATH"' >> ~/.zshrc
 ```
 
 ##### Reload your zsh configuration:
-   
+
 ```sh
 source ~/.zshrc
 ```
@@ -323,9 +323,9 @@ These options are available when running with `--long` (`-l`):
 - **--changed**: use the changed timestamp field
 - **--git**: list each file’s Git status, if tracked or ignored
 - **--git-repos**: list each directory’s Git status, if tracked
-- **--git-repos-no-status**:  list whether a directory is a Git repository, but not its status (faster)
+- **--git-repos-no-status**: list whether a directory is a Git repository, but not its status (faster)
 - **--no-git**: suppress Git status (always overrides `--git`, `--git-repos`, `--git-repos-no-status`)
-- **--time-style**: how to format timestamps
+- **--time-style**: how to format timestamps. valid timestamp styles are ‘`default`’, ‘`iso`’, ‘`long-iso`’, ‘`full-iso`’, ‘`relative`', or you can use a `custom` style with '`+`' as prefix. (Ex: "`+%Y/%m/%d, %H:%M`" => "`2023/9/30, 12:00`"). [more about format syntax](https://docs.rs/chrono/latest/chrono/format/strftime/index.html).
 - **--no-permissions**: suppress the permissions field
 - **-o**, **--octal-permissions**: list each file's permission in octal format
 - **--no-filesize**: suppress the filesize field
diff --git a/man/eza.1.md b/man/eza.1.md
index acfcf05bc..a8f6d70a5 100644
--- a/man/eza.1.md
+++ b/man/eza.1.md
@@ -172,7 +172,7 @@ These options are available when running with `--long` (`-l`):
 `--time-style=STYLE`
 : How to format timestamps.
 
-: Valid timestamp styles are ‘`default`’, ‘`iso`’, ‘`long-iso`’, ‘`full-iso`’, and ‘`relative`’.
+: Valid timestamp styles are ‘`default`’, ‘`iso`’, ‘`long-iso`’, ‘`full-iso`’, ‘`relative`', or you can use a `custom` style with '`+`' as prefix. (Ex: "`+%Y/%m/%d, %H:%M`" => "`2023/9/30, 12:00`"). for more details about format syntax, please read: https://docs.rs/chrono/latest/chrono/format/strftime/index.html
 
 `-u`, `--accessed`
 : Use the accessed timestamp field.
diff --git a/src/options/help.rs b/src/options/help.rs
index 9344737a4..6d0043cc1 100644
--- a/src/options/help.rs
+++ b/src/options/help.rs
@@ -61,7 +61,7 @@ LONG VIEW OPTIONS
   -u, --accessed           use the accessed timestamp field
   -U, --created            use the created timestamp field
   --changed                use the changed timestamp field
-  --time-style             how to format timestamps (default, iso, long-iso, full-iso, relative)
+  --time-style             how to format timestamps (default, iso, long-iso, full-iso, relative, or a custom style with '+' as prefix. Ex: '+%Y/%m/%d')
   --no-permissions         suppress the permissions field
   -o, --octal-permissions  list each file's permission in octal format
   --no-filesize            suppress the filesize field
diff --git a/src/options/mod.rs b/src/options/mod.rs
index 041b44ed2..80eb22229 100644
--- a/src/options/mod.rs
+++ b/src/options/mod.rs
@@ -209,7 +209,16 @@ impl Options {
     }
 }
 
-/// The result of the `Options::getopts` function.
+/// The result of the `Options::parse` function.
+///
+/// NOTE: We disallow the `large_enum_variant` lint here, because we're not
+/// overly concerned about variant fragmentation. We can do this because we are
+/// reasonably sure that the error variant will be rare, and only on faulty
+/// program execution and thus boxing the large variant will be a waste of
+/// resources, but should we come to use it more, we should reconsider.
+///
+/// See <https://github.com/eza-community/eza/pull/437#issuecomment-1738470254>
+#[allow(clippy::large_enum_variant)]
 #[derive(Debug)]
 pub enum OptionsResult<'args> {
     /// The options were parsed successfully.
diff --git a/src/options/view.rs b/src/options/view.rs
index 2dde18126..5795389bd 100644
--- a/src/options/view.rs
+++ b/src/options/view.rs
@@ -319,6 +319,9 @@ impl TimeFormat {
             "iso" => Ok(Self::ISOFormat),
             "long-iso" => Ok(Self::LongISO),
             "full-iso" => Ok(Self::FullISO),
+            fmt if fmt.starts_with('+') => Ok(Self::Custom {
+                fmt: fmt[1..].to_owned(),
+            }),
             _ => Err(OptionsError::BadArgument(&flags::TIME_STYLE, word)),
         }
     }
@@ -533,11 +536,13 @@ mod test {
         test!(empty:     TimeFormat <- [], None;                            Both => like Ok(TimeFormat::DefaultFormat));
 
         // Individual settings
-        test!(default:   TimeFormat <- ["--time-style=default"], None;      Both => like Ok(TimeFormat::DefaultFormat));
-        test!(iso:       TimeFormat <- ["--time-style", "iso"], None;       Both => like Ok(TimeFormat::ISOFormat));
-        test!(relative:  TimeFormat <- ["--time-style", "relative"], None;  Both => like Ok(TimeFormat::Relative));
-        test!(long_iso:  TimeFormat <- ["--time-style=long-iso"], None;     Both => like Ok(TimeFormat::LongISO));
-        test!(full_iso:  TimeFormat <- ["--time-style", "full-iso"], None;  Both => like Ok(TimeFormat::FullISO));
+        test!(default:          TimeFormat <- ["--time-style=default"], None;      Both => like Ok(TimeFormat::DefaultFormat));
+        test!(iso:              TimeFormat <- ["--time-style", "iso"], None;       Both => like Ok(TimeFormat::ISOFormat));
+        test!(relative:         TimeFormat <- ["--time-style", "relative"], None;  Both => like Ok(TimeFormat::Relative));
+        test!(long_iso:         TimeFormat <- ["--time-style=long-iso"], None;     Both => like Ok(TimeFormat::LongISO));
+        test!(full_iso:         TimeFormat <- ["--time-style", "full-iso"], None;  Both => like Ok(TimeFormat::FullISO));
+        test!(custom_style:     TimeFormat <- ["--time-style", "+%Y/%m/%d"], None; Both => like Ok(TimeFormat::Custom { .. }));
+        test!(bad_custom_style: TimeFormat <- ["--time-style", "%Y/%m/%d"], None;  Both => err OptionsError::BadArgument(&flags::TIME_STYLE, OsString::from("%Y/%m/%d")));
 
         // Overriding
         test!(actually:  TimeFormat <- ["--time-style=default", "--time-style", "iso"], None;  Last => like Ok(TimeFormat::ISOFormat));
diff --git a/src/output/table.rs b/src/output/table.rs
index 652885e39..8fd087177 100644
--- a/src/output/table.rs
+++ b/src/output/table.rs
@@ -375,7 +375,7 @@ impl<'a> Table<'a> {
             columns,
             git,
             env,
-            time_format: options.time_format,
+            time_format: options.time_format.clone(),
             size_format: options.size_format,
             #[cfg(unix)]
             user_format: options.user_format,
@@ -471,22 +471,22 @@ impl<'a> Table<'a> {
             Column::Timestamp(TimeType::Modified) => file.modified_time().render(
                 self.theme.ui.date,
                 self.env.time_offset,
-                self.time_format,
+                self.time_format.clone(),
             ),
             Column::Timestamp(TimeType::Changed) => file.changed_time().render(
                 self.theme.ui.date,
                 self.env.time_offset,
-                self.time_format,
+                self.time_format.clone(),
             ),
             Column::Timestamp(TimeType::Created) => file.created_time().render(
                 self.theme.ui.date,
                 self.env.time_offset,
-                self.time_format,
+                self.time_format.clone(),
             ),
             Column::Timestamp(TimeType::Accessed) => file.accessed_time().render(
                 self.theme.ui.date,
                 self.env.time_offset,
-                self.time_format,
+                self.time_format.clone(),
             ),
         }
     }
diff --git a/src/output/time.rs b/src/output/time.rs
index 262bb4943..1dacf1f4f 100644
--- a/src/output/time.rs
+++ b/src/output/time.rs
@@ -20,9 +20,9 @@ use unicode_width::UnicodeWidthStr;
 /// own enum variants. It’s not worth looking the locale up if the formatter
 /// prints month names as numbers.
 ///
-/// Currently exa does not support *custom* styles, where the user enters a
+/// Also, eza supports *custom* styles, where the user enters a
 /// format string in an environment variable or something. Just these four.
-#[derive(PartialEq, Eq, Debug, Copy, Clone)]
+#[derive(PartialEq, Eq, Debug, Clone)]
 pub enum TimeFormat {
     /// The **default format** uses the user’s locale to print month names,
     /// and specifies the timestamp down to the minute for recent times, and
@@ -45,6 +45,9 @@ pub enum TimeFormat {
 
     /// Use a relative but fixed width representation.
     Relative,
+
+    /// Use a custom format
+    Custom { fmt: String },
 }
 
 impl TimeFormat {
@@ -56,6 +59,7 @@ impl TimeFormat {
             Self::LongISO        => long(time),
             Self::FullISO        => full(time),
             Self::Relative       => relative(time),
+            Self::Custom { fmt } => custom(time, &fmt),
         };
     }
 }
@@ -111,6 +115,10 @@ fn full(time: &DateTime<FixedOffset>) -> String {
     time.format("%Y-%m-%d %H:%M:%S.%f %z").to_string()
 }
 
+fn custom(time: &DateTime<FixedOffset>, fmt: &str) -> String {
+    time.format(fmt).to_string()
+}
+
 lazy_static! {
 
     static ref CURRENT_YEAR: i32 = Local::now().year();


Test patch: