From 4842528b92f0bff06d9356aac7f93bcc76b92767 Mon Sep 17 00:00:00 2001 From: Alyssa Evans Date: Sat, 25 Oct 2025 04:04:11 -0700 Subject: [PATCH] Add --theme CLI flag to override theme file path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new --theme (-t) command-line option that allows specifying a direct path to a theme file, similar to how --config works for configuration files. This is useful for environments like Nix where avoiding symlinks and using CLI overrides is preferred. The --theme flag takes precedence over the theme specified in the config file and supports theme inheritance via the 'inherits' field. Usage: hx --theme /path/to/theme.toml file.txt 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- helix-loader/src/lib.rs | 12 ++++++ helix-term/src/application.rs | 77 +++++++++++++++++++++++------------ helix-term/src/args.rs | 5 +++ helix-term/src/main.rs | 2 + helix-view/src/theme.rs | 53 ++++++++++++++++++++++++ 5 files changed, 123 insertions(+), 26 deletions(-) diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index 9872b77a0d2c..fc9b42b7ac44 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -13,6 +13,8 @@ static RUNTIME_DIRS: once_cell::sync::Lazy> = static CONFIG_FILE: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); +static THEME_FILE: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); + static LOG_FILE: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); pub fn initialize_config_file(specified_file: Option) { @@ -21,6 +23,12 @@ pub fn initialize_config_file(specified_file: Option) { CONFIG_FILE.set(config_file).ok(); } +pub fn initialize_theme_file(specified_file: Option) { + if let Some(theme_file) = specified_file { + THEME_FILE.set(theme_file).ok(); + } +} + pub fn initialize_log_file(specified_file: Option) { let log_file = specified_file.unwrap_or_else(default_log_file); ensure_parent_dir(&log_file); @@ -136,6 +144,10 @@ pub fn config_file() -> PathBuf { CONFIG_FILE.get().map(|path| path.to_path_buf()).unwrap() } +pub fn theme_file() -> Option { + THEME_FILE.get().map(|path| path.to_path_buf()) +} + pub fn log_file() -> PathBuf { LOG_FILE.get().map(|path| path.to_path_buf()).unwrap() } diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 8c1db6499080..587606afbe2a 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -462,32 +462,57 @@ impl Application { mode: Option, ) { let true_color = terminal_true_color || config.editor.true_color || crate::true_color(); - let theme = config - .theme - .as_ref() - .and_then(|theme_config| { - let theme = theme_config.choose(mode); - editor - .theme_loader - .load(theme) - .map_err(|e| { - log::warn!("failed to load theme `{}` - {}", theme, e); - e - }) - .ok() - .filter(|theme| { - let colors_ok = true_color || theme.is_16_color(); - if !colors_ok { - log::warn!( - "loaded theme `{}` but cannot use it because true color \ - support is not enabled", - theme.name() - ); - } - colors_ok - }) - }) - .unwrap_or_else(|| editor.theme_loader.default_theme(true_color)); + + // Check if a theme file was specified via --theme flag + let theme = if let Some(theme_file) = helix_loader::theme_file() { + editor + .theme_loader + .load_from_file(&theme_file) + .map_err(|e| { + log::warn!("failed to load theme from file `{}` - {}", theme_file.display(), e); + e + }) + .ok() + .filter(|theme| { + let colors_ok = true_color || theme.is_16_color(); + if !colors_ok { + log::warn!( + "loaded theme from file `{}` but cannot use it because true color \ + support is not enabled", + theme_file.display() + ); + } + colors_ok + }) + } else { + config + .theme + .as_ref() + .and_then(|theme_config| { + let theme = theme_config.choose(mode); + editor + .theme_loader + .load(theme) + .map_err(|e| { + log::warn!("failed to load theme `{}` - {}", theme, e); + e + }) + .ok() + .filter(|theme| { + let colors_ok = true_color || theme.is_16_color(); + if !colors_ok { + log::warn!( + "loaded theme `{}` but cannot use it because true color \ + support is not enabled", + theme.name() + ); + } + colors_ok + }) + }) + } + .unwrap_or_else(|| editor.theme_loader.default_theme(true_color)); + editor.set_theme(theme); } diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs index 090c1192feb9..a7a80d16b0c0 100644 --- a/helix-term/src/args.rs +++ b/helix-term/src/args.rs @@ -17,6 +17,7 @@ pub struct Args { pub verbosity: u64, pub log_file: Option, pub config_file: Option, + pub theme_file: Option, pub files: IndexMap>, pub working_directory: Option, } @@ -70,6 +71,10 @@ impl Args { Some(path) => args.config_file = Some(path.into()), None => anyhow::bail!("--config must specify a path to read"), }, + "-t" | "--theme" => match argv.next().as_deref() { + Some(path) => args.theme_file = Some(path.into()), + None => anyhow::bail!("--theme must specify a path to read"), + }, "--log" => match argv.next().as_deref() { Some(path) => args.log_file = Some(path.into()), None => anyhow::bail!("--log must specify a path to write"), diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index bdca0c012f8d..41839bbf145c 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -42,6 +42,7 @@ async fn main_impl() -> Result { let args = Args::parse_args().context("could not parse arguments")?; helix_loader::initialize_config_file(args.config_file.clone()); + helix_loader::initialize_theme_file(args.theme_file.clone()); helix_loader::initialize_log_file(args.log_file.clone()); // Help has a higher priority and should be handled separately. @@ -68,6 +69,7 @@ FLAGS: the default is the same as 'all', but with languages filtering. -g, --grammar {{fetch|build}} Fetch or builds tree-sitter grammars listed in languages.toml -c, --config Specify a file to use for configuration + -t, --theme Specify a theme file to use -v Increase logging verbosity each use for up to 3 times --log Specify a file to use for logging (default file: {}) diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 173a40f3f9f9..e5d56a9f8679 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -131,6 +131,59 @@ impl Loader { Ok(theme) } + /// Loads a theme from a direct file path. + pub fn load_from_file(&self, path: &std::path::Path) -> Result { + let (theme, warnings) = self.load_from_file_with_warnings(path)?; + + for warning in &warnings { + warn!("Theme '{}': {}", path.display(), warning); + } + + Ok(theme) + } + + /// Loads a theme from a direct file path, returning any warnings + pub fn load_from_file_with_warnings( + &self, + path: &std::path::Path, + ) -> Result<(Theme, Vec)> { + let mut visited_paths = HashSet::new(); + visited_paths.insert(path.to_path_buf()); + + let theme_toml = self.load_toml(path.to_path_buf())?; + let inherits = theme_toml.get("inherits"); + + let theme_toml = if let Some(parent_theme_name) = inherits { + let parent_theme_name = parent_theme_name.as_str().ok_or_else(|| { + anyhow!("Expected 'inherits' to be a string: {}", parent_theme_name) + })?; + + let parent_theme_toml = match parent_theme_name { + "default" => DEFAULT_THEME_DATA.clone(), + "base16_default" => BASE16_DEFAULT_THEME_DATA.clone(), + _ => self.load_theme(parent_theme_name, &mut visited_paths)?, + }; + + self.merge_themes(parent_theme_toml, theme_toml) + } else { + theme_toml + }; + + let (theme, warnings) = Theme::from_toml(theme_toml); + + let theme_name = path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("unknown"); + + let theme = Theme { + name: theme_name.into(), + ..theme + }; + + Ok((theme, warnings)) + } + /// Loads a theme searching directories in priority order, returning any warnings pub fn load_with_warnings(&self, name: &str) -> Result<(Theme, Vec)> { if name == "default" {