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()