-
-
Notifications
You must be signed in to change notification settings - Fork 121
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial support for dark mode terminals (#933)
- Loading branch information
Showing
19 changed files
with
340 additions
and
71 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
#!/bin/bash | ||
# Files in the profile.d directory are executed by the lexicographical order of their file names. | ||
# This file is named _07-term-mode.sh. The leading underscore is needed to ensure this file executes before | ||
# other files that depend on the functions defined here. The number portion is to ensure proper ordering among | ||
# the high-priority scripts. | ||
# | ||
# This file has no dependencies and should come first. | ||
|
||
# This function determines if the terminal is in dark mode. | ||
|
||
# For now, we use OSC sequences to query the terminal's foreground and background colors. | ||
# See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands | ||
# Adapted from https://bugzilla.gnome.org/show_bug.cgi?id=733423#c2 | ||
# | ||
# At some point we may introduce other methods to determine the terminal's color scheme. | ||
|
||
# Normally this function produces no output, but with -b, it outputs "true" or "false", | ||
# with -bb it outputs "true", "false", or "unknown". (Otherwise, unknown assume light mode.) | ||
# and always returns true. With -l it outputs integer luminance values for foreground | ||
# and background colors. With -ll it outputs labels on the luminance values as well. | ||
function _is_term_dark_mode() { | ||
local x fg_rgb bg_rgb fg_lum bg_lum | ||
|
||
# Extract the RGB values of the foreground and background colors via OSC 10 and 11. | ||
# Redirect output to `/dev/tty` in case we are in a subshell where output is a pipe, | ||
# because this output has to go directly to the terminal. | ||
stty -echo | ||
echo -ne '\e]10;?\a\e]11;?\a' >/dev/tty | ||
IFS=: read -t 0.1 -d $'\a' x fg_rgb | ||
IFS=: read -t 0.1 -d $'\a' x bg_rgb | ||
stty echo | ||
|
||
if [[ -z $fg_rgb ]] || [[ -z $bg_rgb ]]; then | ||
if [[ $GEODESIC_TRACE =~ "terminal" ]]; then | ||
echo $(tput setaf 1)* TRACE: "Terminal did not respond to OSC 10 and 11 queries.$(tput sgr0)" >&2 | ||
fi | ||
# If we cannot determine the color scheme, we assume light mode for historical reasons. | ||
if [[ "$*" =~ -b ]]; then | ||
if [[ "$*" =~ -bb ]]; then | ||
echo "unknown" | ||
else | ||
echo "false" | ||
fi | ||
return 0 # when returning text, always return success | ||
fi | ||
return 1 # Assume light mode | ||
fi | ||
|
||
if [[ "${x#*;}" != "rgb" ]]; then | ||
# Always output this error, because we want to hear about | ||
# other color formats users want us to support. | ||
echo "$(tput set bold)($tput setaf 1)Terminal reported unknown color format: ${x#*;}$(tput sgr0)" >&2 | ||
return 1 | ||
fi | ||
|
||
# Convert the RGB values to luminance by summing the values. | ||
fg_lum=$(_srgb_to_luminance "$fg_rgb") | ||
bg_lum=$(_srgb_to_luminance "$bg_rgb") | ||
if [[ "$*" =~ -l ]]; then | ||
if [[ "$*" =~ -ll ]]; then | ||
echo "Foreground luminance: $fg_lum, Background luminance: $bg_lum" | ||
else | ||
echo "$fg_lum $bg_lum" | ||
fi | ||
fi | ||
# If the background luminance is less than the foreground luminance, we are in dark mode. | ||
if ((bg_lum < fg_lum)); then | ||
if [[ "$*" =~ -b ]]; then | ||
echo "true" | ||
fi | ||
return 0 | ||
fi | ||
# If we cannot determine the color scheme, we assume light mode for historical reasons. | ||
if [[ "$*" =~ -b ]]; then | ||
echo "false" | ||
return 0 # when returning text, always return success | ||
fi | ||
return 1 | ||
} | ||
|
||
# Converting RGB to luminance is a lot more complex than summing the values. | ||
# To begin with, you need to know the color space, and if it is not a standard | ||
# one, then you need a full color profile. To start with, we assume sRGB, | ||
# and perhaps can add more color spaces later. | ||
# | ||
# Note: if we just take the simple sum of RGB values, we get in trouble with | ||
# blue backgrounds and foregrounds, which can have very high hex values but still be dark. | ||
# | ||
# To complicate matters, WCAG originally published the wrong formula for | ||
# converting sRGB to luminance. The sRGB space has a linear region, and the switch | ||
# from linear to exponential should be 0.04045, not 0.03928. | ||
# See https://www.w3.org/TR/WCAG21/#dfn-relative-luminance | ||
# | ||
# Unfortunately, many online calculators use the wrong formula, which you can test | ||
# by checking the luminance for #0a570a571010, which should be .0032746029, not .003274534 | ||
# However, there is no difference for 8-bit colors and, as you can see, for 16 bit colors, | ||
# the difference is negligible. | ||
# | ||
# You can use ImageMagick's `convert` command to convert an sRGB color to luminance: | ||
# convert xc:<color> -intensity Rec709Luminance -format "%[fx:intensity]" info: | ||
# For #0a570a571010, this will output 0.00327457, which is slightly off, likely | ||
# due to internal rounding. | ||
# | ||
function _srgb_to_luminance() { | ||
local color="$1" | ||
local red green blue | ||
|
||
if [[ -z $color ]]; then | ||
if [[ $GEODESIC_TRACE =~ "terminal" ]]; then | ||
# Use tput and sgr0 here because this is early in the startup sequence and trace logging | ||
echo "$(tput setaf 1)* TRACE: ${FUNCNAME[0]} called with empty or no argument.$(tput sgr0)" >&2 | ||
fi | ||
echo "0" | ||
return | ||
fi | ||
|
||
# Split the color string into red, green, and blue components | ||
IFS='/' read -r red green blue <<<"$color" | ||
|
||
# Normalize hexadecimal values to [0,1] and linearize them | ||
normalize_and_linearize() { | ||
local hex=${1^^} # Uppercase the hex value, because bc requires it | ||
local float=$(echo "ibase=16; $hex" | bc) | ||
local max=$(echo "ibase=16; 1$(printf '%0*d' ${#hex} 0)" | bc) # Accommodate the number of digits | ||
local normalized=$(echo "scale=10; $float / ($max - 1)" | bc) | ||
|
||
# Apply gamma correction | ||
if (($(echo "$normalized <= 0.04045" | bc))); then | ||
echo "scale=10; $normalized / 12.92" | bc | ||
else | ||
echo "scale=20; e(l(($normalized + 0.055) / 1.055) * 2.4)" | bc -l | ||
fi | ||
} | ||
|
||
# Linearize each color component | ||
local R=$(normalize_and_linearize $red) | ||
local G=$(normalize_and_linearize $green) | ||
local B=$(normalize_and_linearize $blue) | ||
|
||
# Calculate luminance | ||
local luminance=$(echo "scale=10; 0.2126 * $R + 0.7152 * $G + 0.0722 * $B" | bc) | ||
|
||
# Luminance is on a scale of 0 to 1, but we want to be able to | ||
# compare integers in bash, so we multiply by a big enough value | ||
# to get an integer and maintain precision. | ||
echo "scale=0; ($(echo "scale=10; $luminance * 1000000000" | bc) + 0.5) / 1" | bc | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,37 +1,152 @@ | ||
# Files in the profile.d directory are executed by the lexicographical order of their file names. | ||
# This file is named _10-colors.sh. The leading underscore is needed to ensure this file executes before | ||
# other files that depend on the functions defined here. The number portion is to ensure proper ordering among | ||
# the high-priority scripts | ||
# This file has no dependencies and should come first. | ||
function red() { | ||
echo "$(tput setaf 1)$*$(tput setaf 0)" | ||
# the high-priority scripts. | ||
# | ||
# This file depends on _07-term-mode.sh and should come second. | ||
|
||
# This file provides functions to colorize text in the terminal. | ||
# It has moderate support for light and dark mode, but it is not perfect. | ||
# The main change is that it uses the terminal's default colors for foreground and background, | ||
# whereas the previous version "reset" the color by setting it to black, which fails in dark mode. | ||
|
||
function update_terminal_mode() { | ||
local dark_mode=$(_is_term_dark_mode -b) | ||
if [[ ! -v _geodesic_tput_cache ]] || [[ "${_geodesic_tput_cache[dark_mode]}" != "$dark_mode" ]]; then | ||
_geodesic_tput_cache_init | ||
else | ||
local mode="light" | ||
if [[ $dark_mode == "true" ]]; then | ||
mode="dark" | ||
fi | ||
echo "Not updating terminal mode from $mode to $mode" | ||
fi | ||
} | ||
|
||
function green() { | ||
echo "$(tput setaf 2)$*$(tput setaf 0)" | ||
# We call `tput` several times for every prompt, and it can add up, so we cache the results. | ||
function _geodesic_tput_cache_init() { | ||
declare -g -A _geodesic_tput_cache | ||
|
||
local color_off=$(tput op) # reset foreground and background colors to defaults | ||
local bold=$(tput bold) | ||
local bold_off | ||
|
||
if [[ -n "$bold" ]]; then | ||
# Turning on bold is a standard `tput` attribute, but turning it off is not. | ||
# However, turning off bold is an ECMA standard (SGR 22), so it is not | ||
# unreasonable for us to use it. If it causes problems, people can set | ||
# export TERM_BOLD_OFF=$(tput sgr0) | ||
# http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-048.pdf | ||
bold_off=${TERM_BOLD_OFF:-$'\033[22m'} | ||
fi | ||
|
||
# Set up normal colors for light mode | ||
_geodesic_tput_cache=( | ||
[black]=$(tput setaf 0) | ||
[red]=$(tput setaf 1) | ||
[green]=$(tput setaf 2) | ||
[yellow]=$(tput setaf 3) | ||
[blue]=$(tput setaf 4) | ||
[magenta]=$(tput setaf 5) | ||
[cyan]=$(tput setaf 6) | ||
[white]=$(tput setaf 7) | ||
) | ||
|
||
if _is_term_dark_mode; then | ||
_geodesic_tput_cache[black]=$(tput setaf 7) # swap black and white | ||
_geodesic_tput_cache[white]=$(tput setaf 0) # 0 is ANSI black, 7 is ANSI white | ||
_geodesic_tput_cache[blue]=${_geodesic_tput_cache[cyan]} # blue is too dark, use cyan instead | ||
else | ||
_geodesic_tput_cache[yellow]=${_geodesic_tput_cache[magenta]} # yellow is too light, use magenta instead | ||
fi | ||
|
||
local key | ||
for key in "${!_geodesic_tput_cache[@]}"; do | ||
if [[ -n ${_geodesic_tput_cache["$key"]} ]]; then | ||
# Note, we cannot use printf for "-off" because command substitution strips trailing newlines | ||
_geodesic_tput_cache["${key}-off"]="$color_off"$'\n' | ||
_geodesic_tput_cache["bold-${key}"]=$(printf "%s%s" "$bold" "${_geodesic_tput_cache["$key"]}") | ||
_geodesic_tput_cache["bold-${key}-off"]="${color_off}${bold_off}"$'\n' | ||
|
||
# Note $'\x01' and $'\x02' are ASCII codes to put around non-printing characters so that | ||
# bash can correctly calculate the visible length of the string. | ||
# They are equivalent to \[ and \] in a bash prompt string. | ||
# Also note that these variants do not include a newline at the end. | ||
_geodesic_tput_cache["${key}-n"]=$(printf "\x01%s\x02" "${_geodesic_tput_cache["$key"]}") | ||
_geodesic_tput_cache["${key}-n-off"]=$(printf "\x01%s\x02" "$color_off") | ||
_geodesic_tput_cache["bold-${key}-n"]=$(printf "\x01%s%s\x02" "$bold" "${_geodesic_tput_cache["$key"]}") | ||
_geodesic_tput_cache["bold-${key}-n-off"]=$(printf "\x01%s%s\x02" "$color_off" "$bold_off") | ||
fi | ||
done | ||
|
||
# Bold is not a color, handle bold without color change separately | ||
if [[ -n "$bold"} ]]; then | ||
_geodesic_tput_cache["bold"]=$(printf "\x01%s\x02" "$bold") | ||
_geodesic_tput_cache["bold-off"]=$(printf "\x01%s\x02" "$bold_off") | ||
fi | ||
|
||
# Save the terminal type so we can invalidate the cache if it changes | ||
_geodesic_tput_cache[TERM]="$TERM" | ||
_geodesic_tput_cache[dark_mode]="$(_is_term_dark_mode -b)" | ||
} | ||
|
||
function yellow() { | ||
echo "$(tput setaf 3)$*$(tput setaf 0)" | ||
# Colorize text using ANSI escape codes. | ||
# Usage: _geodesic_color style text... | ||
# `style` is defined by the keys of the associative array _geodesic_tput_cache set up above. | ||
# Not intended to be called directly. Use the named style functions below. | ||
function _geodesic_color() { | ||
# The -v test is to see if the variable is set. | ||
# It is required because the associative array syntax does not work with unset variables. | ||
if [[ ! -v _geodesic_tput_cache ]] || [[ "${_geodesic_tput_cache[TERM]}" != "$TERM" ]]; then | ||
_geodesic_tput_cache_init | ||
fi | ||
|
||
local style=$1 | ||
shift | ||
|
||
printf "%s%s%s" "${_geodesic_tput_cache["$style"]}" "$*" "${_geodesic_tput_cache["${style}-off"]}" | ||
} | ||
|
||
function cyan() { | ||
echo "$(tput setaf 6)$*$(tput setaf 0)" | ||
# Named style helpers | ||
# | ||
# For each color there is "color" and "bold-color", where "bold-color" is the bold version of the color. | ||
# | ||
# These come in 2 flavors. | ||
# - The plain ones include a newline in the end and do not include delimiters around the non-printing text. | ||
# - The ones ending with "-n" do not include a newline and do include delimiters around the non-printing text. | ||
# | ||
# Note that the newline is stripped if run via command substitution, so | ||
# echo "$(red "Hello") World" | ||
# will not have a newline between "Hello" and "World". | ||
# However, you should still use the "-n" variants if your string is or might become part of a PS1 prompt. | ||
# Otherwise, bash will not correctly calculate the visible length of the prompt and editing command history will break. | ||
# | ||
# We intentionally do not define blue or magenta, as blue is problematic in dark mode | ||
# and magenta is too much like red in dark mode, plus it is used as a substitute for yellow in light mode. | ||
# We do not define white or black, either, as we should use the terminal's default for those. | ||
# However, those colors are available via _geodesic_color() if needed, and "white" and "black" are | ||
# swapped in dark mode, so they are more appropriately called "bg" and "fg" respectively. | ||
# Also, "yellow" is not necessarily yellow, it varies with the terminal theme, and | ||
# would be better named "caution" or "info". | ||
|
||
function _generate_color_functions() { | ||
local color | ||
for color in red green yellow cyan; do | ||
eval "function ${color}() { _geodesic_color ${color} \"\$*\"; }" | ||
eval "bold-${color}() { _geodesic_color bold-${color} \"\$*\"; }" | ||
eval "function ${color}-n() { _geodesic_color ${color}-n \"\$*\"; }" | ||
eval "bold-${color}-n() { _geodesic_color bold-${color}-n \"\$*\"; }" | ||
done | ||
} | ||
|
||
# Turning on bold is a standard `tput` attribute, but turning it off is not. | ||
# However, turning off bold is an ECMA standard (SGR 22), so it is not | ||
# unreasonable for us to use it. If it causes problems, people can set | ||
# export TERM_BOLD_OFF=$(tput sgr0) | ||
# http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-048.pdf | ||
_generate_color_functions | ||
unset _generate_color_functions | ||
|
||
function bold() { | ||
local bold=$(tput bold) | ||
local boldoff=${TERM_BOLD_OFF:-$'\033[22m'} | ||
# If the terminal supports color | ||
if [[ -n $bold ]]; then | ||
printf "%s%s%s\n" "$bold" "$*" "$boldoff" | ||
else | ||
# The terminal does not support color | ||
printf "%s\n" "$*" | ||
fi | ||
_geodesic_color bold "$*" | ||
} | ||
|
||
# Actually, resets all graphics settings to their defaults. | ||
function reset_terminal_colors() { | ||
tput sgr0 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.