Skip to content

Commit

Permalink
feat: complete theme file implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
PThorpe92 committed Feb 14, 2024
1 parent 48c44c3 commit 0cbcf8f
Show file tree
Hide file tree
Showing 12 changed files with 1,096 additions and 160 deletions.
28 changes: 14 additions & 14 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ specific installation instructions can be found in [INSTALL.md](INSTALL.md).

[![Packaging status](https://repology.org/badge/vertical-allrepos/eza.svg)](https://repology.org/project/eza/versions)

# Themes

If you wish to make or use a custom theme, you can find the default theme.yml file [here](.themes/default-theme.yml). Currently by default, `eza` will look for a file named `theme.yml` in `$XDG_CONFIG_HOME/eza` (`$HOME/.config/eza` on a unix based OS), or in the value of the `$EZA_CONFIG_DIR` if it is set. `Eza` will use as the theme file by default, if it cannot find a theme file, it will use the default theme. Previous methods of setting the theme are still supported (`EXA_COLORS` & `LS_COLORS`), but only as a fallback if no `theme.yml` file is found.

NOTE: __Presently__, you must be sure to rename your file to `theme.yml`, including the provided default theme, when placing it in the chosen directory.

#### Contributing a Theme:

This is a brand new feature, and with everything so highly customizable, we highly encourage you to make your own themes and share them with the community! If you would like to contribute a theme, please open a pull request with your theme file added to the `themes` directory with a descriptive name. More requested configuration options will be coming soon, like custom Icons and more.

---

Click sections to expand.
Expand All @@ -78,8 +88,7 @@ Click sections to expand.
<h1>Command-line options</h1>
</a>

eza’s options are almost, but not quite, entirely unlike `ls`’s.

eza’s options are almost, but not quite, entirely unlike `ls`’s.
### Display options

- **-1**, **--oneline**: display one entry per line
Expand Down
1 change: 0 additions & 1 deletion completions/fish/eza.fish
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,5 @@ complete -c eza -l git -d "List each file's Git status, if tracked"
complete -c eza -l no-git -d "Suppress Git status"
complete -c eza -l git-repos -d "List each git-repos status and branch name"
complete -c eza -l git-repos-no-status -d "List each git-repos branch name (much faster)"
complete -c eza -l write-theme -d "Write default theme.yml file to the specified path"
complete -c eza -s '@' -l extended -d "List each file's extended attributes and sizes"
complete -c eza -s Z -l context -d "List each file's security context"
1 change: 0 additions & 1 deletion completions/nush/eza.nu
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,4 @@ export extern "eza" [
--context(-Z) # List each file's security context
--smart-group # Only show group if it has a different name from owner
--stdin # When piping to eza. Read file paths from stdin
--write-theme # Write the default theme to some directory: default cwd
]
1 change: 0 additions & 1 deletion completions/zsh/_eza
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ __eza() {
--no-git"[Suppress Git status]" \
--git-repos"[List each git-repos status and branch name]" \
--git-repos-no-status"[List each git-repos branch name (much faster)]" \
--write-theme="[Write the default theme.yml to a directory. default: cwd]" \
{-@,--extended}"[List each file's extended attributes and sizes]" \
{-Z,--context}"[List each file's security context]" \
{-M,--mounts}"[Show mount details (long mode only)]" \
Expand Down
6 changes: 0 additions & 6 deletions man/eza.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,6 @@ META OPTIONS
`-v`, `--version`
: Show version of eza.

`--write-theme=DIR`
: Write _eza_ default theme.yml file to the directory passed as argument, or defaults to the current working directory.

`--config` [if eza was built with config support]
: Specify a custom path to configuration file.

DISPLAY OPTIONS
===============

Expand Down
124 changes: 11 additions & 113 deletions src/options/config.rs
Original file line number Diff line number Diff line change
@@ -1,134 +1,32 @@
use crate::options::{MatchedFlags, Vars};
use crate::output::color_scale::ColorScaleOptions;
use crate::theme::UiStyles;
use dirs;
use serde_yaml;
use std::{ffi::OsStr, path::PathBuf};

use super::{flags, OptionsError};
use std::path::PathBuf;

#[derive(Debug, Default, Eq, PartialEq)]
pub struct ThemeConfig {
// This is rather bare for now, will be expanded with config file
pub location: ConfigLoc,
pub theme: UiStyles,
}

#[derive(Debug, Default, PartialEq, Eq)]
pub enum ConfigLoc {
#[default]
Default, // $XDG_CONFIG_HOME/eza/config|theme.yml
Env(PathBuf), // $EZA_CONFIG_DIR
Arg(PathBuf), // --config path/to/config|theme.yml
}

impl ThemeConfig {
pub fn write_default_theme_file(path: Option<&OsStr>) -> std::io::Result<()> {
if path.is_some_and(|path| std::path::Path::new(path).is_dir()) {
let path = std::path::Path::new(path.unwrap());
let path = path.join("theme.yml");
let file = std::fs::File::create(path.clone())?;
println!("Writing default theme to {:?}", path);
serde_yaml::to_writer(file, &UiStyles::default())
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
} else {
let default_path = std::env::var("EZA_CONFIG_DIR")
.map(|dir| PathBuf::from(&dir))
.unwrap_or(dirs::config_dir().unwrap_or_default().join("eza"));
if !default_path.exists() {
std::fs::create_dir_all(&default_path)?;
}
println!("Writing default theme to {:?}", default_path);
let default_file = default_path.join("theme.yml");
let file = std::fs::File::create(default_file)?;
let default = UiStyles::default();
serde_yaml::to_writer(file, &default)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
}
}

pub fn theme_from_yaml(file: Option<&str>) -> UiStyles {
if let Some(file) = file {
let file = std::fs::File::open(file);
if let Err(e) = file {
eprintln!("Could not open theme file: {e}");
return UiStyles::default();
pub fn to_theme(&self) -> Option<UiStyles> {
match &self.location {
ConfigLoc::Default => {
let path = dirs::config_dir()?.join("eza").join("theme.yml");
let file = std::fs::File::open(path).ok()?;
serde_yaml::from_reader(&file).ok()
}
let file = file.expect("Could not open theme file");
let theme: UiStyles = serde_yaml::from_reader(file).unwrap_or_else(|e| {
eprintln!("Could not parse theme file: {e}");
UiStyles::default()
});
theme
} else {
UiStyles::default()
}
}
pub fn deduce<V: Vars>(
matches: &MatchedFlags<'_>,
vars: &V,
opts: ColorScaleOptions,
) -> Result<ThemeConfig, crate::options::OptionsError> {
println!("Deducing theme");
if matches.has(&flags::WRITE_THEME)? {
let path = matches.get(&flags::WRITE_THEME)?;
println!("Writing default theme to {:?}", path);
let err = Self::write_default_theme_file(path).map_err(|e| e.to_string());
if let Err(err) = err {
return Err(OptionsError::WriteTheme(err));
ConfigLoc::Env(path) => {
let file = std::fs::File::open(path).ok()?;
serde_yaml::from_reader(&file).ok()
}
}
let theme_file = if matches.has(&flags::THEME)? {
let path = matches.get(&flags::THEME)?;
// passing --config will require a value as we will check default location
if path.is_none() {
return Err(OptionsError::BadArgument(&flags::THEME, "no value".into()));
}
path.map(|p| p.to_string_lossy().to_string())
} else {
None
};
Ok(Self::find_with_fallback(theme_file, vars, opts))
}

pub fn find_with_fallback<V: Vars>(
path: Option<String>,
vars: &V,
opts: ColorScaleOptions,
) -> Self {
if let Some(path) = path {
let path = std::path::PathBuf::from(path);
if path.is_dir() && path.exists() {
let path = path
.join("theme.yml")
.exists()
.then(|| path.join("theme.yml"));
match path {
Some(path) => {
let file = std::fs::read_to_string(&path).unwrap_or_default();
let uistyles: Option<UiStyles> = serde_yaml::from_str(&file).ok();
return Self {
location: ConfigLoc::Arg(path),
theme: uistyles.unwrap_or(UiStyles::default_theme(opts)),
};
}
None => return Self::default(),
}
}
} else if vars.get("EZA_CONFIG_DIR").is_some() {
let path = std::path::PathBuf::from(&format!(
"{}/theme.yml",
vars.get("EZA_CONFIG_DIR").unwrap().to_string_lossy()
));
if path.exists() {
let file = std::fs::read_to_string(&path).unwrap_or_default();
let uistyles: Option<UiStyles> = serde_yaml::from_str(&file).ok();
return Self {
location: ConfigLoc::Env(path),
theme: uistyles.unwrap_or(UiStyles::default_theme(opts)),
};
}
return Self::default();
};
Self::default()
}
}
4 changes: 2 additions & 2 deletions src/options/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub enum OptionsError {
FailedGlobPattern(String),

/// Error writing theme file to disk.
WriteTheme(String),
ThemeLocation(String),
}

/// The source of a string that failed to be parsed as a number.
Expand Down Expand Up @@ -99,7 +99,7 @@ impl fmt::Display for OptionsError {
Self::TreeAllAll => write!(f, "Option --tree is useless given --all --all"),
Self::FailedParse(s, n, e) => write!(f, "Value {s:?} not valid for {n}: {e}"),
Self::FailedGlobPattern(ref e) => write!(f, "Failed to parse glob pattern: {e}"),
Self::WriteTheme(ref e) => write!(f, "Failed to write theme file: {e}"),
Self::ThemeLocation(ref e) => write!(f, "Invalid or unsupported location for theme file: {e}"),
};
}
}
Expand Down
4 changes: 1 addition & 3 deletions src/options/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,6 @@ pub static OCTAL: Arg = Arg { short: Some(b'o'), long: "octal-permis
pub static SECURITY_CONTEXT: Arg = Arg { short: Some(b'Z'), long: "context", takes_value: TakesValue::Forbidden };
pub static STDIN: Arg = Arg { short: None, long: "stdin", takes_value: TakesValue::Forbidden };
pub static FILE_FLAGS: Arg = Arg { short: Some(b'O'), long: "flags", takes_value: TakesValue::Forbidden };
pub static WRITE_THEME: Arg = Arg { short: None, long: "write-theme", takes_value: TakesValue::Optional(None, ".")};
pub static THEME: Arg = Arg { short: None, long: "theme", takes_value: TakesValue::Optional(None, ".")};
pub static ALL_ARGS: Args = Args(&[
&VERSION, &HELP,

Expand All @@ -99,5 +97,5 @@ pub static ALL_ARGS: Args = Args(&[
&NO_PERMISSIONS, &NO_FILESIZE, &NO_USER, &NO_TIME, &SMART_GROUP,

&GIT, &NO_GIT, &GIT_REPOS, &GIT_REPOS_NO_STAT,
&EXTENDED, &OCTAL, &SECURITY_CONTEXT, &STDIN, &FILE_FLAGS, &WRITE_THEME, &THEME
&EXTENDED, &OCTAL, &SECURITY_CONTEXT, &STDIN, &FILE_FLAGS
]);
34 changes: 32 additions & 2 deletions src/options/theme.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
use std::path::PathBuf;

use crate::options::parser::MatchedFlags;
use crate::options::{flags, vars, OptionsError, Vars};
use crate::output::color_scale::ColorScaleOptions;
use crate::theme::{Definitions, Options, UseColours};

use super::config::ThemeConfig;
use super::config::{ConfigLoc, ThemeConfig};

impl Options {
pub fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
let use_colours = UseColours::deduce(matches, vars)?;
let colour_scale = ColorScaleOptions::deduce(matches, vars)?;
let theme_config = ThemeConfig::deduce(matches, vars, colour_scale)?;
let theme_config = ThemeConfig::deduce(vars)?;

let definitions = if use_colours == UseColours::Never {
Definitions::default()
} else {
Expand All @@ -25,6 +28,33 @@ impl Options {
}
}

impl ThemeConfig {
fn deduce<V: Vars>(vars: &V) -> Result<Option<Self>, OptionsError> {
if let Some(path) = vars.get("EZA_CONFIG_DIR") {
let path = PathBuf::from(path);
let path = path.join("theme.yml");
if path.exists() {
Ok(Some(ThemeConfig {
location: ConfigLoc::Env(path),
}))
} else {
Err(OptionsError::ThemeLocation(
path.to_string_lossy().to_string(),
))
}
} else {
let path = dirs::config_dir().unwrap_or_default();
let path = path.join("eza").join("theme.yml");
if path.exists() {
Ok(Some(ThemeConfig {
location: ConfigLoc::Default,
}))
} else {
Ok(None)
}
}
}
}
impl UseColours {
fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
let default_value = match vars.get(vars::NO_COLOR) {
Expand Down
Loading

0 comments on commit 0cbcf8f

Please sign in to comment.