From 1423dd944066f060aa814817b2867097f2bac4d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Tue, 16 Apr 2024 14:43:16 +0200 Subject: [PATCH 01/28] Choose theme based on the terminal's color scheme --- src/lib.rs | 1 + src/theme.rs | 334 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 335 insertions(+) create mode 100644 src/theme.rs diff --git a/src/lib.rs b/src/lib.rs index 23c4a800..5699d87a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,6 +49,7 @@ pub(crate) mod printer; pub mod style; pub(crate) mod syntax_mapping; mod terminal; +pub mod theme; mod vscreen; pub(crate) mod wrapping; diff --git a/src/theme.rs b/src/theme.rs new file mode 100644 index 00000000..b1607140 --- /dev/null +++ b/src/theme.rs @@ -0,0 +1,334 @@ +use std::convert::Infallible; +use std::str::FromStr; + +/// Chooses an appropriate theme or falls back to a default theme +/// based on the user-provided options and the color scheme of the terminal. +pub fn theme(options: ThemeOptions, detector: &dyn ColorSchemeDetector) -> String { + // Implementation note: This function is mostly pure (i.e. it has no side effects) for the sake of testing. + // All the side effects (e.g. querying the terminal for its colors) are performed in the detector. + if let Some(theme) = options.theme { + theme.into_theme(ColorScheme::default()) + } else { + let color_scheme = detect(options.detect_color_scheme, detector).unwrap_or_default(); + choose_theme(options, color_scheme) + .map(|t| t.into_theme(color_scheme)) + .unwrap_or_else(|| default_theme(color_scheme).to_owned()) + } +} + +fn choose_theme(options: ThemeOptions, color_scheme: ColorScheme) -> Option { + match color_scheme { + ColorScheme::Dark => options.dark_theme, + ColorScheme::Light => options.light_theme, + } +} + +fn detect(when: DetectColorScheme, detector: &dyn ColorSchemeDetector) -> Option { + let should_detect = match when { + DetectColorScheme::Auto => detector.should_detect(), + DetectColorScheme::Always => true, + DetectColorScheme::Never => false, + }; + should_detect.then(|| detector.detect()).flatten() +} + +const fn default_theme(color_scheme: ColorScheme) -> &'static str { + match color_scheme { + ColorScheme::Dark => "Monokai Extended", + ColorScheme::Light => "Monokai Extended Light", + } +} + +/// Options for configuring the theme used for syntax highlighting. +#[derive(Debug, Default)] +pub struct ThemeOptions { + /// Always use this theme regardless of the terminal's background color. + pub theme: Option, + /// The theme to use in case the terminal uses a dark background with light text. + pub dark_theme: Option, + /// The theme to use in case the terminal uses a light background with dark text. + pub light_theme: Option, + /// Detect whether or not the terminal is dark or light by querying for its colors. + pub detect_color_scheme: DetectColorScheme, +} + +/// The name of a theme or the default theme. +#[derive(Debug)] +pub enum ThemeRequest { + Named(String), + Default, +} + +impl FromStr for ThemeRequest { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + if s == "default" { + Ok(ThemeRequest::Default) + } else { + Ok(ThemeRequest::Named(s.to_owned())) + } + } +} + +impl ThemeRequest { + fn into_theme(self, color_scheme: ColorScheme) -> String { + match self { + ThemeRequest::Named(t) => t, + ThemeRequest::Default => default_theme(color_scheme).to_owned(), + } + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum DetectColorScheme { + /// Only query the terminal for its colors when appropriate (e.g. when the the output is not redirected). + #[default] + Auto, + /// Always query the terminal for its colors. + Always, + /// Never query the terminal for its colors. + Never, +} + +/// The color scheme used to pick a fitting theme. Defaults to [`ColorScheme::Dark`]. +#[derive(Default, Copy, Clone)] +pub enum ColorScheme { + #[default] + Dark, + Light, +} + +pub trait ColorSchemeDetector { + fn should_detect(&self) -> bool; + + fn detect(&self) -> Option; +} + +#[cfg(test)] +impl ColorSchemeDetector for Option { + fn should_detect(&self) -> bool { + true + } + + fn detect(&self) -> Option { + *self + } +} + +#[cfg(test)] +mod tests { + use super::ColorScheme::*; + use super::DetectColorScheme::*; + use super::*; + use std::cell::Cell; + use std::iter; + + mod color_scheme_detection { + use super::*; + + #[test] + fn not_called_for_never() { + let detector = DetectorStub::should_detect(Some(Dark)); + let options = ThemeOptions { + detect_color_scheme: Never, + ..Default::default() + }; + _ = theme(options, &detector); + assert!(!detector.was_called.get()); + } + + #[test] + fn called_for_always() { + let detectors = [ + DetectorStub::should_detect(Some(Dark)), + DetectorStub::should_not_detect(), + ]; + for detector in detectors { + let options = ThemeOptions { + detect_color_scheme: Always, + ..Default::default() + }; + _ = theme(options, &detector); + assert!(detector.was_called.get()); + } + } + + #[test] + fn called_for_auto_if_should_detect() { + let detector = DetectorStub::should_detect(Some(Dark)); + _ = theme(ThemeOptions::default(), &detector); + assert!(detector.was_called.get()); + } + + #[test] + fn not_called_for_auto_if_not_should_detect() { + let detector = DetectorStub::should_not_detect(); + _ = theme(ThemeOptions::default(), &detector); + assert!(!detector.was_called.get()); + } + } + + mod precedence { + use super::*; + + #[test] + fn theme_is_preferred_over_light_or_dark_themes() { + for color_scheme in optional(color_schemes()) { + for options in [ + ThemeOptions { + theme: Some(ThemeRequest::Named("Theme".to_string())), + ..Default::default() + }, + ThemeOptions { + theme: Some(ThemeRequest::Named("Theme".to_string())), + dark_theme: Some(ThemeRequest::Named("Dark Theme".to_string())), + light_theme: Some(ThemeRequest::Named("Light Theme".to_string())), + ..Default::default() + }, + ] { + let detector = ConstantDetector(color_scheme); + assert_eq!("Theme", theme(options, &detector)); + } + } + } + + #[test] + fn detector_is_not_called_if_theme_is_present() { + let options = ThemeOptions { + theme: Some(ThemeRequest::Named("Theme".to_string())), + ..Default::default() + }; + let detector = DetectorStub::should_detect(Some(Dark)); + _ = theme(options, &detector); + assert!(!detector.was_called.get()); + } + } + + mod default_theme { + use super::*; + + #[test] + fn dark_if_unable_to_detect_color_scheme() { + let detector = ConstantDetector(None); + assert_eq!( + default_theme(ColorScheme::Dark), + theme(ThemeOptions::default(), &detector) + ); + } + + // For backwards compatibility, if the default theme is requested + // explicitly through BAT_THEME, we always pick the default dark theme. + #[test] + fn dark_if_requested_explicitly_through_theme() { + for color_scheme in optional(color_schemes()) { + let options = ThemeOptions { + theme: Some(ThemeRequest::Default), + ..Default::default() + }; + let detector = ConstantDetector(color_scheme); + assert_eq!(default_theme(ColorScheme::Dark), theme(options, &detector)); + } + } + + #[test] + fn varies_depending_on_color_scheme() { + for color_scheme in color_schemes() { + for options in [ + ThemeOptions::default(), + ThemeOptions { + dark_theme: Some(ThemeRequest::Default), + light_theme: Some(ThemeRequest::Default), + ..Default::default() + }, + ] { + let detector = ConstantDetector(Some(color_scheme)); + assert_eq!(default_theme(color_scheme), theme(options, &detector)); + } + } + } + } + + mod choosing { + use super::*; + + #[test] + fn chooses_dark_theme_if_dark_or_unknown() { + for color_scheme in [Some(Dark), None] { + let options = ThemeOptions { + dark_theme: Some(ThemeRequest::Named("Dark".to_string())), + light_theme: Some(ThemeRequest::Named("Light".to_string())), + ..Default::default() + }; + let detector = ConstantDetector(color_scheme); + assert_eq!("Dark", theme(options, &detector)); + } + } + + #[test] + fn chooses_light_theme_if_light() { + let options = ThemeOptions { + dark_theme: Some(ThemeRequest::Named("Dark".to_string())), + light_theme: Some(ThemeRequest::Named("Light".to_string())), + ..Default::default() + }; + let detector = ConstantDetector(Some(ColorScheme::Light)); + assert_eq!("Light", theme(options, &detector)); + } + } + + struct DetectorStub { + should_detect: bool, + color_scheme: Option, + was_called: Cell, + } + + impl DetectorStub { + fn should_detect(color_scheme: Option) -> Self { + DetectorStub { + should_detect: true, + color_scheme, + was_called: Cell::default(), + } + } + + fn should_not_detect() -> Self { + DetectorStub { + should_detect: false, + color_scheme: None, + was_called: Cell::default(), + } + } + } + + impl ColorSchemeDetector for DetectorStub { + fn should_detect(&self) -> bool { + self.should_detect + } + + fn detect(&self) -> Option { + self.was_called.set(true); + self.color_scheme + } + } + + struct ConstantDetector(Option); + + impl ColorSchemeDetector for ConstantDetector { + fn should_detect(&self) -> bool { + true + } + + fn detect(&self) -> Option { + self.0 + } + } + + fn optional(value: impl Iterator) -> impl Iterator> { + value.map(Some).chain(iter::once(None)) + } + + fn color_schemes() -> impl Iterator { + [Dark, Light].into_iter() + } +} From de796392cfba07a86d48ca9099e053b176ac916b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Tue, 16 Apr 2024 14:43:18 +0200 Subject: [PATCH 02/28] Deprecate old `default_theme` function --- src/assets.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/assets.rs b/src/assets.rs index 9655553d..53414366 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -90,6 +90,7 @@ impl HighlightingAssets { /// /// See and /// for more context. + #[deprecated(note = "use bat::theme::theme instead")] pub fn default_theme() -> &'static str { #[cfg(not(target_os = "macos"))] { From cda363a3f742fe4ef112ffcd4a61bb756332c16c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Tue, 16 Apr 2024 14:43:20 +0200 Subject: [PATCH 03/28] Use `default_theme()` function from theme module --- src/assets.rs | 27 ++++++++------------------- src/theme.rs | 2 +- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/src/assets.rs b/src/assets.rs index 53414366..857f416b 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -13,6 +13,7 @@ use crate::error::*; use crate::input::{InputReader, OpenedInput}; use crate::syntax_mapping::ignored_suffixes::IgnoredSuffixes; use crate::syntax_mapping::MappingTarget; +use crate::theme::{default_theme, ColorScheme}; use crate::{bat_warning, SyntaxMapping}; use lazy_theme_set::LazyThemeSet; @@ -94,33 +95,18 @@ impl HighlightingAssets { pub fn default_theme() -> &'static str { #[cfg(not(target_os = "macos"))] { - Self::default_dark_theme() + default_theme(ColorScheme::Dark) } #[cfg(target_os = "macos")] { if macos_dark_mode_active() { - Self::default_dark_theme() + default_theme(ColorScheme::Dark) } else { - Self::default_light_theme() + default_theme(ColorScheme::Light) } } } - /** - * The default theme that looks good on a dark background. - */ - fn default_dark_theme() -> &'static str { - "Monokai Extended" - } - - /** - * The default theme that looks good on a light background. - */ - #[cfg(target_os = "macos")] - fn default_light_theme() -> &'static str { - "Monokai Extended Light" - } - pub fn from_cache(cache_path: &Path) -> Result { Ok(HighlightingAssets::new( SerializedSyntaxSet::FromFile(cache_path.join("syntaxes.bin")), @@ -249,7 +235,10 @@ impl HighlightingAssets { bat_warning!("Unknown theme '{}', using default.", theme) } self.get_theme_set() - .get(self.fallback_theme.unwrap_or_else(Self::default_theme)) + .get( + self.fallback_theme + .unwrap_or_else(|| default_theme(ColorScheme::Dark)), + ) .expect("something is very wrong if the default theme is missing") } } diff --git a/src/theme.rs b/src/theme.rs index b1607140..8ce75e1f 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -32,7 +32,7 @@ fn detect(when: DetectColorScheme, detector: &dyn ColorSchemeDetector) -> Option should_detect.then(|| detector.detect()).flatten() } -const fn default_theme(color_scheme: ColorScheme) -> &'static str { +pub(crate) const fn default_theme(color_scheme: ColorScheme) -> &'static str { match color_scheme { ColorScheme::Dark => "Monokai Extended", ColorScheme::Light => "Monokai Extended Light", From cea45e05f3e3f2fea3eed6cbd8e2ac4bd1036fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Thu, 18 Jul 2024 15:38:28 +0200 Subject: [PATCH 04/28] Expose new theme selection in CLI --- Cargo.lock | 51 +++++++++++++++++++++++-- Cargo.toml | 3 ++ doc/long-help.txt | 36 +++++++++++++++-- doc/short-help.txt | 6 +++ src/bin/bat/app.rs | 85 ++++++++++++++++++++++++++++++++++------- src/bin/bat/clap_app.rs | 53 ++++++++++++++++++++++++- src/bin/bat/config.rs | 2 + src/theme.rs | 24 ++++++------ 8 files changed, 227 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1168501a..6d413de6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -149,6 +149,7 @@ dependencies = [ "shell-words", "syntect", "tempfile", + "terminal-colorsaurus", "thiserror", "toml", "unicode-width", @@ -620,6 +621,12 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "home" version = "0.5.9" @@ -688,9 +695,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.149" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libgit2-sys" @@ -755,9 +762,9 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "miniz_oxide" @@ -768,6 +775,18 @@ dependencies = [ "adler", ] +[[package]] +name = "mio" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4929e1f84c5e54c3ec6141cd5d8b5a5c055f031f80cf78f2072920173cb4d880" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + [[package]] name = "nix" version = "0.26.4" @@ -1309,6 +1328,30 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal-colorsaurus" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f99bb1dc5cde9eada5a8f466641240f9d5b9f55291d675df4160b097fbfa42e" +dependencies = [ + "cfg-if", + "libc", + "memchr", + "mio", + "terminal-trx", +] + +[[package]] +name = "terminal-trx" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d4c86910e10c782a02d3b7606de43cf7ebd80e1fafdca8e49a0db2b0d4611f0" +dependencies = [ + "cfg-if", + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "terminal_size" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 720e629b..affce17f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ application = [ "bugreport", "build-assets", "git", + "detect-color-scheme", "minimal-application", ] # Mainly for developers that want to iterate quickly @@ -35,6 +36,7 @@ git = ["git2"] # Support indicating git modifications paging = ["shell-words", "grep-cli"] # Support applying a pager on the output lessopen = ["run_script", "os_str_bytes"] # Support $LESSOPEN preprocessor build-assets = ["syntect/yaml-load", "syntect/plist-load", "regex", "walkdir"] +detect-color-scheme = ["dep:terminal-colorsaurus"] # You need to use one of these if you depend on bat as a library: regex-onig = ["syntect/regex-onig"] # Use the "oniguruma" regex engine @@ -68,6 +70,7 @@ bytesize = { version = "1.3.0" } encoding_rs = "0.8.34" os_str_bytes = { version = "~7.0", optional = true } run_script = { version = "^0.10.1", optional = true} +terminal-colorsaurus = { version = "0.4", optional = true } [dependencies.git2] version = "0.18" diff --git a/doc/long-help.txt b/doc/long-help.txt index 2b03490f..86e9a532 100644 --- a/doc/long-help.txt +++ b/doc/long-help.txt @@ -109,9 +109,39 @@ Options: 'bat --ignored-suffix ".dev" my_file.json.dev' will use JSON syntax, and ignore '.dev' --theme - Set the theme for syntax highlighting. Use '--list-themes' to see all available themes. To - set a default theme, add the '--theme="..."' option to the configuration file or export - the BAT_THEME environment variable (e.g.: export BAT_THEME="..."). + Set the theme for syntax highlighting. Note that this option overrides '--theme-dark' and + '--theme-light'. Use '--list-themes' to see all available themes. To set a default theme, + add the '--theme="..."' option to the configuration file or export the BAT_THEME + environment variable (e.g.: export BAT_THEME="..."). + + --detect-color-scheme + Specify when to query the terminal for its colors in order to pick an appropriate syntax + highlighting theme. Use '--theme-light' and '--theme-dark' (or the environment variables + BAT_THEME_LIGHT and BAT_THEME_DARK) to configure which themes are picked. You may also use + '--theme' to set a theme that is used regardless of the terminal's colors. + + Possible values: + * auto (default): + Only query the terminals colors if the output is not redirected. This is to prevent + race conditions with pagers such as less. + * never + Never query the terminal for its colors and assume that the terminal has a dark + background. + * always + Always query the terminal for its colors, regardless of whether or not the output is + redirected. + + --theme-light + Sets the theme name for syntax highlighting used when the terminal uses a light + background. Use '--list-themes' to see all available themes. To set a default theme, add + the '--theme-light="..." option to the configuration file or export the BAT_THEME_LIGHT + environment variable (e.g. export BAT_THEME_LIGHT="..."). + + --theme-dark + Sets the theme name for syntax highlighting used when the terminal uses a dark background. + Use '--list-themes' to see all available themes. To set a default theme, add the + '--theme-dark="..." option to the configuration file or export the BAT_THEME_DARK + environment variable (e.g. export BAT_THEME_DARK="..."). --list-themes Display a list of supported themes for syntax highlighting. diff --git a/doc/short-help.txt b/doc/short-help.txt index 305bbf3d..3e369229 100644 --- a/doc/short-help.txt +++ b/doc/short-help.txt @@ -41,6 +41,12 @@ Options: Use the specified syntax for files matching the glob pattern ('*.cpp:C++'). --theme Set the color theme for syntax highlighting. + --detect-color-scheme + Specify when to query the terminal for its colors. + --theme-light + Sets the color theme for syntax highlighting used for light backgrounds. + --theme-dark + Sets the color theme for syntax highlighting used for dark backgrounds. --list-themes Display all supported highlighting themes. -s, --squeeze-blank diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index d6628668..d9e1662b 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -9,6 +9,9 @@ use crate::{ config::{get_args_from_config_file, get_args_from_env_opts_var, get_args_from_env_vars}, }; use bat::style::StyleComponentList; +use bat::theme::{ + theme, ColorScheme, ColorSchemeDetector, DetectColorScheme, ThemeOptions, ThemeRequest, +}; use bat::StripAnsiMode; use clap::ArgMatches; @@ -16,7 +19,6 @@ use console::Term; use crate::input::{new_file_input, new_stdin_input}; use bat::{ - assets::HighlightingAssets, bat_warning, config::{Config, VisibleLines}, error::*, @@ -254,18 +256,7 @@ impl App { Some("auto") => StripAnsiMode::Auto, _ => unreachable!("other values for --strip-ansi are not allowed"), }, - theme: self - .matches - .get_one::("theme") - .map(String::from) - .map(|s| { - if s == "default" { - String::from(HighlightingAssets::default_theme()) - } else { - s - } - }) - .unwrap_or_else(|| String::from(HighlightingAssets::default_theme())), + theme: theme(self.theme_options(), &TerminalColorSchemeDetector), visible_lines: match self.matches.try_contains_id("diff").unwrap_or_default() && self.matches.get_flag("diff") { @@ -424,4 +415,72 @@ impl App { Ok(styled_components) } + + fn theme_options(&self) -> ThemeOptions { + let theme = self + .matches + .get_one::("theme") + .map(|t| ThemeRequest::from_str(t).unwrap()); + let theme_dark = self + .matches + .get_one::("theme-dark") + .map(|t| ThemeRequest::from_str(t).unwrap()); + let theme_light = self + .matches + .get_one::("theme-light") + .map(|t| ThemeRequest::from_str(t).unwrap()); + let detect_color_scheme = match self + .matches + .get_one::("detect-color-scheme") + .map(|s| s.as_str()) + { + Some("auto") => DetectColorScheme::Auto, + Some("never") => DetectColorScheme::Never, + Some("always") => DetectColorScheme::Always, + _ => unreachable!("other values for --detect-color-scheme are not allowed"), + }; + ThemeOptions { + theme, + theme_dark, + theme_light, + detect_color_scheme, + } + } +} + +struct TerminalColorSchemeDetector; + +#[cfg(feature = "detect-color-scheme")] +impl ColorSchemeDetector for TerminalColorSchemeDetector { + fn should_detect(&self) -> bool { + // Querying the terminal for its colors via OSC 10 / OSC 11 requires "exclusive" access + // since we read/write from the terminal and enable/disable raw mode. + // This causes race conditions with pagers such as less when they are attached to the + // same terminal as us. + // + // This is usually only an issue when the output is manually piped to a pager. + // For example: `bat Cargo.toml | less`. + // Otherwise, if we start the pager ourselves, then there's no race condition + // since the pager is started *after* the color is detected. + std::io::stdout().is_terminal() + } + + fn detect(&self) -> Option { + use terminal_colorsaurus::{color_scheme, ColorScheme as ColorsaurusScheme, QueryOptions}; + match color_scheme(QueryOptions::default()).ok()? { + ColorsaurusScheme::Dark => Some(ColorScheme::Dark), + ColorsaurusScheme::Light => Some(ColorScheme::Light), + } + } +} + +#[cfg(not(feature = "detect-color-scheme"))] +impl ColorSchemeDetector for TerminalColorSchemeDetector { + fn should_detect(&self) -> bool { + false + } + + fn detect(&self) -> Option { + None + } } diff --git a/src/bin/bat/clap_app.rs b/src/bin/bat/clap_app.rs index 33dde980..6abdab9c 100644 --- a/src/bin/bat/clap_app.rs +++ b/src/bin/bat/clap_app.rs @@ -375,13 +375,64 @@ pub fn build_app(interactive_output: bool) -> Command { .overrides_with("theme") .help("Set the color theme for syntax highlighting.") .long_help( - "Set the theme for syntax highlighting. Use '--list-themes' to \ + "Set the theme for syntax highlighting. Note that this option overrides \ + '--theme-dark' and '--theme-light'. Use '--list-themes' to \ see all available themes. To set a default theme, add the \ '--theme=\"...\"' option to the configuration file or export the \ BAT_THEME environment variable (e.g.: export \ BAT_THEME=\"...\").", ), ) + .arg( + Arg::new("detect-color-scheme") + .long("detect-color-scheme") + .overrides_with("detect-color-scheme") + .value_name("when") + .value_parser(["auto", "never", "always"]) + .default_value("auto") + .hide_default_value(true) + .help("Specify when to query the terminal for its colors.") + .long_help( + "Specify when to query the terminal for its colors \ + in order to pick an appropriate syntax highlighting theme. \ + Use '--theme-light' and '--theme-dark' (or the environment variables \ + BAT_THEME_LIGHT and BAT_THEME_DARK) to configure which themes are picked. \ + You may also use '--theme' to set a theme that is used regardless of the terminal's colors.\n\n\ + Possible values:\n\ + * auto (default):\n \ + Only query the terminals colors if the output is not redirected. \ + This is to prevent race conditions with pagers such as less.\n\ + * never\n \ + Never query the terminal for its colors \ + and assume that the terminal has a dark background.\n\ + * always\n \ + Always query the terminal for its colors, \ + regardless of whether or not the output is redirected."), + ) + .arg( + Arg::new("theme-light") + .long("theme-light") + .overrides_with("theme-light") + .value_name("theme") + .help("Sets the color theme for syntax highlighting used for light backgrounds.") + .long_help( + "Sets the theme name for syntax highlighting used when the terminal uses a light background. \ + Use '--list-themes' to see all available themes. To set a default theme, add the \ + '--theme-light=\"...\" option to the configuration file or export the BAT_THEME_LIGHT \ + environment variable (e.g. export BAT_THEME_LIGHT=\"...\")."), + ) + .arg( + Arg::new("theme-dark") + .long("theme-dark") + .overrides_with("theme-dark") + .value_name("theme") + .help("Sets the color theme for syntax highlighting used for dark backgrounds.") + .long_help( + "Sets the theme name for syntax highlighting used when the terminal uses a dark background. \ + Use '--list-themes' to see all available themes. To set a default theme, add the \ + '--theme-dark=\"...\" option to the configuration file or export the BAT_THEME_DARK \ + environment variable (e.g. export BAT_THEME_DARK=\"...\")."), + ) .arg( Arg::new("list-themes") .long("list-themes") diff --git a/src/bin/bat/config.rs b/src/bin/bat/config.rs index 6fa18f09..f1ec3d53 100644 --- a/src/bin/bat/config.rs +++ b/src/bin/bat/config.rs @@ -141,6 +141,8 @@ pub fn get_args_from_env_vars() -> Vec { [ ("--tabs", "BAT_TABS"), ("--theme", "BAT_THEME"), + ("--theme-dark", "BAT_THEME_DARK"), + ("--theme-light", "BAT_THEME_LIGHT"), ("--pager", "BAT_PAGER"), ("--paging", "BAT_PAGING"), ("--style", "BAT_STYLE"), diff --git a/src/theme.rs b/src/theme.rs index 8ce75e1f..ec3e3d34 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -18,8 +18,8 @@ pub fn theme(options: ThemeOptions, detector: &dyn ColorSchemeDetector) -> Strin fn choose_theme(options: ThemeOptions, color_scheme: ColorScheme) -> Option { match color_scheme { - ColorScheme::Dark => options.dark_theme, - ColorScheme::Light => options.light_theme, + ColorScheme::Dark => options.theme_dark, + ColorScheme::Light => options.theme_light, } } @@ -45,9 +45,9 @@ pub struct ThemeOptions { /// Always use this theme regardless of the terminal's background color. pub theme: Option, /// The theme to use in case the terminal uses a dark background with light text. - pub dark_theme: Option, + pub theme_dark: Option, /// The theme to use in case the terminal uses a light background with dark text. - pub light_theme: Option, + pub theme_light: Option, /// Detect whether or not the terminal is dark or light by querying for its colors. pub detect_color_scheme: DetectColorScheme, } @@ -182,8 +182,8 @@ mod tests { }, ThemeOptions { theme: Some(ThemeRequest::Named("Theme".to_string())), - dark_theme: Some(ThemeRequest::Named("Dark Theme".to_string())), - light_theme: Some(ThemeRequest::Named("Light Theme".to_string())), + theme_dark: Some(ThemeRequest::Named("Dark Theme".to_string())), + theme_light: Some(ThemeRequest::Named("Light Theme".to_string())), ..Default::default() }, ] { @@ -237,8 +237,8 @@ mod tests { for options in [ ThemeOptions::default(), ThemeOptions { - dark_theme: Some(ThemeRequest::Default), - light_theme: Some(ThemeRequest::Default), + theme_dark: Some(ThemeRequest::Default), + theme_light: Some(ThemeRequest::Default), ..Default::default() }, ] { @@ -256,8 +256,8 @@ mod tests { fn chooses_dark_theme_if_dark_or_unknown() { for color_scheme in [Some(Dark), None] { let options = ThemeOptions { - dark_theme: Some(ThemeRequest::Named("Dark".to_string())), - light_theme: Some(ThemeRequest::Named("Light".to_string())), + theme_dark: Some(ThemeRequest::Named("Dark".to_string())), + theme_light: Some(ThemeRequest::Named("Light".to_string())), ..Default::default() }; let detector = ConstantDetector(color_scheme); @@ -268,8 +268,8 @@ mod tests { #[test] fn chooses_light_theme_if_light() { let options = ThemeOptions { - dark_theme: Some(ThemeRequest::Named("Dark".to_string())), - light_theme: Some(ThemeRequest::Named("Light".to_string())), + theme_dark: Some(ThemeRequest::Named("Dark".to_string())), + theme_light: Some(ThemeRequest::Named("Light".to_string())), ..Default::default() }; let detector = ConstantDetector(Some(ColorScheme::Light)); From 9a1bfe946dafcd095895737b7b395dd636114737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Tue, 16 Apr 2024 14:43:24 +0200 Subject: [PATCH 05/28] Update completions and man page --- assets/completions/_bat.ps1.in | 3 +++ assets/completions/bat.bash.in | 9 ++++++-- assets/completions/bat.fish.in | 7 +++++++ assets/completions/bat.zsh.in | 3 +++ assets/manual/bat.1.in | 38 +++++++++++++++++++++++++++++++--- 5 files changed, 55 insertions(+), 5 deletions(-) diff --git a/assets/completions/_bat.ps1.in b/assets/completions/_bat.ps1.in index c0c151e1..5635dea2 100644 --- a/assets/completions/_bat.ps1.in +++ b/assets/completions/_bat.ps1.in @@ -32,11 +32,14 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script [CompletionResult]::new('--color', 'color', [CompletionResultType]::ParameterName, 'When to use colors (*auto*, never, always).') [CompletionResult]::new('--italic-text', 'italic-text', [CompletionResultType]::ParameterName, 'Use italics in output (always, *never*)') [CompletionResult]::new('--decorations', 'decorations', [CompletionResultType]::ParameterName, 'When to show the decorations (*auto*, never, always).') + [CompletionResult]::new('--detect-color-scheme', 'detect-color-scheme', [CompletionResultType]::ParameterName, 'When to detect the terminal''s color scheme (*auto*, never, always).') [CompletionResult]::new('--paging', 'paging', [CompletionResultType]::ParameterName, 'Specify when to use the pager, or use `-P` to disable (*auto*, never, always).') [CompletionResult]::new('--pager', 'pager', [CompletionResultType]::ParameterName, 'Determine which pager to use.') [CompletionResult]::new('-m', 'm', [CompletionResultType]::ParameterName, 'Use the specified syntax for files matching the glob pattern (''*.cpp:C++'').') [CompletionResult]::new('--map-syntax', 'map-syntax', [CompletionResultType]::ParameterName, 'Use the specified syntax for files matching the glob pattern (''*.cpp:C++'').') [CompletionResult]::new('--theme', 'theme', [CompletionResultType]::ParameterName, 'Set the color theme for syntax highlighting.') + [CompletionResult]::new('--theme-dark', 'theme', [CompletionResultType]::ParameterName, 'Set the color theme for syntax highlighting for dark backgrounds.') + [CompletionResult]::new('--theme-light', 'theme', [CompletionResultType]::ParameterName, 'Set the color theme for syntax highlighting for light backgrounds.') [CompletionResult]::new('--style', 'style', [CompletionResultType]::ParameterName, 'Comma-separated list of style elements to display (*default*, auto, full, plain, changes, header, header-filename, header-filesize, grid, rule, numbers, snip).') [CompletionResult]::new('-r', 'r', [CompletionResultType]::ParameterName, 'Only print the lines from N to M.') [CompletionResult]::new('--line-range', 'line-range', [CompletionResultType]::ParameterName, 'Only print the lines from N to M.') diff --git a/assets/completions/bat.bash.in b/assets/completions/bat.bash.in index f314bb25..a02c5a04 100644 --- a/assets/completions/bat.bash.in +++ b/assets/completions/bat.bash.in @@ -100,7 +100,7 @@ _bat() { COMPREPLY=($(compgen -W "auto never character" -- "$cur")) return 0 ;; - --color | --decorations | --paging) + --color | --decorations | --paging | --detect-color-scheme) COMPREPLY=($(compgen -W "auto never always" -- "$cur")) return 0 ;; @@ -112,7 +112,9 @@ _bat() { COMPREPLY=($(compgen -c -- "$cur")) return 0 ;; - --theme) + --theme | \ + --theme-dark | \ + --theme-light) local IFS=$'\n' COMPREPLY=($(compgen -W "$("$1" --list-themes)" -- "$cur")) __bat_escape_completions @@ -164,12 +166,15 @@ _bat() { --color --italic-text --decorations + --detect-color-scheme --force-colorization --paging --pager --map-syntax --ignored-suffix --theme + --theme-dark + --theme-light --list-themes --squeeze-blank --squeeze-limit diff --git a/assets/completions/bat.fish.in b/assets/completions/bat.fish.in index 788f71b0..7bd0ebca 100644 --- a/assets/completions/bat.fish.in +++ b/assets/completions/bat.fish.in @@ -99,6 +99,7 @@ set -l color_opts ' ' set -l decorations_opts $color_opts set -l paging_opts $color_opts +set -l detect_color_scheme_opts $color_opts # Include some examples so we can indicate the default. set -l pager_opts ' @@ -143,6 +144,8 @@ complete -c $bat -l config-file -f -d "Display location of configuration file" - complete -c $bat -l decorations -x -a "$decorations_opts" -d "When to use --style decorations" -n __bat_no_excl_args +complete -c $bat -l detect-color-scheme -x -a "$detect_color_scheme_opts" -d "When to detect the terminal's color scheme" -n __bat_no_excl_args + complete -c $bat -l diagnostic -d "Print diagnostic info for bug reports" -n __fish_is_first_arg complete -c $bat -s d -l diff -d "Only show lines with Git changes" -n __bat_no_excl_args @@ -205,6 +208,10 @@ complete -c $bat -l terminal-width -x -d "Set terminal , +, or -< complete -c $bat -l theme -x -a "(command $bat --list-themes | command cat)" -d "Set the syntax highlighting theme" -n __bat_no_excl_args +complete -c $bat -l theme-dark -x -a "(command $bat --list-themes | command cat)" -d "Set the syntax highlighting theme for dark backgrounds" -n __bat_no_excl_args + +complete -c $bat -l theme-light -x -a "(command $bat --list-themes | command cat)" -d "Set the syntax highlighting theme for light backgrounds" -n __bat_no_excl_args + complete -c $bat -s V -l version -f -d "Show version information" -n __fish_is_first_arg complete -c $bat -l wrap -x -a "$wrap_opts" -d "Text-wrapping mode" -n __bat_no_excl_args diff --git a/assets/completions/bat.zsh.in b/assets/completions/bat.zsh.in index 7d03abb3..4a598437 100644 --- a/assets/completions/bat.zsh.in +++ b/assets/completions/bat.zsh.in @@ -40,9 +40,12 @@ _{{PROJECT_EXECUTABLE}}_main() { --color='[specify when to use colors]:when:(auto never always)' --italic-text='[use italics in output]:when:(always never)' --decorations='[specify when to show the decorations]:when:(auto never always)' + --detect-color-scheme="[specify when to detect the terminal's color scheme]:when:(auto never always)" --paging='[specify when to use the pager]:when:(auto never always)' '(-m --map-syntax)'{-m+,--map-syntax=}'[map a glob pattern to an existing syntax name]: :->syntax-maps' '(--theme)'--theme='[set the color theme for syntax highlighting]:theme:->themes' + '(--theme-dark)'--theme-dark='[set the color theme for syntax highlighting for dark backgrounds]:theme:->themes' + '(--theme-light)'--theme-light='[set the color theme for syntax highlighting for light backgrounds]:theme:->themes' '(: --list-themes --list-languages -L)'--list-themes'[show all supported highlighting themes]' --style='[comma-separated list of style elements to display]: : _values "style [default]" default auto full plain changes header header-filename header-filesize grid rule numbers snip' diff --git a/assets/manual/bat.1.in b/assets/manual/bat.1.in index 2bc0a3a5..5dcabe2d 100644 --- a/assets/manual/bat.1.in +++ b/assets/manual/bat.1.in @@ -117,6 +117,24 @@ Specify when to use the decorations that have been specified via '\-\-style'. Th automatic mode only enables decorations if an interactive terminal is detected. Possible values: *auto*, never, always. .HP +\fB\-\-detect\-color\-scheme\fR +.IP +Specify when to query the terminal for its colors in order to pick an appropriate syntax +highlighting theme. Use \fB\-\-theme-light\fP and \fB\-\-theme-dark\fP (or the environment variables +\fBBAT_THEME_LIGHT\fP and \fBBAT_THEME_DARK\fP) to configure which themes are picked. You can also use +\fP\-\-theme\fP to set a theme that is used regardless of the terminal's colors. +.IP +\fI\fP can be one of: +.RS +.IP "\fBauto\fP" +Only query the terminals colors if the output is not redirected. This is to prevent +race conditions with pagers such as less. +.IP "never" +Never query the terminal for its colors and assume that the terminal has a dark background. +.IP "always" +Always query the terminal for its colors, regardless of whether or not the output is redirected. +.RE +.HP \fB\-f\fR, \fB\-\-force\-colorization\fR .IP Alias for '--decorations=always --color=always'. This is useful \ @@ -152,9 +170,23 @@ will use JSON syntax, and ignore '.dev' .HP \fB\-\-theme\fR .IP -Set the theme for syntax highlighting. Use '\-\-list\-themes' to see all available themes. -To set a default theme, add the '\-\-theme="..."' option to the configuration file or -export the BAT_THEME environment variable (e.g.: export BAT_THEME="..."). +Set the theme for syntax highlighting. Use \fB\-\-list\-themes\fP to see all available themes. +To set a default theme, add the \fB\-\-theme="..."\fP option to the configuration file or +export the \fBBAT_THEME\fP environment variable (e.g.: \fBexport BAT_THEME="..."\fP). +.HP +\fB\-\-theme\-dark\fR +.IP +Sets the theme name for syntax highlighting used when the terminal uses a dark background. +To set a default theme, add the \fB\-\-theme-dark="..."\fP option to the configuration file or +export the \fBBAT_THEME_DARK\fP environment variable (e.g. \fBexport BAT_THEME_DARK="..."\fP). +This option is ignored if \fB\-\-theme\fP option is set. +.HP +\fB\-\-theme\-light\fR +.IP +Sets the theme name for syntax highlighting used when the terminal uses a dark background. +To set a default theme, add the \fB\-\-theme-dark="..."\fP option to the configuration file or +export the \fBBAT_THEME_LIGHT\fP environment variable (e.g. \fBexport BAT_THEME_LIGHT="..."\fP). +This option is ignored if \fB\-\-theme\fP option is set. .HP \fB\-\-list\-themes\fR .IP From 14ce668a1d043b9ecdbdf99e6832923ba3576031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Tue, 16 Apr 2024 14:43:27 +0200 Subject: [PATCH 06/28] Add generated powershell completion to ignore list --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a3ea8cff..fbfe6ac6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ **/*.rs.bk # Generated files +/assets/completions/_bat.ps1 /assets/completions/bat.bash /assets/completions/bat.fish /assets/completions/bat.zsh From ff81cfd584ca873fcad186193247af51c4f71627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Tue, 16 Apr 2024 14:43:31 +0200 Subject: [PATCH 07/28] Move actual detection into library --- Cargo.toml | 3 +- src/bin/bat/app.rs | 43 +-------------- src/theme.rs | 133 +++++++++++++++++++++++++++++++-------------- 3 files changed, 95 insertions(+), 84 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index affce17f..62ae693c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,14 +13,13 @@ edition = '2021' rust-version = "1.70" [features] -default = ["application"] +default = ["application", "detect-color-scheme"] # Feature required for bat the application. Should be disabled when depending on # bat as a library. application = [ "bugreport", "build-assets", "git", - "detect-color-scheme", "minimal-application", ] # Mainly for developers that want to iterate quickly diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index d9e1662b..711ab1c6 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -9,9 +9,7 @@ use crate::{ config::{get_args_from_config_file, get_args_from_env_opts_var, get_args_from_env_vars}, }; use bat::style::StyleComponentList; -use bat::theme::{ - theme, ColorScheme, ColorSchemeDetector, DetectColorScheme, ThemeOptions, ThemeRequest, -}; +use bat::theme::{theme, DetectColorScheme, ThemeOptions, ThemeRequest}; use bat::StripAnsiMode; use clap::ArgMatches; @@ -256,7 +254,7 @@ impl App { Some("auto") => StripAnsiMode::Auto, _ => unreachable!("other values for --strip-ansi are not allowed"), }, - theme: theme(self.theme_options(), &TerminalColorSchemeDetector), + theme: theme(self.theme_options()), visible_lines: match self.matches.try_contains_id("diff").unwrap_or_default() && self.matches.get_flag("diff") { @@ -447,40 +445,3 @@ impl App { } } } - -struct TerminalColorSchemeDetector; - -#[cfg(feature = "detect-color-scheme")] -impl ColorSchemeDetector for TerminalColorSchemeDetector { - fn should_detect(&self) -> bool { - // Querying the terminal for its colors via OSC 10 / OSC 11 requires "exclusive" access - // since we read/write from the terminal and enable/disable raw mode. - // This causes race conditions with pagers such as less when they are attached to the - // same terminal as us. - // - // This is usually only an issue when the output is manually piped to a pager. - // For example: `bat Cargo.toml | less`. - // Otherwise, if we start the pager ourselves, then there's no race condition - // since the pager is started *after* the color is detected. - std::io::stdout().is_terminal() - } - - fn detect(&self) -> Option { - use terminal_colorsaurus::{color_scheme, ColorScheme as ColorsaurusScheme, QueryOptions}; - match color_scheme(QueryOptions::default()).ok()? { - ColorsaurusScheme::Dark => Some(ColorScheme::Dark), - ColorsaurusScheme::Light => Some(ColorScheme::Light), - } - } -} - -#[cfg(not(feature = "detect-color-scheme"))] -impl ColorSchemeDetector for TerminalColorSchemeDetector { - fn should_detect(&self) -> bool { - false - } - - fn detect(&self) -> Option { - None - } -} diff --git a/src/theme.rs b/src/theme.rs index ec3e3d34..2491f9bd 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1,35 +1,13 @@ +//! Utilities for choosing an appropriate theme for syntax highlighting. + use std::convert::Infallible; +use std::io::IsTerminal as _; use std::str::FromStr; /// Chooses an appropriate theme or falls back to a default theme /// based on the user-provided options and the color scheme of the terminal. -pub fn theme(options: ThemeOptions, detector: &dyn ColorSchemeDetector) -> String { - // Implementation note: This function is mostly pure (i.e. it has no side effects) for the sake of testing. - // All the side effects (e.g. querying the terminal for its colors) are performed in the detector. - if let Some(theme) = options.theme { - theme.into_theme(ColorScheme::default()) - } else { - let color_scheme = detect(options.detect_color_scheme, detector).unwrap_or_default(); - choose_theme(options, color_scheme) - .map(|t| t.into_theme(color_scheme)) - .unwrap_or_else(|| default_theme(color_scheme).to_owned()) - } -} - -fn choose_theme(options: ThemeOptions, color_scheme: ColorScheme) -> Option { - match color_scheme { - ColorScheme::Dark => options.theme_dark, - ColorScheme::Light => options.theme_light, - } -} - -fn detect(when: DetectColorScheme, detector: &dyn ColorSchemeDetector) -> Option { - let should_detect = match when { - DetectColorScheme::Auto => detector.should_detect(), - DetectColorScheme::Always => true, - DetectColorScheme::Never => false, - }; - should_detect.then(|| detector.detect()).flatten() +pub fn theme(options: ThemeOptions) -> String { + theme_from_detector(options, &TerminalColorSchemeDetector) } pub(crate) const fn default_theme(color_scheme: ColorScheme) -> &'static str { @@ -40,6 +18,7 @@ pub(crate) const fn default_theme(color_scheme: ColorScheme) -> &'static str { } /// Options for configuring the theme used for syntax highlighting. +/// Used together with [`theme`]. #[derive(Debug, Default)] pub struct ThemeOptions { /// Always use this theme regardless of the terminal's background color. @@ -48,7 +27,7 @@ pub struct ThemeOptions { pub theme_dark: Option, /// The theme to use in case the terminal uses a light background with dark text. pub theme_light: Option, - /// Detect whether or not the terminal is dark or light by querying for its colors. + /// Whether or not to test if the terminal is dark or light by querying for its colors. pub detect_color_scheme: DetectColorScheme, } @@ -82,7 +61,7 @@ impl ThemeRequest { #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum DetectColorScheme { - /// Only query the terminal for its colors when appropriate (e.g. when the the output is not redirected). + /// Only query the terminal for its colors when appropriate (i.e. when the the output is not redirected). #[default] Auto, /// Always query the terminal for its colors. @@ -99,12 +78,78 @@ pub enum ColorScheme { Light, } -pub trait ColorSchemeDetector { +fn theme_from_detector(options: ThemeOptions, detector: &dyn ColorSchemeDetector) -> String { + // Implementation note: This function is mostly pure (i.e. it has no side effects) for the sake of testing. + // All the side effects (e.g. querying the terminal for its colors) are performed in the detector. + if let Some(theme) = options.theme { + theme.into_theme(ColorScheme::default()) + } else { + let color_scheme = detect(options.detect_color_scheme, detector).unwrap_or_default(); + choose_theme(options, color_scheme) + .map(|t| t.into_theme(color_scheme)) + .unwrap_or_else(|| default_theme(color_scheme).to_owned()) + } +} + +fn choose_theme(options: ThemeOptions, color_scheme: ColorScheme) -> Option { + match color_scheme { + ColorScheme::Dark => options.theme_dark, + ColorScheme::Light => options.theme_light, + } +} + +fn detect(when: DetectColorScheme, detector: &dyn ColorSchemeDetector) -> Option { + let should_detect = match when { + DetectColorScheme::Auto => detector.should_detect(), + DetectColorScheme::Always => true, + DetectColorScheme::Never => false, + }; + should_detect.then(|| detector.detect()).flatten() +} + +trait ColorSchemeDetector { fn should_detect(&self) -> bool; fn detect(&self) -> Option; } +struct TerminalColorSchemeDetector; + +#[cfg(feature = "detect-color-scheme")] +impl ColorSchemeDetector for TerminalColorSchemeDetector { + fn should_detect(&self) -> bool { + // Querying the terminal for its colors via OSC 10 / OSC 11 requires "exclusive" access + // since we read/write from the terminal and enable/disable raw mode. + // This causes race conditions with pagers such as less when they are attached to the + // same terminal as us. + // + // This is usually only an issue when the output is manually piped to a pager. + // For example: `bat Cargo.toml | less`. + // Otherwise, if we start the pager ourselves, then there's no race condition + // since the pager is started *after* the color is detected. + std::io::stdout().is_terminal() + } + + fn detect(&self) -> Option { + use terminal_colorsaurus::{color_scheme, ColorScheme as ColorsaurusScheme, QueryOptions}; + match color_scheme(QueryOptions::default()).ok()? { + ColorsaurusScheme::Dark => Some(ColorScheme::Dark), + ColorsaurusScheme::Light => Some(ColorScheme::Light), + } + } +} + +#[cfg(not(feature = "detect-color-scheme"))] +impl ColorSchemeDetector for TerminalColorSchemeDetector { + fn should_detect(&self) -> bool { + false + } + + fn detect(&self) -> Option { + None + } +} + #[cfg(test)] impl ColorSchemeDetector for Option { fn should_detect(&self) -> bool { @@ -134,7 +179,7 @@ mod tests { detect_color_scheme: Never, ..Default::default() }; - _ = theme(options, &detector); + _ = theme_from_detector(options, &detector); assert!(!detector.was_called.get()); } @@ -149,7 +194,7 @@ mod tests { detect_color_scheme: Always, ..Default::default() }; - _ = theme(options, &detector); + _ = theme_from_detector(options, &detector); assert!(detector.was_called.get()); } } @@ -157,14 +202,14 @@ mod tests { #[test] fn called_for_auto_if_should_detect() { let detector = DetectorStub::should_detect(Some(Dark)); - _ = theme(ThemeOptions::default(), &detector); + _ = theme_from_detector(ThemeOptions::default(), &detector); assert!(detector.was_called.get()); } #[test] fn not_called_for_auto_if_not_should_detect() { let detector = DetectorStub::should_not_detect(); - _ = theme(ThemeOptions::default(), &detector); + _ = theme_from_detector(ThemeOptions::default(), &detector); assert!(!detector.was_called.get()); } } @@ -188,7 +233,7 @@ mod tests { }, ] { let detector = ConstantDetector(color_scheme); - assert_eq!("Theme", theme(options, &detector)); + assert_eq!("Theme", theme_from_detector(options, &detector)); } } } @@ -200,7 +245,7 @@ mod tests { ..Default::default() }; let detector = DetectorStub::should_detect(Some(Dark)); - _ = theme(options, &detector); + _ = theme_from_detector(options, &detector); assert!(!detector.was_called.get()); } } @@ -213,7 +258,7 @@ mod tests { let detector = ConstantDetector(None); assert_eq!( default_theme(ColorScheme::Dark), - theme(ThemeOptions::default(), &detector) + theme_from_detector(ThemeOptions::default(), &detector) ); } @@ -227,7 +272,10 @@ mod tests { ..Default::default() }; let detector = ConstantDetector(color_scheme); - assert_eq!(default_theme(ColorScheme::Dark), theme(options, &detector)); + assert_eq!( + default_theme(ColorScheme::Dark), + theme_from_detector(options, &detector) + ); } } @@ -243,7 +291,10 @@ mod tests { }, ] { let detector = ConstantDetector(Some(color_scheme)); - assert_eq!(default_theme(color_scheme), theme(options, &detector)); + assert_eq!( + default_theme(color_scheme), + theme_from_detector(options, &detector) + ); } } } @@ -261,7 +312,7 @@ mod tests { ..Default::default() }; let detector = ConstantDetector(color_scheme); - assert_eq!("Dark", theme(options, &detector)); + assert_eq!("Dark", theme_from_detector(options, &detector)); } } @@ -273,7 +324,7 @@ mod tests { ..Default::default() }; let detector = ConstantDetector(Some(ColorScheme::Light)); - assert_eq!("Light", theme(options, &detector)); + assert_eq!("Light", theme_from_detector(options, &detector)); } } From 30b0143ccf93edac1310fca522a89550d5566676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Thu, 18 Jul 2024 15:44:33 +0200 Subject: [PATCH 08/28] Make default_theme pub --- src/theme.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/theme.rs b/src/theme.rs index 2491f9bd..b60184fb 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -10,7 +10,9 @@ pub fn theme(options: ThemeOptions) -> String { theme_from_detector(options, &TerminalColorSchemeDetector) } -pub(crate) const fn default_theme(color_scheme: ColorScheme) -> &'static str { +/// The default theme, suitable for the given color scheme. +/// Use [`theme`], if you want to automatically detect the color scheme from the terminal. +pub const fn default_theme(color_scheme: ColorScheme) -> &'static str { match color_scheme { ColorScheme::Dark => "Monokai Extended", ColorScheme::Light => "Monokai Extended Light", From 6498615f5fb047bef7d84072747676a74fa89c4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Tue, 16 Apr 2024 14:43:33 +0200 Subject: [PATCH 09/28] Improve upon the documentation --- src/pretty_printer.rs | 4 +++- src/theme.rs | 17 ++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/pretty_printer.rs b/src/pretty_printer.rs index eb123ea3..51c9af80 100644 --- a/src/pretty_printer.rs +++ b/src/pretty_printer.rs @@ -245,7 +245,9 @@ impl<'a> PrettyPrinter<'a> { self } - /// Specify the highlighting theme + /// Specify the highlighting theme. + /// You can use [`crate::theme::theme`] to pick a theme based on user preferences + /// and the terminal's background color. pub fn theme(&mut self, theme: impl AsRef) -> &mut Self { self.config.theme = theme.as_ref().to_owned(); self diff --git a/src/theme.rs b/src/theme.rs index b60184fb..54db307b 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -11,7 +11,7 @@ pub fn theme(options: ThemeOptions) -> String { } /// The default theme, suitable for the given color scheme. -/// Use [`theme`], if you want to automatically detect the color scheme from the terminal. +/// Use [`theme`] if you want to automatically detect the color scheme from the terminal. pub const fn default_theme(color_scheme: ColorScheme) -> &'static str { match color_scheme { ColorScheme::Dark => "Monokai Extended", @@ -21,7 +21,7 @@ pub const fn default_theme(color_scheme: ColorScheme) -> &'static str { /// Options for configuring the theme used for syntax highlighting. /// Used together with [`theme`]. -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq, Eq)] pub struct ThemeOptions { /// Always use this theme regardless of the terminal's background color. pub theme: Option, @@ -34,7 +34,14 @@ pub struct ThemeOptions { } /// The name of a theme or the default theme. -#[derive(Debug)] +/// +/// ``` +/// # use bat::theme::ThemeRequest; +/// # use std::str::FromStr as _; +/// assert_eq!(ThemeRequest::Default, ThemeRequest::from_str("default").unwrap()); +/// assert_eq!(ThemeRequest::Named("example".to_string()), ThemeRequest::from_str("example").unwrap()); +/// ``` +#[derive(Debug, PartialEq, Eq, Hash)] pub enum ThemeRequest { Named(String), Default, @@ -61,7 +68,7 @@ impl ThemeRequest { } } -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] pub enum DetectColorScheme { /// Only query the terminal for its colors when appropriate (i.e. when the the output is not redirected). #[default] @@ -73,7 +80,7 @@ pub enum DetectColorScheme { } /// The color scheme used to pick a fitting theme. Defaults to [`ColorScheme::Dark`]. -#[derive(Default, Copy, Clone)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] pub enum ColorScheme { #[default] Dark, From e8ca6ec7c31d5cbb07334fc6a814d5df442bbe91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Tue, 16 Apr 2024 14:43:35 +0200 Subject: [PATCH 10/28] Remove cargo feature --- Cargo.toml | 5 ++--- src/theme.rs | 12 ------------ 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 62ae693c..47600d0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ edition = '2021' rust-version = "1.70" [features] -default = ["application", "detect-color-scheme"] +default = ["application"] # Feature required for bat the application. Should be disabled when depending on # bat as a library. application = [ @@ -35,7 +35,6 @@ git = ["git2"] # Support indicating git modifications paging = ["shell-words", "grep-cli"] # Support applying a pager on the output lessopen = ["run_script", "os_str_bytes"] # Support $LESSOPEN preprocessor build-assets = ["syntect/yaml-load", "syntect/plist-load", "regex", "walkdir"] -detect-color-scheme = ["dep:terminal-colorsaurus"] # You need to use one of these if you depend on bat as a library: regex-onig = ["syntect/regex-onig"] # Use the "oniguruma" regex engine @@ -69,7 +68,7 @@ bytesize = { version = "1.3.0" } encoding_rs = "0.8.34" os_str_bytes = { version = "~7.0", optional = true } run_script = { version = "^0.10.1", optional = true} -terminal-colorsaurus = { version = "0.4", optional = true } +terminal-colorsaurus = "0.4" [dependencies.git2] version = "0.18" diff --git a/src/theme.rs b/src/theme.rs index 54db307b..d518e0b2 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -124,7 +124,6 @@ trait ColorSchemeDetector { struct TerminalColorSchemeDetector; -#[cfg(feature = "detect-color-scheme")] impl ColorSchemeDetector for TerminalColorSchemeDetector { fn should_detect(&self) -> bool { // Querying the terminal for its colors via OSC 10 / OSC 11 requires "exclusive" access @@ -148,17 +147,6 @@ impl ColorSchemeDetector for TerminalColorSchemeDetector { } } -#[cfg(not(feature = "detect-color-scheme"))] -impl ColorSchemeDetector for TerminalColorSchemeDetector { - fn should_detect(&self) -> bool { - false - } - - fn detect(&self) -> Option { - None - } -} - #[cfg(test)] impl ColorSchemeDetector for Option { fn should_detect(&self) -> bool { From 594b1417f1854fbb5c1bdb9cdd27d880b0ca38f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Tue, 16 Apr 2024 14:43:39 +0200 Subject: [PATCH 11/28] Update readme --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 016fe834..81ce2202 100644 --- a/README.md +++ b/README.md @@ -482,8 +482,10 @@ the following command (you need [`fzf`](https://github.com/junegunn/fzf) for thi bat --list-themes | fzf --preview="bat --theme={} --color=always /path/to/file" ``` -`bat` looks good on a dark background by default. However, if your terminal uses a -light background, some themes like `GitHub` or `OneHalfLight` will work better for you. +`bat` automatically picks a fitting theme depending on your terminal's background color. +You can use the `--theme-light` / `--theme-light` options or the `BAT_THEME_DARK` / `BAT_THEME_LIGHT` environment variables +to customize the themes used. This is especially useful if you frequently switch between dark and light mode. + You can also use a custom theme by following the ['Adding new themes' section below](https://github.com/sharkdp/bat#adding-new-themes). From c3b190d45b50a4ab05663de1680dba3cd74d7ecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Tue, 16 Apr 2024 14:43:42 +0200 Subject: [PATCH 12/28] Disable color detection in test --- tests/integration_tests.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 8df4327c..0381f48c 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -405,6 +405,7 @@ fn no_args_doesnt_break() { // as the slave end of a pseudo terminal. Although both point to the same "file", bat should // not exit, because in this case it is safe to read and write to the same fd, which is why // this test exists. + let OpenptyResult { master, slave } = openpty(None, None).expect("Couldn't open pty."); let mut master = unsafe { File::from_raw_fd(master) }; let stdin_file = unsafe { File::from_raw_fd(slave) }; @@ -415,6 +416,7 @@ fn no_args_doesnt_break() { let mut child = bat_raw_command() .stdin(stdin) .stdout(stdout) + .env("TERM", "dumb") // Suppresses color detection .spawn() .expect("Failed to start."); From 06b645435a497e77e28ad6d7d3531b5a5415c695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Tue, 16 Apr 2024 14:43:44 +0200 Subject: [PATCH 13/28] Add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea47b58f..0aa1112a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Syntax highlighting for JavaScript files that start with `#!/usr/bin/env bun` #2913 (@sharunkumar) - `bat --strip-ansi={never,always,auto}` to remove ANSI escape sequences from bat's input, see #2999 (@eth-p) - Add or remove individual style components without replacing all styles #2929 (@eth-p) +- Automatically choose theme based on the terminal's color scheme, see #2896 (@bash) ## Bugfixes From 1b0a6da4be97c7ab2b0e9b53db5a14ac4b1fad44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Tue, 16 Apr 2024 14:43:50 +0200 Subject: [PATCH 14/28] Use new `default_theme` fn for --list-themes --- src/bin/bat/main.rs | 10 +++++++--- src/theme.rs | 5 +++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/bin/bat/main.rs b/src/bin/bat/main.rs index 4528a60b..4dfb8124 100644 --- a/src/bin/bat/main.rs +++ b/src/bin/bat/main.rs @@ -30,12 +30,12 @@ use directories::PROJECT_DIRS; use globset::GlobMatcher; use bat::{ - assets::HighlightingAssets, config::Config, controller::Controller, error::*, input::Input, style::{StyleComponent, StyleComponents}, + theme::{color_scheme, default_theme, ColorScheme, DetectColorScheme}, MappingTarget, PagingMode, }; @@ -200,10 +200,14 @@ pub fn list_themes(cfg: &Config, config_dir: &Path, cache_dir: &Path) -> Result< let stdout = io::stdout(); let mut stdout = stdout.lock(); - let default_theme = HighlightingAssets::default_theme(); + let default_theme_name = default_theme(color_scheme(DetectColorScheme::Auto)); for theme in assets.themes() { - let default_theme_info = if default_theme == theme { + let default_theme_info = if default_theme_name == theme { " (default)" + } else if default_theme(ColorScheme::Dark) == theme { + " (default dark)" + } else if default_theme(ColorScheme::Light) == theme { + " (default light)" } else { "" }; diff --git a/src/theme.rs b/src/theme.rs index d518e0b2..ef5a8ae2 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -19,6 +19,11 @@ pub const fn default_theme(color_scheme: ColorScheme) -> &'static str { } } +/// Detects the color scheme from the terminal. +pub fn color_scheme(when: DetectColorScheme) -> ColorScheme { + detect(when, &TerminalColorSchemeDetector).unwrap_or_default() +} + /// Options for configuring the theme used for syntax highlighting. /// Used together with [`theme`]. #[derive(Debug, Default, PartialEq, Eq)] From 5c6974703e7db3a432be64f301cd08bd35135ac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Wed, 1 May 2024 08:27:31 +0200 Subject: [PATCH 15/28] Respect --detect-color-scheme flag when listing themes --- src/bin/bat/app.rs | 17 ++++++++++------- src/bin/bat/main.rs | 11 ++++++++--- tests/integration_tests.rs | 16 +++++++--------- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index 711ab1c6..606132f7 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -427,7 +427,16 @@ impl App { .matches .get_one::("theme-light") .map(|t| ThemeRequest::from_str(t).unwrap()); - let detect_color_scheme = match self + ThemeOptions { + theme, + theme_dark, + theme_light, + detect_color_scheme: self.detect_color_scheme(), + } + } + + pub(crate) fn detect_color_scheme(&self) -> DetectColorScheme { + match self .matches .get_one::("detect-color-scheme") .map(|s| s.as_str()) @@ -436,12 +445,6 @@ impl App { Some("never") => DetectColorScheme::Never, Some("always") => DetectColorScheme::Always, _ => unreachable!("other values for --detect-color-scheme are not allowed"), - }; - ThemeOptions { - theme, - theme_dark, - theme_light, - detect_color_scheme, } } } diff --git a/src/bin/bat/main.rs b/src/bin/bat/main.rs index 4dfb8124..95493a6d 100644 --- a/src/bin/bat/main.rs +++ b/src/bin/bat/main.rs @@ -189,7 +189,12 @@ fn theme_preview_file<'a>() -> Input<'a> { Input::from_reader(Box::new(BufReader::new(THEME_PREVIEW_DATA))) } -pub fn list_themes(cfg: &Config, config_dir: &Path, cache_dir: &Path) -> Result<()> { +pub fn list_themes( + cfg: &Config, + config_dir: &Path, + cache_dir: &Path, + detect_color_scheme: DetectColorScheme, +) -> Result<()> { let assets = assets_from_cache_or_binary(cfg.use_custom_assets, cache_dir)?; let mut config = cfg.clone(); let mut style = HashSet::new(); @@ -200,7 +205,7 @@ pub fn list_themes(cfg: &Config, config_dir: &Path, cache_dir: &Path) -> Result< let stdout = io::stdout(); let mut stdout = stdout.lock(); - let default_theme_name = default_theme(color_scheme(DetectColorScheme::Auto)); + let default_theme_name = default_theme(color_scheme(detect_color_scheme)); for theme in assets.themes() { let default_theme_info = if default_theme_name == theme { " (default)" @@ -375,7 +380,7 @@ fn run() -> Result { }; run_controller(inputs, &plain_config, cache_dir) } else if app.matches.get_flag("list-themes") { - list_themes(&config, config_dir, cache_dir)?; + list_themes(&config, config_dir, cache_dir, app.detect_color_scheme())?; Ok(true) } else if app.matches.get_flag("config-file") { println!("{}", config_file().to_string_lossy()); diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 0381f48c..23aed5bc 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -274,37 +274,35 @@ fn squeeze_limit_line_numbers() { #[test] fn list_themes_with_colors() { - #[cfg(target_os = "macos")] - let default_theme_chunk = "Monokai Extended Light\x1B[0m (default)"; - - #[cfg(not(target_os = "macos"))] let default_theme_chunk = "Monokai Extended\x1B[0m (default)"; + let default_light_theme_chunk = "Monokai Extended Light\x1B[0m (default light)"; bat() .arg("--color=always") + .arg("--detect-color-scheme=never") .arg("--list-themes") .assert() .success() .stdout(predicate::str::contains("DarkNeon").normalize()) .stdout(predicate::str::contains(default_theme_chunk).normalize()) + .stdout(predicate::str::contains(default_light_theme_chunk).normalize()) .stdout(predicate::str::contains("Output the square of a number.").normalize()); } #[test] fn list_themes_without_colors() { - #[cfg(target_os = "macos")] - let default_theme_chunk = "Monokai Extended Light (default)"; - - #[cfg(not(target_os = "macos"))] let default_theme_chunk = "Monokai Extended (default)"; + let default_light_theme_chunk = "Monokai Extended Light (default light)"; bat() .arg("--color=never") + .arg("--detect-color-scheme=never") .arg("--list-themes") .assert() .success() .stdout(predicate::str::contains("DarkNeon").normalize()) - .stdout(predicate::str::contains(default_theme_chunk).normalize()); + .stdout(predicate::str::contains(default_theme_chunk).normalize()) + .stdout(predicate::str::contains(default_light_theme_chunk).normalize()); } #[test] From abf9dada04b715494ae323ffdc51e1bb39854221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Thu, 18 Jul 2024 15:49:25 +0200 Subject: [PATCH 16/28] Remove `HighlightingAssets::default_theme()` --- CHANGELOG.md | 3 +++ src/assets.rs | 57 --------------------------------------------------- 2 files changed, 3 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0aa1112a..0cd76031 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,9 @@ - [BREAKING] `SyntaxMapping::mappings` is replaced by `SyntaxMapping::{builtin,custom,all}_mappings` - Make `Controller::run_with_error_handler`'s error handler `FnMut`, see #2831 (@rhysd) - Improve compile time by 20%, see #2815 (@dtolnay) +- Add `theme::theme` for choosing an appropriate theme based on the + terminal's color scheme, see #2896 (@bash) + - [BREAKING] Remove `HighlightingAssets::default_theme`. Use `theme::default_theme` instead. # v0.24.0 diff --git a/src/assets.rs b/src/assets.rs index 857f416b..d32ccbd4 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -70,43 +70,6 @@ impl HighlightingAssets { } } - /// The default theme. - /// - /// ### Windows and Linux - /// - /// Windows and most Linux distributions has a dark terminal theme by - /// default. On these platforms, this function always returns a theme that - /// looks good on a dark background. - /// - /// ### macOS - /// - /// On macOS the default terminal background is light, but it is common that - /// Dark Mode is active, which makes the terminal background dark. On this - /// platform, the default theme depends on - /// ```bash - /// defaults read -globalDomain AppleInterfaceStyle - /// ``` - /// To avoid the overhead of the check on macOS, simply specify a theme - /// explicitly via `--theme`, `BAT_THEME`, or `~/.config/bat`. - /// - /// See and - /// for more context. - #[deprecated(note = "use bat::theme::theme instead")] - pub fn default_theme() -> &'static str { - #[cfg(not(target_os = "macos"))] - { - default_theme(ColorScheme::Dark) - } - #[cfg(target_os = "macos")] - { - if macos_dark_mode_active() { - default_theme(ColorScheme::Dark) - } else { - default_theme(ColorScheme::Light) - } - } - } - pub fn from_cache(cache_path: &Path) -> Result { Ok(HighlightingAssets::new( SerializedSyntaxSet::FromFile(cache_path.join("syntaxes.bin")), @@ -389,26 +352,6 @@ fn asset_from_cache( .map_err(|_| format!("Could not parse cached {description}").into()) } -#[cfg(target_os = "macos")] -fn macos_dark_mode_active() -> bool { - const PREFERENCES_FILE: &str = "Library/Preferences/.GlobalPreferences.plist"; - const STYLE_KEY: &str = "AppleInterfaceStyle"; - - let preferences_file = home::home_dir() - .map(|home| home.join(PREFERENCES_FILE)) - .expect("Could not get home directory"); - - match plist::Value::from_file(preferences_file).map(|file| file.into_dictionary()) { - Ok(Some(preferences)) => match preferences.get(STYLE_KEY).and_then(|val| val.as_string()) { - Some(value) => value == "Dark", - // If the key does not exist, then light theme is currently in use. - None => false, - }, - // Unreachable, in theory. All macOS users have a home directory and preferences file setup. - Ok(None) | Err(_) => true, - } -} - #[cfg(test)] mod tests { use super::*; From b9b981f6572c612cce443a8fff0b5fb9c24d3868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Thu, 18 Jul 2024 17:36:57 +0200 Subject: [PATCH 17/28] Generalize --detect-color-scheme to --color-scheme --- assets/completions/_bat.ps1.in | 2 +- assets/completions/bat.bash.in | 6 ++- assets/completions/bat.fish.in | 10 +++- assets/completions/bat.zsh.in | 2 +- doc/long-help.txt | 26 +++++----- doc/short-help.txt | 4 +- src/bin/bat/app.rs | 18 ++++--- src/bin/bat/clap_app.rs | 34 ++++++------- src/bin/bat/main.rs | 13 +++-- src/theme.rs | 90 +++++++++++++++++++++++++++------- tests/integration_tests.rs | 4 +- 11 files changed, 140 insertions(+), 69 deletions(-) diff --git a/assets/completions/_bat.ps1.in b/assets/completions/_bat.ps1.in index 5635dea2..ac66ccc8 100644 --- a/assets/completions/_bat.ps1.in +++ b/assets/completions/_bat.ps1.in @@ -32,7 +32,7 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script [CompletionResult]::new('--color', 'color', [CompletionResultType]::ParameterName, 'When to use colors (*auto*, never, always).') [CompletionResult]::new('--italic-text', 'italic-text', [CompletionResultType]::ParameterName, 'Use italics in output (always, *never*)') [CompletionResult]::new('--decorations', 'decorations', [CompletionResultType]::ParameterName, 'When to show the decorations (*auto*, never, always).') - [CompletionResult]::new('--detect-color-scheme', 'detect-color-scheme', [CompletionResultType]::ParameterName, 'When to detect the terminal''s color scheme (*auto*, never, always).') + [CompletionResult]::new('--color-scheme', 'color-scheme', [CompletionResultType]::ParameterName, 'Whether to choose a dark or light syntax highlighting theme (*auto*, auto:always, dark, light, system).') [CompletionResult]::new('--paging', 'paging', [CompletionResultType]::ParameterName, 'Specify when to use the pager, or use `-P` to disable (*auto*, never, always).') [CompletionResult]::new('--pager', 'pager', [CompletionResultType]::ParameterName, 'Determine which pager to use.') [CompletionResult]::new('-m', 'm', [CompletionResultType]::ParameterName, 'Use the specified syntax for files matching the glob pattern (''*.cpp:C++'').') diff --git a/assets/completions/bat.bash.in b/assets/completions/bat.bash.in index a02c5a04..0a01a054 100644 --- a/assets/completions/bat.bash.in +++ b/assets/completions/bat.bash.in @@ -100,10 +100,12 @@ _bat() { COMPREPLY=($(compgen -W "auto never character" -- "$cur")) return 0 ;; - --color | --decorations | --paging | --detect-color-scheme) + --color | --decorations | --paging) COMPREPLY=($(compgen -W "auto never always" -- "$cur")) return 0 ;; + --color-scheme) + COMPREPLY=($(compgen -W "auto auto:always dark light system" -- "$cur")) --italic-text) COMPREPLY=($(compgen -W "always never" -- "$cur")) return 0 @@ -166,7 +168,6 @@ _bat() { --color --italic-text --decorations - --detect-color-scheme --force-colorization --paging --pager @@ -175,6 +176,7 @@ _bat() { --theme --theme-dark --theme-light + --color-scheme --list-themes --squeeze-blank --squeeze-limit diff --git a/assets/completions/bat.fish.in b/assets/completions/bat.fish.in index 7bd0ebca..33cf8264 100644 --- a/assets/completions/bat.fish.in +++ b/assets/completions/bat.fish.in @@ -99,7 +99,13 @@ set -l color_opts ' ' set -l decorations_opts $color_opts set -l paging_opts $color_opts -set -l detect_color_scheme_opts $color_opts +set -l color_scheme_opts " + auto\t'Use the terminal\'s color scheme if the output is not redirected (default)' + auto:always\t'Always use the terminal\'s color scheme' + dark\t'Use a dark syntax highlighting theme' + light\t'Use a light syntax highlighting theme' + system\t'Query the OS for its color scheme (macOS only)' +" # Include some examples so we can indicate the default. set -l pager_opts ' @@ -144,7 +150,7 @@ complete -c $bat -l config-file -f -d "Display location of configuration file" - complete -c $bat -l decorations -x -a "$decorations_opts" -d "When to use --style decorations" -n __bat_no_excl_args -complete -c $bat -l detect-color-scheme -x -a "$detect_color_scheme_opts" -d "When to detect the terminal's color scheme" -n __bat_no_excl_args +complete -c $bat -l color-scheme -x -a "$color_scheme_opts" -d "Whether to choose a dark or light syntax highlighting theme" -n __bat_no_excl_args complete -c $bat -l diagnostic -d "Print diagnostic info for bug reports" -n __fish_is_first_arg diff --git a/assets/completions/bat.zsh.in b/assets/completions/bat.zsh.in index 4a598437..4bcae11c 100644 --- a/assets/completions/bat.zsh.in +++ b/assets/completions/bat.zsh.in @@ -40,7 +40,7 @@ _{{PROJECT_EXECUTABLE}}_main() { --color='[specify when to use colors]:when:(auto never always)' --italic-text='[use italics in output]:when:(always never)' --decorations='[specify when to show the decorations]:when:(auto never always)' - --detect-color-scheme="[specify when to detect the terminal's color scheme]:when:(auto never always)" + --color-scheme="[whether to choose a dark or light syntax highlighting theme]:scheme:(auto auto:always dark light system)" --paging='[specify when to use the pager]:when:(auto never always)' '(-m --map-syntax)'{-m+,--map-syntax=}'[map a glob pattern to an existing syntax name]: :->syntax-maps' '(--theme)'--theme='[set the color theme for syntax highlighting]:theme:->themes' diff --git a/doc/long-help.txt b/doc/long-help.txt index 86e9a532..c374a039 100644 --- a/doc/long-help.txt +++ b/doc/long-help.txt @@ -114,22 +114,22 @@ Options: add the '--theme="..."' option to the configuration file or export the BAT_THEME environment variable (e.g.: export BAT_THEME="..."). - --detect-color-scheme - Specify when to query the terminal for its colors in order to pick an appropriate syntax - highlighting theme. Use '--theme-light' and '--theme-dark' (or the environment variables - BAT_THEME_LIGHT and BAT_THEME_DARK) to configure which themes are picked. You may also use - '--theme' to set a theme that is used regardless of the terminal's colors. + --color-scheme + Specify whether to choose a dark or light syntax highlighting theme. Use '--theme-light' + and '--theme-dark' (or the environment variables BAT_THEME_LIGHT and BAT_THEME_DARK) to + configure which themes are picked. You may also use '--theme' to set a theme that is used + regardless of this choice. Possible values: * auto (default): - Only query the terminals colors if the output is not redirected. This is to prevent - race conditions with pagers such as less. - * never - Never query the terminal for its colors and assume that the terminal has a dark - background. - * always - Always query the terminal for its colors, regardless of whether or not the output is - redirected. + Query the terminals for its color scheme if the output is not redirected. This is to + prevent race conditions with pagers such as less. + * 'auto:always': + Always query the terminal for its color scheme, regardless of whether or not the + output is redirected. + * dark: Use a dark syntax highlighting theme. + * light: Use a light syntax highlighting theme. + * system: Query the OS for its color scheme. Only works on macOS. --theme-light Sets the theme name for syntax highlighting used when the terminal uses a light diff --git a/doc/short-help.txt b/doc/short-help.txt index 3e369229..f17a6d9d 100644 --- a/doc/short-help.txt +++ b/doc/short-help.txt @@ -41,8 +41,8 @@ Options: Use the specified syntax for files matching the glob pattern ('*.cpp:C++'). --theme Set the color theme for syntax highlighting. - --detect-color-scheme - Specify when to query the terminal for its colors. + --color-scheme + Specify whether to choose a dark or light theme. --theme-light Sets the color theme for syntax highlighting used for light backgrounds. --theme-dark diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index 606132f7..d2e5b4db 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -9,7 +9,7 @@ use crate::{ config::{get_args_from_config_file, get_args_from_env_opts_var, get_args_from_env_vars}, }; use bat::style::StyleComponentList; -use bat::theme::{theme, DetectColorScheme, ThemeOptions, ThemeRequest}; +use bat::theme::{theme, ColorSchemePreference, DetectColorScheme, ThemeOptions, ThemeRequest}; use bat::StripAnsiMode; use clap::ArgMatches; @@ -431,20 +431,22 @@ impl App { theme, theme_dark, theme_light, - detect_color_scheme: self.detect_color_scheme(), + color_scheme: self.color_scheme_preference(), } } - pub(crate) fn detect_color_scheme(&self) -> DetectColorScheme { + pub(crate) fn color_scheme_preference(&self) -> ColorSchemePreference { match self .matches - .get_one::("detect-color-scheme") + .get_one::("color-scheme") .map(|s| s.as_str()) { - Some("auto") => DetectColorScheme::Auto, - Some("never") => DetectColorScheme::Never, - Some("always") => DetectColorScheme::Always, - _ => unreachable!("other values for --detect-color-scheme are not allowed"), + Some("auto") => ColorSchemePreference::Auto(DetectColorScheme::Auto), + Some("auto:always") => ColorSchemePreference::Auto(DetectColorScheme::Always), + Some("dark") => ColorSchemePreference::Dark, + Some("light") => ColorSchemePreference::Light, + Some("system") => ColorSchemePreference::System, + _ => unreachable!("other values for --color-scheme are not allowed"), } } } diff --git a/src/bin/bat/clap_app.rs b/src/bin/bat/clap_app.rs index 6abdab9c..0857dba4 100644 --- a/src/bin/bat/clap_app.rs +++ b/src/bin/bat/clap_app.rs @@ -384,30 +384,30 @@ pub fn build_app(interactive_output: bool) -> Command { ), ) .arg( - Arg::new("detect-color-scheme") - .long("detect-color-scheme") - .overrides_with("detect-color-scheme") - .value_name("when") - .value_parser(["auto", "never", "always"]) + Arg::new("color-scheme") + .long("color-scheme") + .overrides_with("color-scheme") + .value_name("scheme") + .value_parser(["auto", "auto:always", "dark", "light", "system"]) .default_value("auto") .hide_default_value(true) - .help("Specify when to query the terminal for its colors.") + .help("Specify whether to choose a dark or light theme.") .long_help( - "Specify when to query the terminal for its colors \ - in order to pick an appropriate syntax highlighting theme. \ + "Specify whether to choose a dark or light syntax highlighting theme. \ Use '--theme-light' and '--theme-dark' (or the environment variables \ BAT_THEME_LIGHT and BAT_THEME_DARK) to configure which themes are picked. \ - You may also use '--theme' to set a theme that is used regardless of the terminal's colors.\n\n\ + You may also use '--theme' to set a theme that is used regardless of this choice.\n\n\ Possible values:\n\ - * auto (default):\n \ - Only query the terminals colors if the output is not redirected. \ + * auto (default):\n \ + Query the terminals for its color scheme if the output is not redirected. \ This is to prevent race conditions with pagers such as less.\n\ - * never\n \ - Never query the terminal for its colors \ - and assume that the terminal has a dark background.\n\ - * always\n \ - Always query the terminal for its colors, \ - regardless of whether or not the output is redirected."), + * 'auto:always':\n \ + Always query the terminal for its color scheme, \ + regardless of whether or not the output is redirected.\n\ + * dark: Use a dark syntax highlighting theme.\n\ + * light: Use a light syntax highlighting theme.\n\ + * system: Query the OS for its color scheme. Only works on macOS.\n\ + "), ) .arg( Arg::new("theme-light") diff --git a/src/bin/bat/main.rs b/src/bin/bat/main.rs index 95493a6d..891390c1 100644 --- a/src/bin/bat/main.rs +++ b/src/bin/bat/main.rs @@ -35,7 +35,7 @@ use bat::{ error::*, input::Input, style::{StyleComponent, StyleComponents}, - theme::{color_scheme, default_theme, ColorScheme, DetectColorScheme}, + theme::{color_scheme, default_theme, ColorScheme, ColorSchemePreference}, MappingTarget, PagingMode, }; @@ -193,7 +193,7 @@ pub fn list_themes( cfg: &Config, config_dir: &Path, cache_dir: &Path, - detect_color_scheme: DetectColorScheme, + color_scheme_pref: ColorSchemePreference, ) -> Result<()> { let assets = assets_from_cache_or_binary(cfg.use_custom_assets, cache_dir)?; let mut config = cfg.clone(); @@ -205,7 +205,7 @@ pub fn list_themes( let stdout = io::stdout(); let mut stdout = stdout.lock(); - let default_theme_name = default_theme(color_scheme(detect_color_scheme)); + let default_theme_name = default_theme(color_scheme(color_scheme_pref)); for theme in assets.themes() { let default_theme_info = if default_theme_name == theme { " (default)" @@ -380,7 +380,12 @@ fn run() -> Result { }; run_controller(inputs, &plain_config, cache_dir) } else if app.matches.get_flag("list-themes") { - list_themes(&config, config_dir, cache_dir, app.detect_color_scheme())?; + list_themes( + &config, + config_dir, + cache_dir, + app.color_scheme_preference(), + )?; Ok(true) } else if app.matches.get_flag("config-file") { println!("{}", config_file().to_string_lossy()); diff --git a/src/theme.rs b/src/theme.rs index ef5a8ae2..1c6d7e55 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -20,8 +20,8 @@ pub const fn default_theme(color_scheme: ColorScheme) -> &'static str { } /// Detects the color scheme from the terminal. -pub fn color_scheme(when: DetectColorScheme) -> ColorScheme { - detect(when, &TerminalColorSchemeDetector).unwrap_or_default() +pub fn color_scheme(preference: ColorSchemePreference) -> ColorScheme { + color_scheme_impl(preference, &TerminalColorSchemeDetector) } /// Options for configuring the theme used for syntax highlighting. @@ -34,8 +34,8 @@ pub struct ThemeOptions { pub theme_dark: Option, /// The theme to use in case the terminal uses a light background with dark text. pub theme_light: Option, - /// Whether or not to test if the terminal is dark or light by querying for its colors. - pub detect_color_scheme: DetectColorScheme, + /// How to choose between dark and light. + pub color_scheme: ColorSchemePreference, } /// The name of a theme or the default theme. @@ -73,6 +73,25 @@ impl ThemeRequest { } } +/// How to choose between dark and light. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ColorSchemePreference { + /// Detect the color scheme from the terminal. + Auto(DetectColorScheme), + /// Use a dark theme. + Dark, + /// Use a light theme. + Light, + /// Detect the color scheme from the OS instead (macOS only). + System, +} + +impl Default for ColorSchemePreference { + fn default() -> Self { + Self::Auto(DetectColorScheme::default()) + } +} + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] pub enum DetectColorScheme { /// Only query the terminal for its colors when appropriate (i.e. when the the output is not redirected). @@ -80,8 +99,6 @@ pub enum DetectColorScheme { Auto, /// Always query the terminal for its colors. Always, - /// Never query the terminal for its colors. - Never, } /// The color scheme used to pick a fitting theme. Defaults to [`ColorScheme::Dark`]. @@ -92,13 +109,25 @@ pub enum ColorScheme { Light, } +fn color_scheme_impl( + pref: ColorSchemePreference, + detector: &dyn ColorSchemeDetector, +) -> ColorScheme { + match pref { + ColorSchemePreference::Auto(when) => detect(when, detector).unwrap_or_default(), + ColorSchemePreference::Dark => ColorScheme::Dark, + ColorSchemePreference::Light => ColorScheme::Light, + ColorSchemePreference::System => color_scheme_from_system().unwrap_or_default(), + } +} + fn theme_from_detector(options: ThemeOptions, detector: &dyn ColorSchemeDetector) -> String { // Implementation note: This function is mostly pure (i.e. it has no side effects) for the sake of testing. // All the side effects (e.g. querying the terminal for its colors) are performed in the detector. if let Some(theme) = options.theme { theme.into_theme(ColorScheme::default()) } else { - let color_scheme = detect(options.detect_color_scheme, detector).unwrap_or_default(); + let color_scheme = color_scheme_impl(options.color_scheme, detector); choose_theme(options, color_scheme) .map(|t| t.into_theme(color_scheme)) .unwrap_or_else(|| default_theme(color_scheme).to_owned()) @@ -116,7 +145,6 @@ fn detect(when: DetectColorScheme, detector: &dyn ColorSchemeDetector) -> Option let should_detect = match when { DetectColorScheme::Auto => detector.should_detect(), DetectColorScheme::Always => true, - DetectColorScheme::Never => false, }; should_detect.then(|| detector.detect()).flatten() } @@ -152,6 +180,31 @@ impl ColorSchemeDetector for TerminalColorSchemeDetector { } } +#[cfg(not(target_os = "macos"))] +fn color_scheme_from_system() -> Option { + None +} + +#[cfg(target_os = "macos")] +fn color_scheme_from_system() -> Option { + const PREFERENCES_FILE: &str = "Library/Preferences/.GlobalPreferences.plist"; + const STYLE_KEY: &str = "AppleInterfaceStyle"; + + let preferences_file = home::home_dir() + .map(|home| home.join(PREFERENCES_FILE)) + .expect("Could not get home directory"); + + match plist::Value::from_file(preferences_file).map(|file| file.into_dictionary()) { + Ok(Some(preferences)) => match preferences.get(STYLE_KEY).and_then(|val| val.as_string()) { + Some(value) if value == "Dark" => Some(ColorScheme::Dark), + // If the key does not exist, then light theme is currently in use. + Some(_) | None => Some(ColorScheme::Light), + }, + // Unreachable, in theory. All macOS users have a home directory and preferences file setup. + Ok(None) | Err(_) => None, + } +} + #[cfg(test)] impl ColorSchemeDetector for Option { fn should_detect(&self) -> bool { @@ -166,6 +219,7 @@ impl ColorSchemeDetector for Option { #[cfg(test)] mod tests { use super::ColorScheme::*; + use super::ColorSchemePreference as Pref; use super::DetectColorScheme::*; use super::*; use std::cell::Cell; @@ -175,14 +229,16 @@ mod tests { use super::*; #[test] - fn not_called_for_never() { - let detector = DetectorStub::should_detect(Some(Dark)); - let options = ThemeOptions { - detect_color_scheme: Never, - ..Default::default() - }; - _ = theme_from_detector(options, &detector); - assert!(!detector.was_called.get()); + fn not_called_for_dark_or_light() { + for pref in [Pref::Dark, Pref::Light] { + let detector = DetectorStub::should_detect(Some(Dark)); + let options = ThemeOptions { + color_scheme: pref, + ..Default::default() + }; + _ = theme_from_detector(options, &detector); + assert!(!detector.was_called.get()); + } } #[test] @@ -193,7 +249,7 @@ mod tests { ]; for detector in detectors { let options = ThemeOptions { - detect_color_scheme: Always, + color_scheme: Pref::Auto(Always), ..Default::default() }; _ = theme_from_detector(options, &detector); diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 23aed5bc..e4b73c59 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -279,7 +279,7 @@ fn list_themes_with_colors() { bat() .arg("--color=always") - .arg("--detect-color-scheme=never") + .arg("--color-scheme=dark") .arg("--list-themes") .assert() .success() @@ -296,7 +296,7 @@ fn list_themes_without_colors() { bat() .arg("--color=never") - .arg("--detect-color-scheme=never") + .arg("--color-scheme=dark") .arg("--list-themes") .assert() .success() From bc42149a726b73e5e4cd6b8afdeb340a3ae23c9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Sun, 18 Aug 2024 14:59:14 +0200 Subject: [PATCH 18/28] Merge color scheme options into theme / BAT_THEME --- assets/completions/_bat.ps1.in | 1 - assets/completions/bat.bash.in | 3 - assets/completions/bat.fish.in | 9 --- assets/completions/bat.zsh.in | 1 - assets/manual/bat.1.in | 26 ++------ doc/long-help.txt | 17 ----- doc/short-help.txt | 2 - src/bin/bat/app.rs | 25 ++------ src/bin/bat/clap_app.rs | 26 -------- src/bin/bat/main.rs | 2 +- src/theme.rs | 111 ++++++++++++++++++++++----------- tests/integration_tests.rs | 2 - 12 files changed, 83 insertions(+), 142 deletions(-) diff --git a/assets/completions/_bat.ps1.in b/assets/completions/_bat.ps1.in index ac66ccc8..b6f62aae 100644 --- a/assets/completions/_bat.ps1.in +++ b/assets/completions/_bat.ps1.in @@ -32,7 +32,6 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script [CompletionResult]::new('--color', 'color', [CompletionResultType]::ParameterName, 'When to use colors (*auto*, never, always).') [CompletionResult]::new('--italic-text', 'italic-text', [CompletionResultType]::ParameterName, 'Use italics in output (always, *never*)') [CompletionResult]::new('--decorations', 'decorations', [CompletionResultType]::ParameterName, 'When to show the decorations (*auto*, never, always).') - [CompletionResult]::new('--color-scheme', 'color-scheme', [CompletionResultType]::ParameterName, 'Whether to choose a dark or light syntax highlighting theme (*auto*, auto:always, dark, light, system).') [CompletionResult]::new('--paging', 'paging', [CompletionResultType]::ParameterName, 'Specify when to use the pager, or use `-P` to disable (*auto*, never, always).') [CompletionResult]::new('--pager', 'pager', [CompletionResultType]::ParameterName, 'Determine which pager to use.') [CompletionResult]::new('-m', 'm', [CompletionResultType]::ParameterName, 'Use the specified syntax for files matching the glob pattern (''*.cpp:C++'').') diff --git a/assets/completions/bat.bash.in b/assets/completions/bat.bash.in index 0a01a054..162d1c53 100644 --- a/assets/completions/bat.bash.in +++ b/assets/completions/bat.bash.in @@ -104,8 +104,6 @@ _bat() { COMPREPLY=($(compgen -W "auto never always" -- "$cur")) return 0 ;; - --color-scheme) - COMPREPLY=($(compgen -W "auto auto:always dark light system" -- "$cur")) --italic-text) COMPREPLY=($(compgen -W "always never" -- "$cur")) return 0 @@ -176,7 +174,6 @@ _bat() { --theme --theme-dark --theme-light - --color-scheme --list-themes --squeeze-blank --squeeze-limit diff --git a/assets/completions/bat.fish.in b/assets/completions/bat.fish.in index 33cf8264..9907718f 100644 --- a/assets/completions/bat.fish.in +++ b/assets/completions/bat.fish.in @@ -99,13 +99,6 @@ set -l color_opts ' ' set -l decorations_opts $color_opts set -l paging_opts $color_opts -set -l color_scheme_opts " - auto\t'Use the terminal\'s color scheme if the output is not redirected (default)' - auto:always\t'Always use the terminal\'s color scheme' - dark\t'Use a dark syntax highlighting theme' - light\t'Use a light syntax highlighting theme' - system\t'Query the OS for its color scheme (macOS only)' -" # Include some examples so we can indicate the default. set -l pager_opts ' @@ -150,8 +143,6 @@ complete -c $bat -l config-file -f -d "Display location of configuration file" - complete -c $bat -l decorations -x -a "$decorations_opts" -d "When to use --style decorations" -n __bat_no_excl_args -complete -c $bat -l color-scheme -x -a "$color_scheme_opts" -d "Whether to choose a dark or light syntax highlighting theme" -n __bat_no_excl_args - complete -c $bat -l diagnostic -d "Print diagnostic info for bug reports" -n __fish_is_first_arg complete -c $bat -s d -l diff -d "Only show lines with Git changes" -n __bat_no_excl_args diff --git a/assets/completions/bat.zsh.in b/assets/completions/bat.zsh.in index 4bcae11c..bec0d3c9 100644 --- a/assets/completions/bat.zsh.in +++ b/assets/completions/bat.zsh.in @@ -40,7 +40,6 @@ _{{PROJECT_EXECUTABLE}}_main() { --color='[specify when to use colors]:when:(auto never always)' --italic-text='[use italics in output]:when:(always never)' --decorations='[specify when to show the decorations]:when:(auto never always)' - --color-scheme="[whether to choose a dark or light syntax highlighting theme]:scheme:(auto auto:always dark light system)" --paging='[specify when to use the pager]:when:(auto never always)' '(-m --map-syntax)'{-m+,--map-syntax=}'[map a glob pattern to an existing syntax name]: :->syntax-maps' '(--theme)'--theme='[set the color theme for syntax highlighting]:theme:->themes' diff --git a/assets/manual/bat.1.in b/assets/manual/bat.1.in index 5dcabe2d..05239d50 100644 --- a/assets/manual/bat.1.in +++ b/assets/manual/bat.1.in @@ -117,24 +117,6 @@ Specify when to use the decorations that have been specified via '\-\-style'. Th automatic mode only enables decorations if an interactive terminal is detected. Possible values: *auto*, never, always. .HP -\fB\-\-detect\-color\-scheme\fR -.IP -Specify when to query the terminal for its colors in order to pick an appropriate syntax -highlighting theme. Use \fB\-\-theme-light\fP and \fB\-\-theme-dark\fP (or the environment variables -\fBBAT_THEME_LIGHT\fP and \fBBAT_THEME_DARK\fP) to configure which themes are picked. You can also use -\fP\-\-theme\fP to set a theme that is used regardless of the terminal's colors. -.IP -\fI\fP can be one of: -.RS -.IP "\fBauto\fP" -Only query the terminals colors if the output is not redirected. This is to prevent -race conditions with pagers such as less. -.IP "never" -Never query the terminal for its colors and assume that the terminal has a dark background. -.IP "always" -Always query the terminal for its colors, regardless of whether or not the output is redirected. -.RE -.HP \fB\-f\fR, \fB\-\-force\-colorization\fR .IP Alias for '--decorations=always --color=always'. This is useful \ @@ -177,14 +159,14 @@ export the \fBBAT_THEME\fP environment variable (e.g.: \fBexport BAT_THEME="..." \fB\-\-theme\-dark\fR .IP Sets the theme name for syntax highlighting used when the terminal uses a dark background. -To set a default theme, add the \fB\-\-theme-dark="..."\fP option to the configuration file or +To set a default theme, add the \fB\-\-theme-dark="..."\fP option to the configuration file or export the \fBBAT_THEME_DARK\fP environment variable (e.g. \fBexport BAT_THEME_DARK="..."\fP). This option is ignored if \fB\-\-theme\fP option is set. .HP \fB\-\-theme\-light\fR .IP Sets the theme name for syntax highlighting used when the terminal uses a dark background. -To set a default theme, add the \fB\-\-theme-dark="..."\fP option to the configuration file or +To set a default theme, add the \fB\-\-theme-dark="..."\fP option to the configuration file or export the \fBBAT_THEME_LIGHT\fP environment variable (e.g. \fBexport BAT_THEME_LIGHT="..."\fP). This option is ignored if \fB\-\-theme\fP option is set. .HP @@ -339,7 +321,7 @@ To use the preprocessor, call: \fB{{PROJECT_EXECUTABLE}} --lessopen\fR -Alternatively, the preprocessor may be enabled by default by adding the '\-\-lessopen' option to the configuration file. +Alternatively, the preprocessor may be enabled by default by adding the '\-\-lessopen' option to the configuration file. To temporarily disable the preprocessor if it is enabled by default, call: @@ -355,7 +337,7 @@ Enable the $LESSOPEN preprocessor. .IP Disable the $LESSOPEN preprocessor if enabled (overrides --lessopen) .PP -For more information, see the "INPUT PREPROCESSOR" section of less(1). +For more information, see the "INPUT PREPROCESSOR" section of less(1). .SH "MORE INFORMATION" diff --git a/doc/long-help.txt b/doc/long-help.txt index c374a039..8d5a3a5e 100644 --- a/doc/long-help.txt +++ b/doc/long-help.txt @@ -114,23 +114,6 @@ Options: add the '--theme="..."' option to the configuration file or export the BAT_THEME environment variable (e.g.: export BAT_THEME="..."). - --color-scheme - Specify whether to choose a dark or light syntax highlighting theme. Use '--theme-light' - and '--theme-dark' (or the environment variables BAT_THEME_LIGHT and BAT_THEME_DARK) to - configure which themes are picked. You may also use '--theme' to set a theme that is used - regardless of this choice. - - Possible values: - * auto (default): - Query the terminals for its color scheme if the output is not redirected. This is to - prevent race conditions with pagers such as less. - * 'auto:always': - Always query the terminal for its color scheme, regardless of whether or not the - output is redirected. - * dark: Use a dark syntax highlighting theme. - * light: Use a light syntax highlighting theme. - * system: Query the OS for its color scheme. Only works on macOS. - --theme-light Sets the theme name for syntax highlighting used when the terminal uses a light background. Use '--list-themes' to see all available themes. To set a default theme, add diff --git a/doc/short-help.txt b/doc/short-help.txt index f17a6d9d..d5c35059 100644 --- a/doc/short-help.txt +++ b/doc/short-help.txt @@ -41,8 +41,6 @@ Options: Use the specified syntax for files matching the glob pattern ('*.cpp:C++'). --theme Set the color theme for syntax highlighting. - --color-scheme - Specify whether to choose a dark or light theme. --theme-light Sets the color theme for syntax highlighting used for light backgrounds. --theme-dark diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index d2e5b4db..6d83c9c9 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -9,7 +9,7 @@ use crate::{ config::{get_args_from_config_file, get_args_from_env_opts_var, get_args_from_env_vars}, }; use bat::style::StyleComponentList; -use bat::theme::{theme, ColorSchemePreference, DetectColorScheme, ThemeOptions, ThemeRequest}; +use bat::theme::{theme, ThemeName, ThemeOptions, ThemePreference}; use bat::StripAnsiMode; use clap::ArgMatches; @@ -418,35 +418,20 @@ impl App { let theme = self .matches .get_one::("theme") - .map(|t| ThemeRequest::from_str(t).unwrap()); + .map(|t| ThemePreference::from_str(t).unwrap()) + .unwrap_or_default(); let theme_dark = self .matches .get_one::("theme-dark") - .map(|t| ThemeRequest::from_str(t).unwrap()); + .map(|t| ThemeName::from_str(t).unwrap()); let theme_light = self .matches .get_one::("theme-light") - .map(|t| ThemeRequest::from_str(t).unwrap()); + .map(|t| ThemeName::from_str(t).unwrap()); ThemeOptions { theme, theme_dark, theme_light, - color_scheme: self.color_scheme_preference(), - } - } - - pub(crate) fn color_scheme_preference(&self) -> ColorSchemePreference { - match self - .matches - .get_one::("color-scheme") - .map(|s| s.as_str()) - { - Some("auto") => ColorSchemePreference::Auto(DetectColorScheme::Auto), - Some("auto:always") => ColorSchemePreference::Auto(DetectColorScheme::Always), - Some("dark") => ColorSchemePreference::Dark, - Some("light") => ColorSchemePreference::Light, - Some("system") => ColorSchemePreference::System, - _ => unreachable!("other values for --color-scheme are not allowed"), } } } diff --git a/src/bin/bat/clap_app.rs b/src/bin/bat/clap_app.rs index 0857dba4..e8056f3a 100644 --- a/src/bin/bat/clap_app.rs +++ b/src/bin/bat/clap_app.rs @@ -383,32 +383,6 @@ pub fn build_app(interactive_output: bool) -> Command { BAT_THEME=\"...\").", ), ) - .arg( - Arg::new("color-scheme") - .long("color-scheme") - .overrides_with("color-scheme") - .value_name("scheme") - .value_parser(["auto", "auto:always", "dark", "light", "system"]) - .default_value("auto") - .hide_default_value(true) - .help("Specify whether to choose a dark or light theme.") - .long_help( - "Specify whether to choose a dark or light syntax highlighting theme. \ - Use '--theme-light' and '--theme-dark' (or the environment variables \ - BAT_THEME_LIGHT and BAT_THEME_DARK) to configure which themes are picked. \ - You may also use '--theme' to set a theme that is used regardless of this choice.\n\n\ - Possible values:\n\ - * auto (default):\n \ - Query the terminals for its color scheme if the output is not redirected. \ - This is to prevent race conditions with pagers such as less.\n\ - * 'auto:always':\n \ - Always query the terminal for its color scheme, \ - regardless of whether or not the output is redirected.\n\ - * dark: Use a dark syntax highlighting theme.\n\ - * light: Use a light syntax highlighting theme.\n\ - * system: Query the OS for its color scheme. Only works on macOS.\n\ - "), - ) .arg( Arg::new("theme-light") .long("theme-light") diff --git a/src/bin/bat/main.rs b/src/bin/bat/main.rs index 891390c1..09470504 100644 --- a/src/bin/bat/main.rs +++ b/src/bin/bat/main.rs @@ -384,7 +384,7 @@ fn run() -> Result { &config, config_dir, cache_dir, - app.color_scheme_preference(), + ColorSchemePreference::default(), )?; Ok(true) } else if app.matches.get_flag("config-file") { diff --git a/src/theme.rs b/src/theme.rs index 1c6d7e55..5f6460cc 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -29,46 +29,79 @@ pub fn color_scheme(preference: ColorSchemePreference) -> ColorScheme { #[derive(Debug, Default, PartialEq, Eq)] pub struct ThemeOptions { /// Always use this theme regardless of the terminal's background color. - pub theme: Option, + /// This corresponds with the `BAT_THEME` environment variable and the `--theme` option. + pub theme: ThemePreference, /// The theme to use in case the terminal uses a dark background with light text. - pub theme_dark: Option, + /// This corresponds with the `BAT_THEME_DARK` environment variable and the `--theme-dark` option. + pub theme_dark: Option, /// The theme to use in case the terminal uses a light background with dark text. - pub theme_light: Option, - /// How to choose between dark and light. - pub color_scheme: ColorSchemePreference, + /// This corresponds with the `BAT_THEME_LIGHT` environment variable and the `--theme-light` option. + pub theme_light: Option, +} + +/// What theme should `bat` use? +#[derive(Debug, PartialEq, Eq, Hash)] +pub enum ThemePreference { + /// Choose between [`ThemeOptions::theme_dark`] and [`ThemeOptions::theme_light`] + /// based on the terminal's (or the OS') color scheme. + Auto(ColorSchemePreference), + /// Always use the same theme regardless of the terminal's color scheme. + Fixed(ThemeName), +} + +impl Default for ThemePreference { + fn default() -> Self { + ThemePreference::Auto(ColorSchemePreference::default()) + } +} + +impl FromStr for ThemePreference { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + use ThemePreference::*; + match s { + "auto" => Ok(Auto(ColorSchemePreference::default())), + "auto:always" => Ok(Auto(ColorSchemePreference::Auto(DetectColorScheme::Always))), + "auto:system" => Ok(Auto(ColorSchemePreference::System)), + "dark" => Ok(Auto(ColorSchemePreference::Dark)), + "light" => Ok(Auto(ColorSchemePreference::Light)), + _ => ThemeName::from_str(s).map(Fixed), + } + } } /// The name of a theme or the default theme. /// /// ``` -/// # use bat::theme::ThemeRequest; +/// # use bat::theme::ThemeName; /// # use std::str::FromStr as _; -/// assert_eq!(ThemeRequest::Default, ThemeRequest::from_str("default").unwrap()); -/// assert_eq!(ThemeRequest::Named("example".to_string()), ThemeRequest::from_str("example").unwrap()); +/// assert_eq!(ThemeName::Default, ThemeName::from_str("default").unwrap()); +/// assert_eq!(ThemeName::Named("example".to_string()), ThemeName::from_str("example").unwrap()); /// ``` #[derive(Debug, PartialEq, Eq, Hash)] -pub enum ThemeRequest { +pub enum ThemeName { Named(String), Default, } -impl FromStr for ThemeRequest { +impl FromStr for ThemeName { type Err = Infallible; fn from_str(s: &str) -> Result { if s == "default" { - Ok(ThemeRequest::Default) + Ok(ThemeName::Default) } else { - Ok(ThemeRequest::Named(s.to_owned())) + Ok(ThemeName::Named(s.to_owned())) } } } -impl ThemeRequest { +impl ThemeName { fn into_theme(self, color_scheme: ColorScheme) -> String { match self { - ThemeRequest::Named(t) => t, - ThemeRequest::Default => default_theme(color_scheme).to_owned(), + ThemeName::Named(t) => t, + ThemeName::Default => default_theme(color_scheme).to_owned(), } } } @@ -124,17 +157,18 @@ fn color_scheme_impl( fn theme_from_detector(options: ThemeOptions, detector: &dyn ColorSchemeDetector) -> String { // Implementation note: This function is mostly pure (i.e. it has no side effects) for the sake of testing. // All the side effects (e.g. querying the terminal for its colors) are performed in the detector. - if let Some(theme) = options.theme { - theme.into_theme(ColorScheme::default()) - } else { - let color_scheme = color_scheme_impl(options.color_scheme, detector); - choose_theme(options, color_scheme) - .map(|t| t.into_theme(color_scheme)) - .unwrap_or_else(|| default_theme(color_scheme).to_owned()) + match options.theme { + ThemePreference::Fixed(theme_name) => theme_name.into_theme(ColorScheme::default()), + ThemePreference::Auto(color_scheme_preference) => { + let color_scheme = color_scheme_impl(color_scheme_preference, detector); + choose_theme(options, color_scheme) + .map(|t| t.into_theme(color_scheme)) + .unwrap_or_else(|| default_theme(color_scheme).to_owned()) + } } } -fn choose_theme(options: ThemeOptions, color_scheme: ColorScheme) -> Option { +fn choose_theme(options: ThemeOptions, color_scheme: ColorScheme) -> Option { match color_scheme { ColorScheme::Dark => options.theme_dark, ColorScheme::Light => options.theme_light, @@ -220,7 +254,6 @@ impl ColorSchemeDetector for Option { mod tests { use super::ColorScheme::*; use super::ColorSchemePreference as Pref; - use super::DetectColorScheme::*; use super::*; use std::cell::Cell; use std::iter; @@ -233,7 +266,7 @@ mod tests { for pref in [Pref::Dark, Pref::Light] { let detector = DetectorStub::should_detect(Some(Dark)); let options = ThemeOptions { - color_scheme: pref, + theme: ThemePreference::Auto(pref), ..Default::default() }; _ = theme_from_detector(options, &detector); @@ -249,7 +282,9 @@ mod tests { ]; for detector in detectors { let options = ThemeOptions { - color_scheme: Pref::Auto(Always), + theme: ThemePreference::Auto(ColorSchemePreference::Auto( + DetectColorScheme::Always, + )), ..Default::default() }; _ = theme_from_detector(options, &detector); @@ -280,13 +315,13 @@ mod tests { for color_scheme in optional(color_schemes()) { for options in [ ThemeOptions { - theme: Some(ThemeRequest::Named("Theme".to_string())), + theme: ThemePreference::Fixed(ThemeName::Named("Theme".to_string())), ..Default::default() }, ThemeOptions { - theme: Some(ThemeRequest::Named("Theme".to_string())), - theme_dark: Some(ThemeRequest::Named("Dark Theme".to_string())), - theme_light: Some(ThemeRequest::Named("Light Theme".to_string())), + theme: ThemePreference::Fixed(ThemeName::Named("Theme".to_string())), + theme_dark: Some(ThemeName::Named("Dark Theme".to_string())), + theme_light: Some(ThemeName::Named("Light Theme".to_string())), ..Default::default() }, ] { @@ -299,7 +334,7 @@ mod tests { #[test] fn detector_is_not_called_if_theme_is_present() { let options = ThemeOptions { - theme: Some(ThemeRequest::Named("Theme".to_string())), + theme: ThemePreference::Fixed(ThemeName::Named("Theme".to_string())), ..Default::default() }; let detector = DetectorStub::should_detect(Some(Dark)); @@ -326,7 +361,7 @@ mod tests { fn dark_if_requested_explicitly_through_theme() { for color_scheme in optional(color_schemes()) { let options = ThemeOptions { - theme: Some(ThemeRequest::Default), + theme: ThemePreference::Fixed(ThemeName::Default), ..Default::default() }; let detector = ConstantDetector(color_scheme); @@ -343,8 +378,8 @@ mod tests { for options in [ ThemeOptions::default(), ThemeOptions { - theme_dark: Some(ThemeRequest::Default), - theme_light: Some(ThemeRequest::Default), + theme_dark: Some(ThemeName::Default), + theme_light: Some(ThemeName::Default), ..Default::default() }, ] { @@ -365,8 +400,8 @@ mod tests { fn chooses_dark_theme_if_dark_or_unknown() { for color_scheme in [Some(Dark), None] { let options = ThemeOptions { - theme_dark: Some(ThemeRequest::Named("Dark".to_string())), - theme_light: Some(ThemeRequest::Named("Light".to_string())), + theme_dark: Some(ThemeName::Named("Dark".to_string())), + theme_light: Some(ThemeName::Named("Light".to_string())), ..Default::default() }; let detector = ConstantDetector(color_scheme); @@ -377,8 +412,8 @@ mod tests { #[test] fn chooses_light_theme_if_light() { let options = ThemeOptions { - theme_dark: Some(ThemeRequest::Named("Dark".to_string())), - theme_light: Some(ThemeRequest::Named("Light".to_string())), + theme_dark: Some(ThemeName::Named("Dark".to_string())), + theme_light: Some(ThemeName::Named("Light".to_string())), ..Default::default() }; let detector = ConstantDetector(Some(ColorScheme::Light)); diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index e4b73c59..582b255d 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -279,7 +279,6 @@ fn list_themes_with_colors() { bat() .arg("--color=always") - .arg("--color-scheme=dark") .arg("--list-themes") .assert() .success() @@ -296,7 +295,6 @@ fn list_themes_without_colors() { bat() .arg("--color=never") - .arg("--color-scheme=dark") .arg("--list-themes") .assert() .success() From 89ce06018340fda343072022000891e03a3b372b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Sun, 18 Aug 2024 20:32:23 +0200 Subject: [PATCH 19/28] Update help, man page and completions --- assets/completions/bat.bash.in | 7 ++++++- assets/completions/bat.fish.in | 10 +++++++++- assets/completions/bat.zsh.in | 10 ++++++++-- assets/manual/bat.1.in | 19 +++++++++++++++++-- doc/long-help.txt | 16 ++++++++++++---- src/bin/bat/clap_app.rs | 12 +++++++++--- src/theme.rs | 4 ++++ 7 files changed, 65 insertions(+), 13 deletions(-) diff --git a/assets/completions/bat.bash.in b/assets/completions/bat.bash.in index 162d1c53..90931f24 100644 --- a/assets/completions/bat.bash.in +++ b/assets/completions/bat.bash.in @@ -112,7 +112,12 @@ _bat() { COMPREPLY=($(compgen -c -- "$cur")) return 0 ;; - --theme | \ + --theme) + local IFS=$'\n' + COMPREPLY=($(compgen -W "auto${IFS}auto:always${IFS}auto:system${IFS}dark${IFS}light${IFS}$("$1" --list-themes)" -- "$cur")) + __bat_escape_completions + return 0 + ;; --theme-dark | \ --theme-light) local IFS=$'\n' diff --git a/assets/completions/bat.fish.in b/assets/completions/bat.fish.in index 9907718f..e2712706 100644 --- a/assets/completions/bat.fish.in +++ b/assets/completions/bat.fish.in @@ -129,6 +129,14 @@ set -l tabs_opts ' 8\t ' +set -l special_themes ' + auto\tdefault,\ Choose\ a\ theme\ based\ on\ dark\ or\ light\ mode + auto:always\tChoose\ a\ theme\ based\ on\ dark\ or\ light\ mode + auto:system\tChoose\ a\ theme\ based\ on\ dark\ or\ light\ mode + dark\tUse\ the\ theme\ specified\ by\ --theme-dark + light\tUse\ the\ theme\ specified\ by\ --theme-light +' + # Completions: complete -c $bat -l acknowledgements -d "Print acknowledgements" -n __fish_is_first_arg @@ -203,7 +211,7 @@ complete -c $bat -l tabs -x -a "$tabs_opts" -d "Set tab width" -n __bat_no_excl_ complete -c $bat -l terminal-width -x -d "Set terminal , +, or -" -n __bat_no_excl_args -complete -c $bat -l theme -x -a "(command $bat --list-themes | command cat)" -d "Set the syntax highlighting theme" -n __bat_no_excl_args +complete -c $bat -l theme -x -a "$special_themes(command $bat --list-themes | command cat)" -d "Set the syntax highlighting theme" -n __bat_no_excl_args complete -c $bat -l theme-dark -x -a "(command $bat --list-themes | command cat)" -d "Set the syntax highlighting theme for dark backgrounds" -n __bat_no_excl_args diff --git a/assets/completions/bat.zsh.in b/assets/completions/bat.zsh.in index bec0d3c9..76b981b6 100644 --- a/assets/completions/bat.zsh.in +++ b/assets/completions/bat.zsh.in @@ -42,7 +42,7 @@ _{{PROJECT_EXECUTABLE}}_main() { --decorations='[specify when to show the decorations]:when:(auto never always)' --paging='[specify when to use the pager]:when:(auto never always)' '(-m --map-syntax)'{-m+,--map-syntax=}'[map a glob pattern to an existing syntax name]: :->syntax-maps' - '(--theme)'--theme='[set the color theme for syntax highlighting]:theme:->themes' + '(--theme)'--theme='[set the color theme for syntax highlighting]:theme:->theme_preferences' '(--theme-dark)'--theme-dark='[set the color theme for syntax highlighting for dark backgrounds]:theme:->themes' '(--theme-light)'--theme-light='[set the color theme for syntax highlighting for light backgrounds]:theme:->themes' '(: --list-themes --list-languages -L)'--list-themes'[show all supported highlighting themes]' @@ -84,7 +84,13 @@ _{{PROJECT_EXECUTABLE}}_main() { themes) local -a themes expl - themes=( ${(f)"$(_call_program themes {{PROJECT_EXECUTABLE}} --list-themes)"} ) + themes=(${(f)"$(_call_program themes {{PROJECT_EXECUTABLE}} --list-themes)"} ) + + _wanted themes expl 'theme' compadd -a themes && ret=0 + ;; + theme_preferences) + local -a themes expl + themes=(auto dark light auto:always auto:system ${(f)"$(_call_program themes {{PROJECT_EXECUTABLE}} --list-themes)"} ) _wanted themes expl 'theme' compadd -a themes && ret=0 ;; diff --git a/assets/manual/bat.1.in b/assets/manual/bat.1.in index 05239d50..ccc70629 100644 --- a/assets/manual/bat.1.in +++ b/assets/manual/bat.1.in @@ -155,20 +155,35 @@ will use JSON syntax, and ignore '.dev' Set the theme for syntax highlighting. Use \fB\-\-list\-themes\fP to see all available themes. To set a default theme, add the \fB\-\-theme="..."\fP option to the configuration file or export the \fBBAT_THEME\fP environment variable (e.g.: \fBexport BAT_THEME="..."\fP). + +Special values: +.RS +.IP "auto (\fIdefault\fR)" +Picks a dark or light theme depending on the terminal's colors. +Use \fB-\-theme\-light\fR and \fB-\-theme\-dark\fR to customize the selected theme. +.IP "auto:always" +Variation of \fBauto\fR where where the terminal's colors are detected even when the output is redirected. +.IP "auto:system (macOS only)" +Variation of \fBauto\fR where the color scheme is detected from the system-wide preference instead. +.IP "dark" +Use the dark theme specified by \fB-\-theme-dark\fR. +.IP "light" +Use the light theme specified by \fB-\-theme-light\fR. +.RE .HP \fB\-\-theme\-dark\fR .IP Sets the theme name for syntax highlighting used when the terminal uses a dark background. To set a default theme, add the \fB\-\-theme-dark="..."\fP option to the configuration file or export the \fBBAT_THEME_DARK\fP environment variable (e.g. \fBexport BAT_THEME_DARK="..."\fP). -This option is ignored if \fB\-\-theme\fP option is set. +This option only has an effect when \fB\-\-theme\fP option is set to \fBauto\fR or \fBdark\fR. .HP \fB\-\-theme\-light\fR .IP Sets the theme name for syntax highlighting used when the terminal uses a dark background. To set a default theme, add the \fB\-\-theme-dark="..."\fP option to the configuration file or export the \fBBAT_THEME_LIGHT\fP environment variable (e.g. \fBexport BAT_THEME_LIGHT="..."\fP). -This option is ignored if \fB\-\-theme\fP option is set. +This option only has an effect when \fB\-\-theme\fP option is set to \fBauto\fR or \fBlight\fR. .HP \fB\-\-list\-themes\fR .IP diff --git a/doc/long-help.txt b/doc/long-help.txt index 8d5a3a5e..e93c8a2d 100644 --- a/doc/long-help.txt +++ b/doc/long-help.txt @@ -109,10 +109,18 @@ Options: 'bat --ignored-suffix ".dev" my_file.json.dev' will use JSON syntax, and ignore '.dev' --theme - Set the theme for syntax highlighting. Note that this option overrides '--theme-dark' and - '--theme-light'. Use '--list-themes' to see all available themes. To set a default theme, - add the '--theme="..."' option to the configuration file or export the BAT_THEME - environment variable (e.g.: export BAT_THEME="..."). + Set the theme for syntax highlighting. Use '--list-themes' to see all available themes. To + set a default theme, add the '--theme="..."' option to the configuration file or export + the BAT_THEME environment variable (e.g.: export BAT_THEME="..."). + + Special values: + + * auto: Picks a dark or light theme depending on the terminal's colors (default). + Use '--theme-light' and '--theme-dark' to customize the selected theme. + * auto:always: Detect the terminal's colors even when the output is redirected. + * auto:system: Detect the color scheme from the system-wide preference (macOS only). + * dark: Use the dark theme specified by '--theme-dark'. + * light: Use the light theme specified by '--theme-light'. --theme-light Sets the theme name for syntax highlighting used when the terminal uses a light diff --git a/src/bin/bat/clap_app.rs b/src/bin/bat/clap_app.rs index e8056f3a..0fb34748 100644 --- a/src/bin/bat/clap_app.rs +++ b/src/bin/bat/clap_app.rs @@ -375,12 +375,18 @@ pub fn build_app(interactive_output: bool) -> Command { .overrides_with("theme") .help("Set the color theme for syntax highlighting.") .long_help( - "Set the theme for syntax highlighting. Note that this option overrides \ - '--theme-dark' and '--theme-light'. Use '--list-themes' to \ + "Set the theme for syntax highlighting. Use '--list-themes' to \ see all available themes. To set a default theme, add the \ '--theme=\"...\"' option to the configuration file or export the \ BAT_THEME environment variable (e.g.: export \ - BAT_THEME=\"...\").", + BAT_THEME=\"...\").\n\n\ + Special values:\n\n \ + * auto: Picks a dark or light theme depending on the terminal's colors (default).\n \ + Use '--theme-light' and '--theme-dark' to customize the selected theme.\n \ + * auto:always: Detect the terminal's colors even when the output is redirected.\n \ + * auto:system: Detect the color scheme from the system-wide preference (macOS only).\n \ + * dark: Use the dark theme specified by '--theme-dark'.\n \ + * light: Use the light theme specified by '--theme-light'.", ), ) .arg( diff --git a/src/theme.rs b/src/theme.rs index 5f6460cc..dcac4e6c 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -216,6 +216,10 @@ impl ColorSchemeDetector for TerminalColorSchemeDetector { #[cfg(not(target_os = "macos"))] fn color_scheme_from_system() -> Option { + crate::bat_warning!( + "Theme 'auto:system' is only supported on macOS, \ + using default." + ); None } From 50958472e5b4bdc7451a609b877dc8d5f6a4860e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Fri, 23 Aug 2024 18:03:07 +0200 Subject: [PATCH 20/28] Return theme alongside detected color scheme --- src/bin/bat/app.rs | 2 +- src/theme.rs | 92 ++++++++++++++++++++++++++++++---------------- 2 files changed, 61 insertions(+), 33 deletions(-) diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index 6d83c9c9..f58d4965 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -254,7 +254,7 @@ impl App { Some("auto") => StripAnsiMode::Auto, _ => unreachable!("other values for --strip-ansi are not allowed"), }, - theme: theme(self.theme_options()), + theme: theme(self.theme_options()).to_string(), visible_lines: match self.matches.try_contains_id("diff").unwrap_or_default() && self.matches.get_flag("diff") { diff --git a/src/theme.rs b/src/theme.rs index dcac4e6c..ae36b6f9 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1,13 +1,18 @@ //! Utilities for choosing an appropriate theme for syntax highlighting. use std::convert::Infallible; +use std::fmt; use std::io::IsTerminal as _; use std::str::FromStr; /// Chooses an appropriate theme or falls back to a default theme /// based on the user-provided options and the color scheme of the terminal. -pub fn theme(options: ThemeOptions) -> String { - theme_from_detector(options, &TerminalColorSchemeDetector) +/// +/// Intentionally returns a [`ThemeResult`] instead of a simple string so +/// that downstream consumers such as `delta` can easily apply their own +/// default theme and can use the detected color scheme elsewhere. +pub fn theme(options: ThemeOptions) -> ThemeResult { + theme_impl(options, &TerminalColorSchemeDetector) } /// The default theme, suitable for the given color scheme. @@ -26,7 +31,7 @@ pub fn color_scheme(preference: ColorSchemePreference) -> ColorScheme { /// Options for configuring the theme used for syntax highlighting. /// Used together with [`theme`]. -#[derive(Debug, Default, PartialEq, Eq)] +#[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ThemeOptions { /// Always use this theme regardless of the terminal's background color. /// This corresponds with the `BAT_THEME` environment variable and the `--theme` option. @@ -40,7 +45,14 @@ pub struct ThemeOptions { } /// What theme should `bat` use? -#[derive(Debug, PartialEq, Eq, Hash)] +/// +/// The easiest way to construct this is from a string: +/// ``` +/// # use bat::theme::ThemePreference; +/// # use std::str::FromStr as _; +/// let preference = ThemePreference::from_str("auto:system").unwrap(); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ThemePreference { /// Choose between [`ThemeOptions::theme_dark`] and [`ThemeOptions::theme_light`] /// based on the terminal's (or the OS') color scheme. @@ -79,7 +91,7 @@ impl FromStr for ThemePreference { /// assert_eq!(ThemeName::Default, ThemeName::from_str("default").unwrap()); /// assert_eq!(ThemeName::Named("example".to_string()), ThemeName::from_str("example").unwrap()); /// ``` -#[derive(Debug, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ThemeName { Named(String), Default, @@ -97,15 +109,6 @@ impl FromStr for ThemeName { } } -impl ThemeName { - fn into_theme(self, color_scheme: ColorScheme) -> String { - match self { - ThemeName::Named(t) => t, - ThemeName::Default => default_theme(color_scheme).to_owned(), - } - } -} - /// How to choose between dark and light. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ColorSchemePreference { @@ -142,6 +145,26 @@ pub enum ColorScheme { Light, } +/// The resolved theme and the color scheme as determined from +/// the terminal, OS or fallback. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ThemeResult { + /// The theme selected according to the [`ThemeOptions`]. + pub theme: ThemeName, + /// Either the user's chosen color scheme, the terminal's color scheme, the OS's + /// color scheme or `None` if the color scheme was not detected because the user chose a fixed theme. + pub color_scheme: Option, +} + +impl fmt::Display for ThemeResult { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.theme { + ThemeName::Named(name) => f.write_str(name), + ThemeName::Default => f.write_str(default_theme(self.color_scheme.unwrap_or_default())), + } + } +} + fn color_scheme_impl( pref: ColorSchemePreference, detector: &dyn ColorSchemeDetector, @@ -154,16 +177,21 @@ fn color_scheme_impl( } } -fn theme_from_detector(options: ThemeOptions, detector: &dyn ColorSchemeDetector) -> String { +fn theme_impl(options: ThemeOptions, detector: &dyn ColorSchemeDetector) -> ThemeResult { // Implementation note: This function is mostly pure (i.e. it has no side effects) for the sake of testing. // All the side effects (e.g. querying the terminal for its colors) are performed in the detector. match options.theme { - ThemePreference::Fixed(theme_name) => theme_name.into_theme(ColorScheme::default()), - ThemePreference::Auto(color_scheme_preference) => { - let color_scheme = color_scheme_impl(color_scheme_preference, detector); - choose_theme(options, color_scheme) - .map(|t| t.into_theme(color_scheme)) - .unwrap_or_else(|| default_theme(color_scheme).to_owned()) + ThemePreference::Fixed(theme) => ThemeResult { + theme, + color_scheme: None, + }, + ThemePreference::Auto(pref) => { + let color_scheme = color_scheme_impl(pref, detector); + let theme = choose_theme(options, color_scheme).unwrap_or(ThemeName::Default); + ThemeResult { + theme, + color_scheme: Some(color_scheme), + } } } } @@ -273,7 +301,7 @@ mod tests { theme: ThemePreference::Auto(pref), ..Default::default() }; - _ = theme_from_detector(options, &detector); + _ = theme_impl(options, &detector); assert!(!detector.was_called.get()); } } @@ -291,7 +319,7 @@ mod tests { )), ..Default::default() }; - _ = theme_from_detector(options, &detector); + _ = theme_impl(options, &detector); assert!(detector.was_called.get()); } } @@ -299,14 +327,14 @@ mod tests { #[test] fn called_for_auto_if_should_detect() { let detector = DetectorStub::should_detect(Some(Dark)); - _ = theme_from_detector(ThemeOptions::default(), &detector); + _ = theme_impl(ThemeOptions::default(), &detector); assert!(detector.was_called.get()); } #[test] fn not_called_for_auto_if_not_should_detect() { let detector = DetectorStub::should_not_detect(); - _ = theme_from_detector(ThemeOptions::default(), &detector); + _ = theme_impl(ThemeOptions::default(), &detector); assert!(!detector.was_called.get()); } } @@ -330,7 +358,7 @@ mod tests { }, ] { let detector = ConstantDetector(color_scheme); - assert_eq!("Theme", theme_from_detector(options, &detector)); + assert_eq!("Theme", theme_impl(options, &detector).to_string()); } } } @@ -342,7 +370,7 @@ mod tests { ..Default::default() }; let detector = DetectorStub::should_detect(Some(Dark)); - _ = theme_from_detector(options, &detector); + _ = theme_impl(options, &detector); assert!(!detector.was_called.get()); } } @@ -355,7 +383,7 @@ mod tests { let detector = ConstantDetector(None); assert_eq!( default_theme(ColorScheme::Dark), - theme_from_detector(ThemeOptions::default(), &detector) + theme_impl(ThemeOptions::default(), &detector).to_string() ); } @@ -371,7 +399,7 @@ mod tests { let detector = ConstantDetector(color_scheme); assert_eq!( default_theme(ColorScheme::Dark), - theme_from_detector(options, &detector) + theme_impl(options, &detector).to_string() ); } } @@ -390,7 +418,7 @@ mod tests { let detector = ConstantDetector(Some(color_scheme)); assert_eq!( default_theme(color_scheme), - theme_from_detector(options, &detector) + theme_impl(options, &detector).to_string() ); } } @@ -409,7 +437,7 @@ mod tests { ..Default::default() }; let detector = ConstantDetector(color_scheme); - assert_eq!("Dark", theme_from_detector(options, &detector)); + assert_eq!("Dark", theme_impl(options, &detector).to_string()); } } @@ -421,7 +449,7 @@ mod tests { ..Default::default() }; let detector = ConstantDetector(Some(ColorScheme::Light)); - assert_eq!("Light", theme_from_detector(options, &detector)); + assert_eq!("Light", theme_impl(options, &detector).to_string()); } } From 16d9b99f6cbdf4529f8e638f22cd1561ee95160e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Wed, 4 Sep 2024 21:18:29 +0200 Subject: [PATCH 21/28] Flatten preference enum --- src/bin/bat/main.rs | 14 ++--- src/theme.rs | 121 ++++++++++++++++++++------------------------ 2 files changed, 60 insertions(+), 75 deletions(-) diff --git a/src/bin/bat/main.rs b/src/bin/bat/main.rs index 09470504..2b27eff4 100644 --- a/src/bin/bat/main.rs +++ b/src/bin/bat/main.rs @@ -14,6 +14,7 @@ use std::io::{BufReader, Write}; use std::path::Path; use std::process; +use bat::theme::DetectColorScheme; use nu_ansi_term::Color::Green; use nu_ansi_term::Style; @@ -35,7 +36,7 @@ use bat::{ error::*, input::Input, style::{StyleComponent, StyleComponents}, - theme::{color_scheme, default_theme, ColorScheme, ColorSchemePreference}, + theme::{color_scheme, default_theme, ColorScheme}, MappingTarget, PagingMode, }; @@ -193,7 +194,7 @@ pub fn list_themes( cfg: &Config, config_dir: &Path, cache_dir: &Path, - color_scheme_pref: ColorSchemePreference, + detect_color_scheme: DetectColorScheme, ) -> Result<()> { let assets = assets_from_cache_or_binary(cfg.use_custom_assets, cache_dir)?; let mut config = cfg.clone(); @@ -205,7 +206,7 @@ pub fn list_themes( let stdout = io::stdout(); let mut stdout = stdout.lock(); - let default_theme_name = default_theme(color_scheme(color_scheme_pref)); + let default_theme_name = default_theme(color_scheme(detect_color_scheme).unwrap_or_default()); for theme in assets.themes() { let default_theme_info = if default_theme_name == theme { " (default)" @@ -380,12 +381,7 @@ fn run() -> Result { }; run_controller(inputs, &plain_config, cache_dir) } else if app.matches.get_flag("list-themes") { - list_themes( - &config, - config_dir, - cache_dir, - ColorSchemePreference::default(), - )?; + list_themes(&config, config_dir, cache_dir, DetectColorScheme::default())?; Ok(true) } else if app.matches.get_flag("config-file") { println!("{}", config_file().to_string_lossy()); diff --git a/src/theme.rs b/src/theme.rs index ae36b6f9..7b41ff4a 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -25,8 +25,8 @@ pub const fn default_theme(color_scheme: ColorScheme) -> &'static str { } /// Detects the color scheme from the terminal. -pub fn color_scheme(preference: ColorSchemePreference) -> ColorScheme { - color_scheme_impl(preference, &TerminalColorSchemeDetector) +pub fn color_scheme(when: DetectColorScheme) -> Option { + detect(when, &TerminalColorSchemeDetector) } /// Options for configuring the theme used for syntax highlighting. @@ -55,15 +55,19 @@ pub struct ThemeOptions { #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ThemePreference { /// Choose between [`ThemeOptions::theme_dark`] and [`ThemeOptions::theme_light`] - /// based on the terminal's (or the OS') color scheme. - Auto(ColorSchemePreference), + /// based on the terminal's color scheme. + Auto(DetectColorScheme), /// Always use the same theme regardless of the terminal's color scheme. Fixed(ThemeName), + /// Use a dark theme. + Dark, + /// Use a light theme. + Light, } impl Default for ThemePreference { fn default() -> Self { - ThemePreference::Auto(ColorSchemePreference::default()) + ThemePreference::Auto(Default::default()) } } @@ -73,11 +77,11 @@ impl FromStr for ThemePreference { fn from_str(s: &str) -> Result { use ThemePreference::*; match s { - "auto" => Ok(Auto(ColorSchemePreference::default())), - "auto:always" => Ok(Auto(ColorSchemePreference::Auto(DetectColorScheme::Always))), - "auto:system" => Ok(Auto(ColorSchemePreference::System)), - "dark" => Ok(Auto(ColorSchemePreference::Dark)), - "light" => Ok(Auto(ColorSchemePreference::Light)), + "auto" => Ok(Auto(Default::default())), + "auto:always" => Ok(Auto(DetectColorScheme::Always)), + "auto:system" => Ok(Auto(DetectColorScheme::System)), + "dark" => Ok(Dark), + "light" => Ok(Light), _ => ThemeName::from_str(s).map(Fixed), } } @@ -109,25 +113,6 @@ impl FromStr for ThemeName { } } -/// How to choose between dark and light. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ColorSchemePreference { - /// Detect the color scheme from the terminal. - Auto(DetectColorScheme), - /// Use a dark theme. - Dark, - /// Use a light theme. - Light, - /// Detect the color scheme from the OS instead (macOS only). - System, -} - -impl Default for ColorSchemePreference { - fn default() -> Self { - Self::Auto(DetectColorScheme::default()) - } -} - #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] pub enum DetectColorScheme { /// Only query the terminal for its colors when appropriate (i.e. when the the output is not redirected). @@ -135,6 +120,8 @@ pub enum DetectColorScheme { Auto, /// Always query the terminal for its colors. Always, + /// Detect the system-wide dark/light preference (macOS only). + System, } /// The color scheme used to pick a fitting theme. Defaults to [`ColorScheme::Dark`]. @@ -165,18 +152,6 @@ impl fmt::Display for ThemeResult { } } -fn color_scheme_impl( - pref: ColorSchemePreference, - detector: &dyn ColorSchemeDetector, -) -> ColorScheme { - match pref { - ColorSchemePreference::Auto(when) => detect(when, detector).unwrap_or_default(), - ColorSchemePreference::Dark => ColorScheme::Dark, - ColorSchemePreference::Light => ColorScheme::Light, - ColorSchemePreference::System => color_scheme_from_system().unwrap_or_default(), - } -} - fn theme_impl(options: ThemeOptions, detector: &dyn ColorSchemeDetector) -> ThemeResult { // Implementation note: This function is mostly pure (i.e. it has no side effects) for the sake of testing. // All the side effects (e.g. querying the terminal for its colors) are performed in the detector. @@ -185,14 +160,18 @@ fn theme_impl(options: ThemeOptions, detector: &dyn ColorSchemeDetector) -> Them theme, color_scheme: None, }, - ThemePreference::Auto(pref) => { - let color_scheme = color_scheme_impl(pref, detector); - let theme = choose_theme(options, color_scheme).unwrap_or(ThemeName::Default); - ThemeResult { - theme, - color_scheme: Some(color_scheme), - } - } + ThemePreference::Dark => choose_theme_opt(Some(ColorScheme::Dark), options), + ThemePreference::Light => choose_theme_opt(Some(ColorScheme::Light), options), + ThemePreference::Auto(when) => choose_theme_opt(detect(when, detector), options), + } +} + +fn choose_theme_opt(color_scheme: Option, options: ThemeOptions) -> ThemeResult { + ThemeResult { + color_scheme, + theme: color_scheme + .and_then(|c| choose_theme(options, c)) + .unwrap_or(ThemeName::Default), } } @@ -207,6 +186,7 @@ fn detect(when: DetectColorScheme, detector: &dyn ColorSchemeDetector) -> Option let should_detect = match when { DetectColorScheme::Auto => detector.should_detect(), DetectColorScheme::Always => true, + DetectColorScheme::System => return color_scheme_from_system(), }; should_detect.then(|| detector.detect()).flatten() } @@ -285,7 +265,6 @@ impl ColorSchemeDetector for Option { #[cfg(test)] mod tests { use super::ColorScheme::*; - use super::ColorSchemePreference as Pref; use super::*; use std::cell::Cell; use std::iter; @@ -295,10 +274,10 @@ mod tests { #[test] fn not_called_for_dark_or_light() { - for pref in [Pref::Dark, Pref::Light] { + for theme in [ThemePreference::Dark, ThemePreference::Light] { let detector = DetectorStub::should_detect(Some(Dark)); let options = ThemeOptions { - theme: ThemePreference::Auto(pref), + theme, ..Default::default() }; _ = theme_impl(options, &detector); @@ -314,9 +293,7 @@ mod tests { ]; for detector in detectors { let options = ThemeOptions { - theme: ThemePreference::Auto(ColorSchemePreference::Auto( - DetectColorScheme::Always, - )), + theme: ThemePreference::Auto(DetectColorScheme::Always), ..Default::default() }; _ = theme_impl(options, &detector); @@ -379,7 +356,7 @@ mod tests { use super::*; #[test] - fn dark_if_unable_to_detect_color_scheme() { + fn default_dark_if_unable_to_detect_color_scheme() { let detector = ConstantDetector(None); assert_eq!( default_theme(ColorScheme::Dark), @@ -390,7 +367,7 @@ mod tests { // For backwards compatibility, if the default theme is requested // explicitly through BAT_THEME, we always pick the default dark theme. #[test] - fn dark_if_requested_explicitly_through_theme() { + fn default_dark_if_requested_explicitly_through_theme() { for color_scheme in optional(color_schemes()) { let options = ThemeOptions { theme: ThemePreference::Fixed(ThemeName::Default), @@ -428,17 +405,29 @@ mod tests { mod choosing { use super::*; + #[test] + fn chooses_default_theme_if_unknown() { + let options = ThemeOptions { + theme_dark: Some(ThemeName::Named("Dark".to_string())), + theme_light: Some(ThemeName::Named("Light".to_string())), + ..Default::default() + }; + let detector = ConstantDetector(None); + assert_eq!( + default_theme(ColorScheme::default()), + theme_impl(options, &detector).to_string() + ); + } + #[test] fn chooses_dark_theme_if_dark_or_unknown() { - for color_scheme in [Some(Dark), None] { - let options = ThemeOptions { - theme_dark: Some(ThemeName::Named("Dark".to_string())), - theme_light: Some(ThemeName::Named("Light".to_string())), - ..Default::default() - }; - let detector = ConstantDetector(color_scheme); - assert_eq!("Dark", theme_impl(options, &detector).to_string()); - } + let options = ThemeOptions { + theme_dark: Some(ThemeName::Named("Dark".to_string())), + theme_light: Some(ThemeName::Named("Light".to_string())), + ..Default::default() + }; + let detector = ConstantDetector(Some(ColorScheme::Dark)); + assert_eq!("Dark", theme_impl(options, &detector).to_string()); } #[test] From e075fee5bf2fe6b40e16bb4ac02f88bcfe1fe782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Sat, 7 Sep 2024 20:24:57 +0200 Subject: [PATCH 22/28] Add infallible constructor --- src/theme.rs | 60 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/src/theme.rs b/src/theme.rs index 7b41ff4a..41ce84e1 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -48,9 +48,9 @@ pub struct ThemeOptions { /// /// The easiest way to construct this is from a string: /// ``` -/// # use bat::theme::ThemePreference; -/// # use std::str::FromStr as _; -/// let preference = ThemePreference::from_str("auto:system").unwrap(); +/// # use bat::theme::{ThemePreference, DetectColorScheme}; +/// let preference = ThemePreference::new("auto:system"); +/// assert_eq!(ThemePreference::Auto(DetectColorScheme::System), preference); /// ``` #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ThemePreference { @@ -71,19 +71,26 @@ impl Default for ThemePreference { } } +impl ThemePreference { + /// Creates a theme preference from a string. + pub fn new(s: &str) -> Self { + use ThemePreference::*; + match s { + "auto" => Auto(Default::default()), + "auto:always" => Auto(DetectColorScheme::Always), + "auto:system" => Auto(DetectColorScheme::System), + "dark" => Dark, + "light" => Light, + _ => Fixed(ThemeName::new(s)), + } + } +} + impl FromStr for ThemePreference { type Err = Infallible; fn from_str(s: &str) -> Result { - use ThemePreference::*; - match s { - "auto" => Ok(Auto(Default::default())), - "auto:always" => Ok(Auto(DetectColorScheme::Always)), - "auto:system" => Ok(Auto(DetectColorScheme::System)), - "dark" => Ok(Dark), - "light" => Ok(Light), - _ => ThemeName::from_str(s).map(Fixed), - } + Ok(ThemePreference::new(s)) } } @@ -91,9 +98,8 @@ impl FromStr for ThemePreference { /// /// ``` /// # use bat::theme::ThemeName; -/// # use std::str::FromStr as _; -/// assert_eq!(ThemeName::Default, ThemeName::from_str("default").unwrap()); -/// assert_eq!(ThemeName::Named("example".to_string()), ThemeName::from_str("example").unwrap()); +/// assert_eq!(ThemeName::Default, ThemeName::new("default")); +/// assert_eq!(ThemeName::Named("example".to_string()), ThemeName::new("example")); /// ``` #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ThemeName { @@ -101,14 +107,30 @@ pub enum ThemeName { Default, } +impl ThemeName { + /// Creates a theme name from a string. + pub fn new(s: &str) -> Self { + if s == "default" { + ThemeName::Default + } else { + ThemeName::Named(s.to_owned()) + } + } +} + impl FromStr for ThemeName { type Err = Infallible; fn from_str(s: &str) -> Result { - if s == "default" { - Ok(ThemeName::Default) - } else { - Ok(ThemeName::Named(s.to_owned())) + Ok(ThemeName::new(s)) + } +} + +impl fmt::Display for ThemeName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ThemeName::Named(t) => f.write_str(t), + ThemeName::Default => f.write_str("default"), } } } From 60e402733241f2154688722fbe75225fc4c06c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Sat, 7 Sep 2024 21:34:05 +0200 Subject: [PATCH 23/28] Expose theme env vars --- src/bin/bat/config.rs | 6 +++--- src/theme.rs | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/bin/bat/config.rs b/src/bin/bat/config.rs index f1ec3d53..a0ee7ba3 100644 --- a/src/bin/bat/config.rs +++ b/src/bin/bat/config.rs @@ -140,9 +140,9 @@ fn get_args_from_str(content: &str) -> Result, shell_words::ParseE pub fn get_args_from_env_vars() -> Vec { [ ("--tabs", "BAT_TABS"), - ("--theme", "BAT_THEME"), - ("--theme-dark", "BAT_THEME_DARK"), - ("--theme-light", "BAT_THEME_LIGHT"), + ("--theme", bat::theme::env::BAT_THEME), + ("--theme-dark", bat::theme::env::BAT_THEME_DARK), + ("--theme-light", bat::theme::env::BAT_THEME_LIGHT), ("--pager", "BAT_PAGER"), ("--paging", "BAT_PAGING"), ("--style", "BAT_STYLE"), diff --git a/src/theme.rs b/src/theme.rs index 41ce84e1..0276e5e0 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -5,6 +5,16 @@ use std::fmt; use std::io::IsTerminal as _; use std::str::FromStr; +/// Environment variable names. +pub mod env { + /// See [`crate::theme::ThemeOptions::theme`]. + pub const BAT_THEME: &str = "BAT_THEME"; + /// See [`crate::theme::ThemeOptions::theme_dark`]. + pub const BAT_THEME_DARK: &str = "BAT_THEME"; + /// See [`crate::theme::ThemeOptions::theme_light`]. + pub const BAT_THEME_LIGHT: &str = "BAT_THEME"; +} + /// Chooses an appropriate theme or falls back to a default theme /// based on the user-provided options and the color scheme of the terminal. /// From 10e823c4b760870eac4569e42d07704cffd3e79e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Sat, 7 Sep 2024 21:34:44 +0200 Subject: [PATCH 24/28] Rename internal function --- src/theme.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/theme.rs b/src/theme.rs index 0276e5e0..71c09e98 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -36,7 +36,7 @@ pub const fn default_theme(color_scheme: ColorScheme) -> &'static str { /// Detects the color scheme from the terminal. pub fn color_scheme(when: DetectColorScheme) -> Option { - detect(when, &TerminalColorSchemeDetector) + color_scheme_impl(when, &TerminalColorSchemeDetector) } /// Options for configuring the theme used for syntax highlighting. @@ -194,7 +194,7 @@ fn theme_impl(options: ThemeOptions, detector: &dyn ColorSchemeDetector) -> Them }, ThemePreference::Dark => choose_theme_opt(Some(ColorScheme::Dark), options), ThemePreference::Light => choose_theme_opt(Some(ColorScheme::Light), options), - ThemePreference::Auto(when) => choose_theme_opt(detect(when, detector), options), + ThemePreference::Auto(when) => choose_theme_opt(color_scheme_impl(when, detector), options), } } @@ -214,7 +214,10 @@ fn choose_theme(options: ThemeOptions, color_scheme: ColorScheme) -> Option Option { +fn color_scheme_impl( + when: DetectColorScheme, + detector: &dyn ColorSchemeDetector, +) -> Option { let should_detect = match when { DetectColorScheme::Auto => detector.should_detect(), DetectColorScheme::Always => true, From f6cbee9e270b18681c2b39ad507bb128b98a35a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Sat, 7 Sep 2024 21:35:37 +0200 Subject: [PATCH 25/28] Update docs --- src/theme.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/theme.rs b/src/theme.rs index 71c09e98..b8c4c67b 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -43,7 +43,8 @@ pub fn color_scheme(when: DetectColorScheme) -> Option { /// Used together with [`theme`]. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ThemeOptions { - /// Always use this theme regardless of the terminal's background color. + /// Configures how the theme is chosen. If set to a [`ThemePreference::Fixed`] value, + /// then the given theme is used regardless of the terminal's background color. /// This corresponds with the `BAT_THEME` environment variable and the `--theme` option. pub theme: ThemePreference, /// The theme to use in case the terminal uses a dark background with light text. From 0ebb9cbfe2651127d22cea12ca359db5e26c8595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Sat, 7 Sep 2024 21:57:27 +0200 Subject: [PATCH 26/28] Add Display impl --- src/theme.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/theme.rs b/src/theme.rs index b8c4c67b..5fed4faa 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -105,6 +105,20 @@ impl FromStr for ThemePreference { } } +impl fmt::Display for ThemePreference { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use ThemePreference::*; + match self { + Auto(DetectColorScheme::Auto) => f.write_str("auto"), + Auto(DetectColorScheme::Always) => f.write_str("auto:always"), + Auto(DetectColorScheme::System) => f.write_str("auto:system"), + Fixed(theme) => theme.fmt(f), + Dark => f.write_str("dark"), + Light => f.write_str("light"), + } + } +} + /// The name of a theme or the default theme. /// /// ``` @@ -478,6 +492,26 @@ mod tests { } } + mod theme_preference { + use super::*; + + #[test] + fn values_roundtrip_via_display() { + let prefs = [ + ThemePreference::Auto(DetectColorScheme::Auto), + ThemePreference::Auto(DetectColorScheme::Always), + ThemePreference::Auto(DetectColorScheme::System), + ThemePreference::Fixed(ThemeName::Default), + ThemePreference::Fixed(ThemeName::new("foo")), + ThemePreference::Dark, + ThemePreference::Light, + ]; + for pref in prefs { + assert_eq!(pref, ThemePreference::new(&pref.to_string())); + } + } + } + struct DetectorStub { should_detect: bool, color_scheme: Option, From 02ae6ef348d19dd5c117c0d53733c4df076ec87b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Sat, 7 Sep 2024 22:59:27 +0200 Subject: [PATCH 27/28] Remove redundant guard --- src/theme.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/theme.rs b/src/theme.rs index 5fed4faa..dcc6ee42 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -292,7 +292,7 @@ fn color_scheme_from_system() -> Option { match plist::Value::from_file(preferences_file).map(|file| file.into_dictionary()) { Ok(Some(preferences)) => match preferences.get(STYLE_KEY).and_then(|val| val.as_string()) { - Some(value) if value == "Dark" => Some(ColorScheme::Dark), + Some("Dark") => Some(ColorScheme::Dark), // If the key does not exist, then light theme is currently in use. Some(_) | None => Some(ColorScheme::Light), }, From b7471847889ee2f0faab7e9f9d12df70d351edd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Sun, 8 Sep 2024 17:10:46 +0200 Subject: [PATCH 28/28] Accept `impl Into` to avoid cloning strings --- src/theme.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/theme.rs b/src/theme.rs index dcc6ee42..9fbef238 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -84,9 +84,10 @@ impl Default for ThemePreference { impl ThemePreference { /// Creates a theme preference from a string. - pub fn new(s: &str) -> Self { + pub fn new(s: impl Into) -> Self { use ThemePreference::*; - match s { + let s = s.into(); + match s.as_str() { "auto" => Auto(Default::default()), "auto:always" => Auto(DetectColorScheme::Always), "auto:system" => Auto(DetectColorScheme::System), @@ -134,11 +135,12 @@ pub enum ThemeName { impl ThemeName { /// Creates a theme name from a string. - pub fn new(s: &str) -> Self { + pub fn new(s: impl Into) -> Self { + let s = s.into(); if s == "default" { ThemeName::Default } else { - ThemeName::Named(s.to_owned()) + ThemeName::Named(s) } } }