Compare commits

...

17 Commits

Author SHA1 Message Date
Tau 1099fc7bcf
Merge 6213319adc into a1f85b9e06 2024-03-31 22:56:13 -04:00
Tau 6213319adc
Document breaking change in library 2024-03-19 10:20:01 +01:00
Tau 1a6353d576
Update terminal-colorsaurus (again) 2024-03-19 10:20:00 +01:00
Tau 4a7b65e2c2
Add changelog entry 2024-03-19 10:19:59 +01:00
Tau 02967933bc
Disable color detection in test 2024-03-19 10:19:58 +01:00
Tau de358c1d65
Update readme 2024-03-19 10:19:57 +01:00
Tau ffc95650fb
Update terminal-colorsaurus 2024-03-19 10:19:56 +01:00
Tau b283a30091
Remove cargo feature 2024-03-19 10:19:55 +01:00
Tau 8f9a8f03d0
Improve upon the documentation 2024-03-19 10:19:54 +01:00
Tau df89caeb85
Move actual detection into library 2024-03-19 10:19:53 +01:00
Tau 5fdc8066aa
Remove HighlightingAssets::default_theme() 2024-03-19 10:19:52 +01:00
Tau 22b23737a1
Add generated powershell completion to ignore list 2024-03-19 10:19:51 +01:00
Tau 725f3d0ac4
Update completions and man page 2024-03-19 10:19:50 +01:00
Tau b6070cc3fb
Expose new theme selection in CLI 2024-03-19 10:19:49 +01:00
Tau f106714673
Use `default_theme()` function from theme module 2024-03-19 10:19:48 +01:00
Tau b4ef251af2
Deprecate old `default_theme` function 2024-03-19 10:19:47 +01:00
Tau d22d864d7e
Choose theme based on the terminal's color scheme 2024-03-19 10:19:43 +01:00
20 changed files with 636 additions and 111 deletions

1
.gitignore vendored
View File

@ -2,6 +2,7 @@
**/*.rs.bk
# Generated files
/assets/completions/_bat.ps1
/assets/completions/bat.bash
/assets/completions/bat.fish
/assets/completions/bat.zsh

View File

@ -6,6 +6,7 @@
- `bat --squeeze-blank`/`bat -s` will now squeeze consecutive empty lines, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815)
- `bat --squeeze-limit` to set the maximum number of empty consecutive when using `--squeeze-blank`, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815)
- `PrettyPrinter::squeeze_empty_lines` to support line squeezing for bat as a library, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815)
- Automatically choose theme based on the terminal's color scheme, see #2896 (@bash)
## Bugfixes
@ -49,6 +50,9 @@
## `bat` as a library
- 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.
- Changes to `syntax_mapping::SyntaxMapping` #2755 (@cyqsimon)
- `SyntaxMapping::get_syntax_for` is now correctly public
- [BREAKING] `SyntaxMapping::{empty,builtin}` are removed; use `SyntaxMapping::new` instead

63
Cargo.lock generated
View File

@ -149,6 +149,7 @@ dependencies = [
"shell-words",
"syntect",
"tempfile",
"terminal-colorsaurus",
"thiserror",
"toml",
"unicode-width",
@ -697,9 +698,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.149"
version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "libgit2-sys"
@ -764,9 +765,9 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "memchr"
version = "2.6.4"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
[[package]]
name = "miniz_oxide"
@ -777,6 +778,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.26.4"
@ -958,9 +970,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.66"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
@ -976,9 +988,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.33"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
@ -1255,9 +1267,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
version = "2.0.32"
version = "2.0.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2"
checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07"
dependencies = [
"proc-macro2",
"quote",
@ -1319,6 +1331,29 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "terminal-colorsaurus"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a11d4fd698c3b697b6f712fa75f62f7b15119f41b0bb660741dd02297f534d78"
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"
@ -1337,18 +1372,18 @@ checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b"
[[package]]
name = "thiserror"
version = "1.0.53"
version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2cd5904763bad08ad5513ddbb12cf2ae273ca53fa9f68e843e236ec6dfccc09"
checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.53"
version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dcf4a824cce0aeacd6f38ae6f24234c8e80d68632338ebaa1443b5df9e29e19"
checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
dependencies = [
"proc-macro2",
"quote",

View File

@ -68,6 +68,7 @@ bytesize = { version = "1.3.0" }
encoding_rs = "0.8.33"
os_str_bytes = { version = "~6.6", optional = true }
run_script = { version = "^0.10.1", optional = true}
terminal-colorsaurus = { version = "0.3.3" }
[dependencies.git2]
version = "0.18"

View File

@ -474,8 +474,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).

View File

@ -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.')

View File

@ -99,7 +99,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
;;
@ -111,7 +111,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
@ -162,12 +164,15 @@ _bat() {
--color
--italic-text
--decorations
--detect-color-scheme
--force-colorization
--paging
--pager
--map-syntax
--ignored-suffix
--theme
--theme-dark
--theme-light
--list-themes
--style
--line-range

View File

@ -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 '
@ -141,6 +142,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
@ -203,6 +206,10 @@ complete -c $bat -l terminal-width -x -d "Set terminal <width>, +<offset>, 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

View File

@ -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'

View File

@ -113,6 +113,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 <when>
.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<when>\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 \
@ -143,9 +161,23 @@ Note that the right-hand side is the *name* of the syntax, not a file extension.
.HP
\fB\-\-theme\fR <theme>
.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 <theme>
.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 <theme>
.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

View File

@ -109,9 +109,39 @@ Options:
'bat --ignored-suffix ".dev" my_file.json.dev' will use JSON syntax, and ignore '.dev'
--theme <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 <when>
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 <theme>
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 <theme>
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.

View File

@ -41,6 +41,12 @@ Options:
Use the specified syntax for files matching the glob pattern ('*.cpp:C++').
--theme <theme>
Set the color theme for syntax highlighting.
--detect-color-scheme <when>
Specify when to query the terminal for its colors.
--theme-light <theme>
Sets the color theme for syntax highlighting used for light backgrounds.
--theme-dark <theme>
Sets the color theme for syntax highlighting used for dark backgrounds.
--list-themes
Display all supported highlighting themes.
-s, --squeeze-blank

View File

@ -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;
@ -69,57 +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 <https://github.com/sharkdp/bat/issues/1746> and
/// <https://github.com/sharkdp/bat/issues/1928> for more context.
pub fn default_theme() -> &'static str {
#[cfg(not(target_os = "macos"))]
{
Self::default_dark_theme()
}
#[cfg(target_os = "macos")]
{
if macos_dark_mode_active() {
Self::default_dark_theme()
} else {
Self::default_light_theme()
}
}
}
/**
* 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<Self> {
Ok(HighlightingAssets::new(
SerializedSyntaxSet::FromFile(cache_path.join("syntaxes.bin")),
@ -248,7 +198,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")
}
}
@ -399,26 +352,6 @@ fn asset_from_cache<T: serde::de::DeserializeOwned>(
.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::*;

View File

@ -2,18 +2,19 @@ use std::collections::HashSet;
use std::env;
use std::io::IsTerminal;
use std::path::{Path, PathBuf};
use std::str::FromStr as _;
use crate::{
clap_app,
config::{get_args_from_config_file, get_args_from_env_opts_var, get_args_from_env_vars},
};
use bat::theme::{theme, DetectColorScheme, ThemeOptions, ThemeRequest};
use clap::ArgMatches;
use console::Term;
use crate::input::{new_file_input, new_stdin_input};
use bat::{
assets::HighlightingAssets,
bat_warning,
config::{Config, VisibleLines},
error::*,
@ -242,18 +243,7 @@ impl App {
4
},
),
theme: self
.matches
.get_one::<String>("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()),
visible_lines: match self.matches.try_contains_id("diff").unwrap_or_default()
&& self.matches.get_flag("diff")
{
@ -389,4 +379,35 @@ impl App {
Ok(styled_components)
}
fn theme_options(&self) -> ThemeOptions {
let theme = self
.matches
.get_one::<String>("theme")
.map(|t| ThemeRequest::from_str(t).unwrap());
let theme_dark = self
.matches
.get_one::<String>("theme-dark")
.map(|t| ThemeRequest::from_str(t).unwrap());
let theme_light = self
.matches
.get_one::<String>("theme-light")
.map(|t| ThemeRequest::from_str(t).unwrap());
let detect_color_scheme = match self
.matches
.get_one::<String>("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,
}
}
}

View File

@ -373,13 +373,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")

View File

@ -141,6 +141,8 @@ pub fn get_args_from_env_vars() -> Vec<OsString> {
[
("--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"),

View File

@ -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;

View File

@ -236,7 +236,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<str>) -> &mut Self {
self.config.theme = theme.as_ref().to_owned();
self

384
src/theme.rs Normal file
View File

@ -0,0 +1,384 @@
//! 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) -> String {
theme_from_detector(options, &TerminalColorSchemeDetector)
}
/// 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",
}
}
/// Options for configuring the theme used for syntax highlighting.
/// Used together with [`theme`].
#[derive(Debug, Default, PartialEq, Eq)]
pub struct ThemeOptions {
/// Always use this theme regardless of the terminal's background color.
pub theme: Option<ThemeRequest>,
/// The theme to use in case the terminal uses a dark background with light text.
pub theme_dark: Option<ThemeRequest>,
/// The theme to use in case the terminal uses a light background with dark text.
pub theme_light: Option<ThemeRequest>,
/// Whether or not to test if 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.
///
/// ```
/// # 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,
}
impl FromStr for ThemeRequest {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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(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]
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(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ColorScheme {
#[default]
Dark,
Light,
}
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<ThemeRequest> {
match color_scheme {
ColorScheme::Dark => options.theme_dark,
ColorScheme::Light => options.theme_light,
}
}
fn detect(when: DetectColorScheme, detector: &dyn ColorSchemeDetector) -> Option<ColorScheme> {
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<ColorScheme>;
}
struct TerminalColorSchemeDetector;
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<ColorScheme> {
use terminal_colorsaurus::{color_scheme, QueryOptions};
let colors = color_scheme(QueryOptions::default()).ok()?;
if colors.is_light_on_dark() {
Some(ColorScheme::Dark)
} else {
Some(ColorScheme::Light)
}
}
}
#[cfg(test)]
impl ColorSchemeDetector for Option<ColorScheme> {
fn should_detect(&self) -> bool {
true
}
fn detect(&self) -> Option<ColorScheme> {
*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_from_detector(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_from_detector(options, &detector);
assert!(detector.was_called.get());
}
}
#[test]
fn called_for_auto_if_should_detect() {
let detector = DetectorStub::should_detect(Some(Dark));
_ = 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_from_detector(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())),
theme_dark: Some(ThemeRequest::Named("Dark Theme".to_string())),
theme_light: Some(ThemeRequest::Named("Light Theme".to_string())),
..Default::default()
},
] {
let detector = ConstantDetector(color_scheme);
assert_eq!("Theme", theme_from_detector(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_from_detector(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_from_detector(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_from_detector(options, &detector)
);
}
}
#[test]
fn varies_depending_on_color_scheme() {
for color_scheme in color_schemes() {
for options in [
ThemeOptions::default(),
ThemeOptions {
theme_dark: Some(ThemeRequest::Default),
theme_light: Some(ThemeRequest::Default),
..Default::default()
},
] {
let detector = ConstantDetector(Some(color_scheme));
assert_eq!(
default_theme(color_scheme),
theme_from_detector(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 {
theme_dark: Some(ThemeRequest::Named("Dark".to_string())),
theme_light: Some(ThemeRequest::Named("Light".to_string())),
..Default::default()
};
let detector = ConstantDetector(color_scheme);
assert_eq!("Dark", theme_from_detector(options, &detector));
}
}
#[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())),
..Default::default()
};
let detector = ConstantDetector(Some(ColorScheme::Light));
assert_eq!("Light", theme_from_detector(options, &detector));
}
}
struct DetectorStub {
should_detect: bool,
color_scheme: Option<ColorScheme>,
was_called: Cell<bool>,
}
impl DetectorStub {
fn should_detect(color_scheme: Option<ColorScheme>) -> 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<ColorScheme> {
self.was_called.set(true);
self.color_scheme
}
}
struct ConstantDetector(Option<ColorScheme>);
impl ColorSchemeDetector for ConstantDetector {
fn should_detect(&self) -> bool {
true
}
fn detect(&self) -> Option<ColorScheme> {
self.0
}
}
fn optional<T>(value: impl Iterator<Item = T>) -> impl Iterator<Item = Option<T>> {
value.map(Some).chain(iter::once(None))
}
fn color_schemes() -> impl Iterator<Item = ColorScheme> {
[Dark, Light].into_iter()
}
}

View File

@ -370,6 +370,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) };
@ -380,6 +381,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.");