kitty: remember the light/dark mode across restarts

kitty gives *-theme.auto.conf priority over everything -- includes and
even --override -- and has no option to stop following the OS. So the
only way to start in a chosen mode is to control what those files
contain. Make them generated:

  themes/{light,dark,no-preference}.conf  canonical, the real themes
  theme-mode                              auto | light | dark
  apply-theme-mode.sh                      writes the three .auto.conf

Under `auto` each appearance keeps its own theme, so kitty follows the
OS as before. Pinned, all three get the pinned theme, so kitty starts in
it whatever the OS says and a config reload no longer resets it.

`theme` now records the mode and reloads instead of calling set-colors,
which is why the setting survives restarts. A bare `theme` records
`auto` rather than pinning when the toggle lands on the macOS
appearance, so nothing is pinned unless it actually differs.

The kitten shim hashes the automatic theme files before running `kitten
themes` and copies whichever changed into themes/, so picking a new
light theme while pinned to dark is kept and applied on the next switch.
Covered by tests/test_theme_mode.sh.

conf.d/kitty_theme.fish is gone: theme-mode is now the source of truth,
so the universal-variable tracker it maintained is redundant.
This commit is contained in:
Anton Volnuhin 2026-08-06 01:49:45 +03:00
parent f66ee4d845
commit addce489b4
13 changed files with 421 additions and 70 deletions

View File

@ -0,0 +1,68 @@
#!/bin/sh
# Regenerate the *-theme.auto.conf files kitty reads at startup from the
# canonical themes in ./themes, honouring the mode recorded in ./theme-mode:
#
# auto every appearance keeps its own theme, so kitty follows the OS
# light every appearance gets the light theme, so kitty stays light
# dark every appearance gets the dark theme, so kitty stays dark
#
# kitty gives the automatic theme files priority over everything else -- includes
# and even --override -- so pinning a mode means writing the wanted theme into
# whichever file the OS makes kitty pick. That is why the real themes live in
# ./themes and these three files are generated.
set -eu
script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
config_dir=${1:-"$script_dir"}
canonical_dir="$config_dir/themes"
mode_file="$config_dir/theme-mode"
mode=auto
if [ -f "$mode_file" ]; then
mode=$(tr -d ' \t\r\n' < "$mode_file")
fi
case "$mode" in
auto | light | dark) ;;
*) mode=auto ;;
esac
mkdir -p "$canonical_dir"
# Adopt the current automatic themes as canonical when they have not been
# captured yet. Only safe while following the OS: under a pinned mode the
# generated files all hold the pinned theme and would overwrite the others.
if [ "$mode" = auto ]; then
for appearance in dark light no-preference; do
canonical="$canonical_dir/$appearance.conf"
generated="$config_dir/$appearance-theme.auto.conf"
if [ ! -f "$canonical" ] && [ -f "$generated" ]; then
cp "$generated" "$canonical"
fi
done
fi
for appearance in dark light no-preference; do
if [ "$mode" = auto ]; then
source_name=$appearance
else
source_name=$mode
fi
canonical="$canonical_dir/$source_name.conf"
[ -f "$canonical" ] || continue
generated="$config_dir/$appearance-theme.auto.conf"
mode_bits=600
[ -f "$generated" ] && mode_bits=$(stat -f '%Lp' "$generated")
tmp=$(mktemp "$generated.tmp.XXXXXX")
trap 'rm -f "$tmp"' EXIT HUP INT TERM
cat "$canonical" > "$tmp"
chmod "$mode_bits" "$tmp"
mv -f "$tmp" "$generated"
trap - EXIT HUP INT TERM
done
"$script_dir/repair-auto-themes.sh" "$config_dir"

View File

@ -0,0 +1,152 @@
#!/bin/sh
# Behavioural regression for the remembered light/dark mode:
# - apply-theme-mode.sh generates the automatic theme files from ./themes
# - a pinned mode puts one theme in all three, so kitty starts in it
# - `theme auto` restores per-appearance themes without losing any
# - `kitten themes` changes are captured even while a mode is pinned
# - a bare `theme` records `auto` when the result already matches macOS
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
fish_bin=${FISH:-$(command -v fish)}
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT HUP INT TERM
config_dir="$tmpdir/config"
fake_bin="$tmpdir/bin"
mkdir -p "$config_dir/themes" "$fake_bin"
DARK='#111111'
LIGHT='#eeeeee'
NOPREF='#222222'
NEW_LIGHT='#abcdef'
printf 'background %s\nmacos_titlebar_color background\n' "$DARK" \
>"$config_dir/themes/dark.conf"
printf 'background %s\nmacos_titlebar_color background\n' "$LIGHT" \
>"$config_dir/themes/light.conf"
printf 'background %s\nmacos_titlebar_color background\n' "$NOPREF" \
>"$config_dir/themes/no-preference.conf"
bg() {
awk '/^background/ { print $2; exit }' "$1"
}
expect() {
# expect <label> <file> <wanted>
got=$(bg "$2")
if [ "$got" != "$3" ]; then
echo "not ok - $1: expected $3, got $got" >&2
exit 1
fi
}
run_fish() {
# run_fish <macos-appearance> <fish code>
appearance=$1
shift
if [ "$appearance" = dark ]; then
printf '#!/bin/sh\necho Dark\n' >"$fake_bin/defaults"
else
printf '#!/bin/sh\nexit 1\n' >"$fake_bin/defaults"
fi
chmod +x "$fake_bin/defaults"
env -u KITTY_WINDOW_ID PATH="$fake_bin:/usr/bin:/bin:/usr/sbin" \
KITTY_CONFIG_DIRECTORY="$config_dir" \
"$fish_bin" --no-config -c "
for f in __kitty_config_dir __kitty_macos_appearance \
__kitty_theme_mode_setting __kitty_theme_current theme kitten
source \$HOME/.config/fish/functions/\$f.fish
end
$*"
}
# --- auto keeps every appearance on its own theme -------------------------
echo auto >"$config_dir/theme-mode"
"$root/apply-theme-mode.sh" "$config_dir"
expect "auto/dark" "$config_dir/dark-theme.auto.conf" "$DARK"
expect "auto/light" "$config_dir/light-theme.auto.conf" "$LIGHT"
expect "auto/no-preference" "$config_dir/no-preference-theme.auto.conf" "$NOPREF"
# --- a pinned mode wins for every appearance ------------------------------
for pinned in dark light; do
echo "$pinned" >"$config_dir/theme-mode"
"$root/apply-theme-mode.sh" "$config_dir"
case $pinned in
dark) want=$DARK ;;
light) want=$LIGHT ;;
esac
for appearance in dark light no-preference; do
expect "pinned $pinned/$appearance" \
"$config_dir/$appearance-theme.auto.conf" "$want"
done
done
# --- and auto restores them, so pinning never destroys a theme ------------
echo auto >"$config_dir/theme-mode"
"$root/apply-theme-mode.sh" "$config_dir"
expect "restored dark" "$config_dir/dark-theme.auto.conf" "$DARK"
expect "restored light" "$config_dir/light-theme.auto.conf" "$LIGHT"
expect "restored no-preference" "$config_dir/no-preference-theme.auto.conf" "$NOPREF"
# --- `kitten themes` is captured even while a mode is pinned --------------
cat >"$fake_bin/kitten" <<EOF
#!/bin/sh
printf 'background %s\n' "$NEW_LIGHT" \
> "\$KITTY_CONFIG_DIRECTORY/light-theme.auto.conf"
exit 0
EOF
printf '#!/bin/sh\nexit 0\n' >"$fake_bin/kitty"
chmod +x "$fake_bin/kitten" "$fake_bin/kitty"
run_fish light 'theme dark >/dev/null; kitten themes' >/dev/null
expect "captured new light theme" "$config_dir/themes/light.conf" "$NEW_LIGHT"
expect "pin still held after kitten themes" \
"$config_dir/light-theme.auto.conf" "$DARK"
[ "$(tr -d ' \n' <"$config_dir/theme-mode")" = dark ] || {
echo "not ok - kitten themes cleared the pinned mode" >&2
exit 1
}
run_fish light 'theme light >/dev/null'
expect "new light theme is used once selected" \
"$config_dir/light-theme.auto.conf" "$NEW_LIGHT"
expect "dark theme survived the whole exchange" \
"$config_dir/themes/dark.conf" "$DARK"
# --- a bare `theme` records auto when it lands on the macOS appearance ----
echo auto >"$config_dir/theme-mode"
"$root/apply-theme-mode.sh" "$config_dir"
# macOS light, effective light -> toggles to dark, which differs -> pinned
run_fish light 'theme >/dev/null'
[ "$(tr -d ' \n' <"$config_dir/theme-mode")" = dark ] || {
echo "not ok - toggle away from the macOS appearance should pin" >&2
exit 1
}
# toggling back lands on light, which is what macOS says -> auto, not pinned
run_fish light 'theme >/dev/null'
[ "$(tr -d ' \n' <"$config_dir/theme-mode")" = auto ] || {
echo "not ok - toggle onto the macOS appearance should record auto" >&2
exit 1
}
# same shape with macOS in dark mode
run_fish dark 'theme >/dev/null'
[ "$(tr -d ' \n' <"$config_dir/theme-mode")" = light ] || {
echo "not ok - toggle away from a dark macOS should pin light" >&2
exit 1
}
run_fish dark 'theme >/dev/null'
[ "$(tr -d ' \n' <"$config_dir/theme-mode")" = auto ] || {
echo "not ok - toggle onto a dark macOS should record auto" >&2
exit 1
}
"$root/tests/verify_auto_themes.sh" "$config_dir"
echo "PASS: theme mode is remembered, survives kitten themes, and collapses to auto"

View File

@ -0,0 +1 @@
auto

View File

@ -0,0 +1,31 @@
background #1c2836
foreground #ffffff
cursor #bbbbbb
selection_background #b4d5ff
color0 #000000
color8 #545454
color1 #f9555f
color9 #fa8b8e
color2 #20af89
color10 #34bb99
color3 #fdf029
color11 #ffff55
color4 #589cf5
color12 #589cf5
color5 #934d95
color13 #e75598
color6 #1e9ee6
color14 #3978bb
color7 #bbbbbb
color15 #ffffff
selection_foreground #1c2836
# START_AUTOGENERATED_TAB_STYLE
# Feel free to update these colors manually and remove these comments.
active_tab_foreground #1c2836
active_tab_background #2e84e6
inactive_tab_foreground #2e84e6
inactive_tab_background #1c2836
# END_AUTOGENERATED_TAB_STYLE
macos_titlebar_color background

View File

@ -0,0 +1,31 @@
background #f8f8f8
foreground #2a2b33
cursor #bbbbbb
selection_background #e5e5e6
color0 #000000
color8 #000000
color1 #de3d35
color9 #de3d35
color2 #3e953a
color10 #3e953a
color3 #d2b67b
color11 #d2b67b
color4 #2f5af3
color12 #2f5af3
color5 #950095
color13 #a00095
color6 #3e953a
color14 #3e953a
color7 #bbbbbb
color15 #ffffff
selection_foreground #2a2b33
# START_AUTOGENERATED_TAB_STYLE
# Feel free to update these colors manually and remove these comments.
active_tab_foreground #f8f8f8
active_tab_background #2f5af3
inactive_tab_foreground #2f5af3
inactive_tab_background #f8f8f8
# END_AUTOGENERATED_TAB_STYLE
macos_titlebar_color background

View File

@ -0,0 +1,28 @@
background #1c2836
foreground #ffffff
cursor #bbbbbb
selection_background #b4d5ff
color0 #000000
color8 #545454
color1 #f9555f
color9 #fa8b8e
color2 #20af89
color10 #34bb99
color3 #fdf029
color11 #ffff55
color4 #589cf5
color12 #589cf5
color5 #934d95
color13 #e75598
color6 #1e9ee6
color14 #3978bb
color7 #bbbbbb
color15 #ffffff
selection_foreground #1c2836
active_tab_foreground #1c2836
active_tab_background #2e84e6
inactive_tab_foreground #2e84e6
inactive_tab_background #1c2836
macos_titlebar_color background

View File

@ -1,16 +0,0 @@
status is-interactive
or exit
# Keep $kitty_theme_mode tracking what the terminal actually reports, so that
# `theme` toggles from the real state even after kitty follows an OS appearance
# change on its own. fish populates $fish_terminal_color_theme at the first
# prompt and again whenever the terminal signals a palette change.
function __kitty_theme_track --on-variable fish_terminal_color_theme
contains -- "$fish_terminal_color_theme" light dark
or return
test "$kitty_theme_mode" = "$fish_terminal_color_theme"
and return
set --universal kitty_theme_mode $fish_terminal_color_theme
end

View File

@ -0,0 +1,7 @@
function __kitty_config_dir --description 'Path to the kitty config directory'
if set -q KITTY_CONFIG_DIRECTORY
echo $KITTY_CONFIG_DIRECTORY
else
echo $HOME/.config/kitty
end
end

View File

@ -0,0 +1,7 @@
function __kitty_macos_appearance --description 'Report the macOS appearance as light or dark'
if test "$(defaults read -g AppleInterfaceStyle 2>/dev/null)" = Dark
echo dark
else
echo light
end
end

View File

@ -1,14 +1,9 @@
function __kitty_theme_current --description 'Report the color mode kitty is currently using' function __kitty_theme_current --description 'Report the color mode kitty is currently using'
# $kitty_theme_mode is kept in sync with the terminal by conf.d/kitty_theme.fish set -l mode (__kitty_theme_mode_setting)
# and by `theme` itself; the rest are fallbacks for shells that have not yet
# seen an interactive prompt. if test "$mode" = auto
if contains -- "$kitty_theme_mode" light dark __kitty_macos_appearance
echo $kitty_theme_mode
else if contains -- "$fish_terminal_color_theme" light dark
echo $fish_terminal_color_theme
else if test "$(defaults read -g AppleInterfaceStyle 2>/dev/null)" = Dark
echo dark
else else
echo light echo $mode
end end
end end

View File

@ -0,0 +1,13 @@
function __kitty_theme_mode_setting --description 'Report the recorded kitty theme mode: auto, light or dark'
set -l config_dir (__kitty_config_dir)
set -l mode auto
if test -r $config_dir/theme-mode
set mode (string trim -- (cat $config_dir/theme-mode 2>/dev/null))
end
contains -- "$mode" auto light dark
or set mode auto
echo $mode
end

View File

@ -1,16 +1,53 @@
function kitten --description 'Run kitten and restore Kitty theme overrides' function kitten --description 'Run kitten and restore Kitty theme overrides'
command kitten $argv
set -l kitten_status $status
if test $kitten_status -eq 0
and test (count $argv) -gt 0
and test "$argv[1]" = themes
set -l config_dir "$HOME/.config/kitty" set -l config_dir "$HOME/.config/kitty"
if set -q KITTY_CONFIG_DIRECTORY if set -q KITTY_CONFIG_DIRECTORY
set config_dir "$KITTY_CONFIG_DIRECTORY" set config_dir "$KITTY_CONFIG_DIRECTORY"
end end
"$HOME/.config/kitty/repair-auto-themes.sh" "$config_dir" set -l is_themes false
if test (count $argv) -gt 0; and test "$argv[1]" = themes
set is_themes true
end
# The automatic theme files are generated from $config_dir/themes, so record
# their state first: whichever one the kitten rewrites is the theme just
# chosen for that appearance, and has to be captured before the mode is
# re-applied over the top of it.
set -l appearances dark light no-preference
set -l before
if test $is_themes = true
for appearance in $appearances
set -l file "$config_dir/$appearance-theme.auto.conf"
if test -r "$file"
set -a before (command shasum <"$file" | string split -f1 ' ')
else
set -a before missing
end
end
end
command kitten $argv
set -l kitten_status $status
if test $kitten_status -eq 0
and test $is_themes = true
for index in (seq (count $appearances))
set -l appearance $appearances[$index]
set -l file "$config_dir/$appearance-theme.auto.conf"
test -r "$file"
or continue
set -l now (command shasum <"$file" | string split -f1 ' ')
test "$now" = "$before[$index]"
and continue
command mkdir -p "$config_dir/themes"
or return $status
command cp "$file" "$config_dir/themes/$appearance.conf"
or return $status
end
"$HOME/.config/kitty/apply-theme-mode.sh" "$config_dir"
or return $status or return $status
if set -q KITTY_WINDOW_ID if set -q KITTY_WINDOW_ID

View File

@ -1,7 +1,5 @@
function theme --description 'Switch kitty between its light and dark color themes' function theme --description 'Switch kitty between its light and dark color themes'
set -l config_dir $HOME/.config/kitty set -l config_dir (__kitty_config_dir)
set -q KITTY_CONFIG_DIRECTORY
and set config_dir $KITTY_CONFIG_DIRECTORY
if test (count $argv) -gt 1 if test (count $argv) -gt 1
echo "theme: expected at most one argument" >&2 echo "theme: expected at most one argument" >&2
@ -18,13 +16,21 @@ function theme --description 'Switch kitty between its light and dark color them
'Usage: theme [light|dark|auto|status]' \ 'Usage: theme [light|dark|auto|status]' \
'' \ '' \
' theme toggle between light and dark' \ ' theme toggle between light and dark' \
' theme light switch to light' \ ' theme light pin kitty to light' \
' theme dark switch to dark' \ ' theme dark pin kitty to dark' \
' theme auto follow the macOS appearance again' \ ' theme auto follow the macOS appearance again' \
' theme status print the current mode' ' theme status print the current mode' \
'' \
'The mode is remembered in '(__kitty_config_dir)'/theme-mode and' \
'applies to new kitty instances too.'
return 0 return 0
case status case status
__kitty_theme_current set -l setting (__kitty_theme_mode_setting)
if test "$setting" = auto
printf '%s (auto, following macOS)\n' (__kitty_theme_current)
else
printf '%s (pinned)\n' $setting
end
return 0 return 0
case toggle case toggle
if test (__kitty_theme_current) = dark if test (__kitty_theme_current) = dark
@ -32,6 +38,10 @@ function theme --description 'Switch kitty between its light and dark color them
else else
set target dark set target dark
end end
# Nothing to pin when the result already matches macOS -- record
# `auto` so kitty keeps following the OS from here on.
test "$target" = (__kitty_macos_appearance)
and set target auto
case auto light dark case auto light dark
# handled below # handled below
case '*' case '*'
@ -39,45 +49,28 @@ function theme --description 'Switch kitty between its light and dark color them
return 2 return 2
end end
if not set -q KITTY_WINDOW_ID set -l apply "$HOME/.config/kitty/apply-theme-mode.sh"
echo "theme: not running inside kitty" >&2 if not test -x $apply
echo "theme: missing $apply" >&2
return 1 return 1
end end
if test "$target" = auto echo $target >$config_dir/theme-mode
# Re-reading the config restores kitty's own OS-appearance handling, or return $status
# which a manual override suppresses until the OS appearance changes.
$apply "$config_dir"
or return $status
# Reloading is what makes kitty pick up the regenerated theme files.
if set -q KITTY_WINDOW_ID
kitty @ load-config kitty @ load-config
or return $status or return $status
if test "$(defaults read -g AppleInterfaceStyle 2>/dev/null)" = Dark
set target dark
else
set target light
end
else
set -l theme_file $config_dir/$target-theme.auto.conf
if not test -r $theme_file
echo "theme: cannot read $theme_file" >&2
return 1
end end
# Only pass through settings whose value is a hex color; kitty's set -l effective (__kitty_theme_current)
# set-colors rejects everything else these files may contain.
set -l colors (awk 'NF == 2 && $1 !~ /^#/ && $2 ~ /^#[0-9a-fA-F]+$/ { print $1 "=" $2 }' $theme_file)
if test (count $colors) -eq 0
echo "theme: no colors found in $theme_file" >&2
return 1
end
kitty @ set-colors --all --configured $colors
or return $status
end
set --universal kitty_theme_mode $target
if functions -q __tide_apply_terminal_appearance if functions -q __tide_apply_terminal_appearance
__tide_apply_terminal_appearance $target __tide_apply_terminal_appearance $effective
set -l apply_status $status set -l apply_status $status
if test $apply_status -eq 0 if test $apply_status -eq 0
functions -q _tide_cache_variables functions -q _tide_cache_variables
@ -87,5 +80,9 @@ function theme --description 'Switch kitty between its light and dark color them
end end
end end
echo $target if test "$target" = auto
printf '%s (auto, following macOS)\n' $effective
else
printf '%s (pinned)\n' $effective
end
end end