From 2f76c56d91d3d49feb170b89d7526e0272634998 Mon Sep 17 00:00:00 2001 From: Tau Date: Tue, 12 Mar 2024 16:05:44 +0100 Subject: Detect Dark/Light Mode from Terminal (#1615) --- Cargo.lock | 39 +++++++++++++++++++++-- Cargo.toml | 1 + src/cli.rs | 36 ++++++++++++++++++++- src/features/side_by_side.rs | 2 ++ src/options/set.rs | 1 + src/options/theme.rs | 76 +++++++++++++++++++++++++++++++++++++++++--- 6 files changed, 148 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d4344afa..de5486e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -581,6 +581,7 @@ dependencies = [ "smol_str", "syntect", "sysinfo", + "terminal-colorsaurus", "unicode-segmentation", "unicode-width", "xdg", @@ -753,9 +754,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.151" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libgit2-sys" @@ -834,6 +835,17 @@ dependencies = [ "adler", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "nix" version = "0.27.1" @@ -1295,6 +1307,29 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal-colorsaurus" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c374383f597b763eb3bd06bc4e18f85510a52d7f1ac762f0c7e413ce696079fc" +dependencies = [ + "libc", + "memchr", + "mio", + "terminal-trx", + "thiserror", +] + +[[package]] +name = "terminal-trx" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a4af7c93f02d5bd5e120c812f7fb413003b7060e8a22d0ea90346f1be769210" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "terminal_size" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 0a714f0e..8d0917be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ unicode-segmentation = "1.10.1" unicode-width = "0.1.10" xdg = "2.4.1" clap_complete = "4.4.4" +terminal-colorsaurus = "0.3.1" [dependencies.git2] version = "0.18.2" diff --git a/src/cli.rs b/src/cli.rs index d098e736..4cddd6fb 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -3,7 +3,7 @@ use std::ffi::OsString; use std::path::{Path, PathBuf}; use bat::assets::HighlightingAssets; -use clap::{ColorChoice, CommandFactory, FromArgMatches, Parser, ValueHint}; +use clap::{ColorChoice, CommandFactory, FromArgMatches, Parser, ValueEnum, ValueHint}; use clap_complete::Shell; use lazy_static::lazy_static; use syntect::highlighting::Theme as SyntaxTheme; @@ -294,6 +294,29 @@ pub struct Opt { /// set this in per-repository git config (.git/config) pub default_language: Option, + /// Detect whether or not the terminal is dark or light by querying for its colors. + /// + /// Ignored if either `--dark` or `--light` is specified. + /// + /// Querying the terminal for its colors requires "exclusive" access + /// since delta reads/writes from the terminal and enables/disables raw mode. + /// This causes race conditions with pagers such as less when they are attached to the + /// same terminal as delta. + /// + /// This is usually only an issue when the output is manually piped to a pager. + /// For example: `git diff | delta | less`. + /// Otherwise, if delta starts the pager itself, then there's no race condition + /// since the pager is started *after* the color is detected. + /// + /// `auto` tries to account for these situations by testing if the output is redirected. + /// + /// The `--color-only` option is treated as an indicator that delta is used + /// as `interactive.diffFilter`. In this case the color is queried from the terminal even + /// though the output is redirected. + /// + #[arg(long = "detect-dark-light", value_enum, default_value_t = DetectDarkLight::default())] + pub detect_dark_light: DetectDarkLight, + #[arg(long = "diff-highlight")] /// Emulate diff-highlight. /// @@ -1124,6 +1147,17 @@ pub enum InspectRawLines { False, } +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)] +pub enum DetectDarkLight { + /// Only query the terminal for its colors if the output is not redirected. + #[default] + Auto, + /// Always query the terminal for its colors. + Always, + /// Never query the terminal for its colors. + Never, +} + impl Opt { pub fn from_args_and_git_config( env: DeltaEnv, diff --git a/src/features/side_by_side.rs b/src/features/side_by_side.rs index 82d66593..a8182359 100644 --- a/src/features/side_by_side.rs +++ b/src/features/side_by_side.rs @@ -592,6 +592,7 @@ pub mod ansifill { pub mod tests { use crate::ansi::strip_ansi_codes; use crate::features::line_numbers::tests::*; + use crate::options::theme; use crate::tests::integration_test_utils::{make_config_from_args, run_delta, DeltaTest}; #[test] @@ -642,6 +643,7 @@ pub mod tests { #[test] fn test_two_plus_lines_spaces_and_ansi() { + let _override = theme::test_utils::DetectLightModeOverride::new(false); DeltaTest::with_args(&[ "--side-by-side", "--width", diff --git a/src/options/set.rs b/src/options/set.rs index 262096c5..cfb42987 100644 --- a/src/options/set.rs +++ b/src/options/set.rs @@ -43,6 +43,7 @@ macro_rules! set_options { "24-bit-color", "diff-highlight", // Does not exist as a flag on config "diff-so-fancy", // Does not exist as a flag on config + "detect-dark-light", // Does not exist as a flag on config "features", // Processed differently // Set prior to the rest "no-gitconfig", diff --git a/src/options/theme.rs b/src/options/theme.rs index cb7b9b04..f53a0821 100644 --- a/src/options/theme.rs +++ b/src/options/theme.rs @@ -1,3 +1,5 @@ +use std::io::{stdout, IsTerminal}; + /// Delta doesn't have a formal concept of a "theme". What it has is /// (a) the choice of syntax-highlighting theme /// (b) the choice of light-background-mode vs dark-background-mode, which determine certain @@ -9,7 +11,7 @@ use bat; use bat::assets::HighlightingAssets; -use crate::cli; +use crate::cli::{self, DetectDarkLight}; #[allow(non_snake_case)] pub fn set__is_light_mode__syntax_theme__syntax_set( @@ -20,7 +22,7 @@ pub fn set__is_light_mode__syntax_theme__syntax_set( let (is_light_mode, syntax_theme_name) = get_is_light_mode_and_syntax_theme_name( opt.syntax_theme.as_ref(), syntax_theme_name_from_bat_theme.as_ref(), - opt.light, + get_is_light(opt), ); opt.computed.is_light_mode = is_light_mode; @@ -84,9 +86,9 @@ fn is_no_syntax_highlighting_syntax_theme_name(theme_name: &str) -> bool { fn get_is_light_mode_and_syntax_theme_name( theme_arg: Option<&String>, bat_theme_env_var: Option<&String>, - light_mode_arg: bool, + light_mode: bool, ) -> (bool, String) { - match (theme_arg, bat_theme_env_var, light_mode_arg) { + match (theme_arg, bat_theme_env_var, light_mode) { (None, None, false) => (false, DEFAULT_DARK_SYNTAX_THEME.to_string()), (Some(theme_name), _, false) => (is_light_syntax_theme(theme_name), theme_name.to_string()), (None, Some(theme_name), false) => { @@ -98,8 +100,72 @@ fn get_is_light_mode_and_syntax_theme_name( } } +fn get_is_light(opt: &cli::Opt) -> bool { + get_is_light_opt(opt) + .or_else(|| should_detect_dark_light(opt).then(detect_light_mode)) + .unwrap_or_default() +} + +fn get_is_light_opt(opt: &cli::Opt) -> Option { + if opt.light { + Some(true) + } else if opt.dark { + Some(false) + } else { + None + } +} + +/// See [`cli::Opt::detect_dark_light`] for a detailed explanation. +fn should_detect_dark_light(opt: &cli::Opt) -> bool { + match opt.detect_dark_light { + DetectDarkLight::Auto => opt.color_only || stdout().is_terminal(), + DetectDarkLight::Always => true, + DetectDarkLight::Never => false, + } +} + +fn detect_light_mode() -> bool { + use terminal_colorsaurus::{color_scheme, QueryOptions}; + + #[cfg(test)] + if let Some(value) = test_utils::DETECT_LIGHT_MODE_OVERRIDE.get() { + return value; + } + + color_scheme(QueryOptions::default()) + .map(|c| c.is_dark_on_light()) + .unwrap_or_default() +} + +#[cfg(test)] +pub(crate) mod test_utils { + thread_local! { + pub(super) static DETECT_LIGHT_MODE_OVERRIDE: std::cell::Cell> = std::cell::Cell::new(None); + } + + pub(crate) struct DetectLightModeOverride { + old_value: Option, + } + + impl DetectLightModeOverride { + pub(crate) fn new(value: bool) -> Self { + let old_value = DETECT_LIGHT_MODE_OVERRIDE.get(); + DETECT_LIGHT_MODE_OVERRIDE.set(Some(value)); + DetectLightModeOverride { old_value } + } + } + + impl Drop for DetectLightModeOverride { + fn drop(&mut self) { + DETECT_LIGHT_MODE_OVERRIDE.set(self.old_value) + } + } +} + #[cfg(test)] mod tests { + use super::test_utils::DetectLightModeOverride; use super::*; use crate::color; use crate::tests::integration_test_utils; @@ -107,6 +173,8 @@ mod tests { // TODO: Test influence of BAT_THEME env var. E.g. see utils::process::tests::FakeParentArgs. #[test] fn test_syntax_theme_selection() { + let _override = DetectLightModeOverride::new(false); + #[derive(PartialEq)] enum Mode { Light, -- cgit v1.2.3