Skip to content

Commit

Permalink
Initial support for dark mode terminals (#933)
Browse files Browse the repository at this point in the history
  • Loading branch information
Nuru committed May 2, 2024
1 parent 5e0e021 commit 75c48f6
Show file tree
Hide file tree
Showing 19 changed files with 340 additions and 71 deletions.
2 changes: 2 additions & 0 deletions packages.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ aws-iam-authenticator@cloudposse
bash
bash-completion
bats@community
# bc is for doing floating point math in the shell
bc
# no arm64 cfssl@cloudposse
coreutils
chamber@cloudposse
Expand Down
4 changes: 2 additions & 2 deletions rootfs/etc/codefresh/require_vars
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,11 @@ function require_cfvars() {
}

function red() {
echo "$(tput setaf 1)$*$(tput sgr0)"
echo "$(tput setaf 1)$*$(tput op)"
}

function yellow() {
echo "$(tput setaf 3)$*$(tput sgr0)"
echo "$(tput setaf 3)$*$(tput op)"
}

echo "REQUIRE VARS: checking for presence of required variables"
Expand Down
147 changes: 147 additions & 0 deletions rootfs/etc/profile.d/_07-term-mode.sh
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
}
163 changes: 139 additions & 24 deletions rootfs/etc/profile.d/_10-colors.sh
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
}
1 change: 1 addition & 0 deletions rootfs/etc/profile.d/_30-geodesic-config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ function _expand_dir_or_file() {

[[ -n $_GEODESIC_TRACE_CUSTOMIZATION ]] && echo trace: looking for resources of type "$resource" in "$dir"

local item
for item in "${dir}/$resource" "${dir}/${resource}.d"/*; do
if [[ -f $item ]]; then
[[ $item =~ $exclude ]] && ([[ -n $_GEODESIC_TRACE_CUSTOMIZATION ]] && echo trace: excluding "$item" || true) && continue
Expand Down
1 change: 1 addition & 0 deletions rootfs/etc/profile.d/_40-preferences.sh
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ function _load_geodesic_preferences() {
[[ -n $_GEODESIC_TRACE_CUSTOMIZATION ]] && echo trace: LOADING preference files

_search_geodesic_dirs preference_list preferences
local file
for file in "${preference_list[@]}"; do
[[ -n $_GEODESIC_TRACE_CUSTOMIZATION ]] && echo trace: loading preference file "$file"
source "$file"
Expand Down
7 changes: 4 additions & 3 deletions rootfs/etc/profile.d/aws.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ if [ ! -f "${AWS_CONFIG_FILE:=${GEODESIC_AWS_HOME}/config}" ] && [ -d ${GEODESIC
fi

# Install autocompletion rules for aws CLI v1 and v2
for aws in aws aws1 aws2; do
if command -v ${aws}_completer >/dev/null; then
complete -C "$(command -v ${aws}_completer)" ${aws}
for __aws in aws aws1 aws2; do
if command -v ${__aws}_completer >/dev/null; then
complete -C "$(command -v ${__aws}_completer)" ${__aws}
fi
done
unset __aws

# This is the default assume-role function, but it can be overridden/replaced later
# by aws-okta or aws-vault, etc. or could have already been overridden.
Expand Down

0 comments on commit 75c48f6

Please sign in to comment.