Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect Dark/Light Mode from Terminal #1615

Merged
merged 7 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
36 changes: 35 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -294,6 +294,29 @@ pub struct Opt {
/// set this in per-repository git config (.git/config)
pub default_language: Option<String>,

/// 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,
bash marked this conversation as resolved.
Show resolved Hide resolved

#[arg(long = "diff-highlight")]
/// Emulate diff-highlight.
///
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/features/side_by_side.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/options/set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
76 changes: 72 additions & 4 deletions src/options/theme.rs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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(
Expand All @@ -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;

Expand Down Expand Up @@ -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) => {
Expand All @@ -98,15 +100,81 @@ 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<bool> {
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<Option<bool>> = std::cell::Cell::new(None);
}

pub(crate) struct DetectLightModeOverride {
old_value: Option<bool>,
}

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;

// 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,
Expand Down
Loading