dotfiles/private_dot_config/kitty/private_executable_apply-theme-mode.sh
Anton Volnuhin 9e2667f8fe kitty: capture theme choices however the kitten was launched
Saving a theme stopped sticking: the choice was captured only by the fish
`kitten` wrapper, so `kitty +kitten themes`, a keybinding or any non-fish
shell wrote <appearance>-theme.auto.conf directly and the next regeneration
overwrote it from theme-modes. The dark slot had already lost a pick that
way -- dark-theme.auto.conf held #0d0f31 while canonical dark said #2b2c2e.

Move the capture into apply-theme-mode.sh, which every path runs. It records
a cksum of each generated file after the repair step; on the next run a file
whose checksum differs was written by something else, so it becomes canonical
for that appearance. With no state recorded yet nothing is adopted, so a
first run cannot mistake a pinned mode's duplicated files for real choices.

The fish wrapper keeps only its useful part: re-apply the mode and reload so
the change shows at once.

theme-modes/dark.conf is the recovered #0d0f31 (1984). Covered by a new case
in tests/test_theme_mode.sh that writes the theme file with no shell involved.
2026-08-06 04:42:59 +03:00

129 lines
4.5 KiB
Bash

#!/bin/sh
# Regenerate the *-theme.auto.conf files kitty reads at startup from the
# canonical themes in ./theme-modes, 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
# ./theme-modes and these three files are generated. (./themes is separate:
# that is where the themes kitten looks for user themes.)
set -eu
# Permission bits of a file, or 600 if they cannot be read. GNU stat spells this
# `-c %a` while BSD/macOS spells it `-f %Lp`, and each mis-parses the other's
# form -- GNU treats `-f` as --file-system and prints a block of filesystem info
# before failing -- so try one at a time and only accept an octal answer.
file_mode() {
_m=$(stat -c '%a' "$1" 2>/dev/null) || _m=''
case "$_m" in
[0-7][0-7][0-7] | [0-7][0-7][0-7][0-7]) printf '%s\n' "$_m"; return 0 ;;
esac
_m=$(stat -f '%Lp' "$1" 2>/dev/null) || _m=''
case "$_m" in
[0-7][0-7][0-7] | [0-7][0-7][0-7][0-7]) printf '%s\n' "$_m"; return 0 ;;
esac
printf '600\n'
}
script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
config_dir=${1:-"$script_dir"}
canonical_dir="$config_dir/theme-modes"
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"
# Checksums of the files as this script last wrote them. Anything that differs
# now was rewritten by something else -- in practice the themes kitten saving a
# theme for that appearance -- so it is a new choice and becomes canonical.
# Recording it here rather than in the fish `kitten` wrapper means the choice
# survives however the kitten was launched: `kitty +kitten themes`, a keybinding
# or another shell all bypass that wrapper.
state_file="$config_dir/.theme-generated"
# cksum is POSIX, unlike shasum/sha256sum which differ across platforms.
sum_of() {
[ -f "$1" ] || return 1
cksum < "$1"
}
recorded_sum() {
[ -f "$state_file" ] || return 1
awk -v want="$1" '$1 == want { $1 = ""; sub(/^ /, ""); print; found = 1 }
END { exit !found }' "$state_file"
}
for appearance in dark light no-preference; do
canonical="$canonical_dir/$appearance.conf"
generated="$config_dir/$appearance-theme.auto.conf"
[ -f "$generated" ] || continue
# First adoption of an existing setup: take whatever is there, but only
# while following the OS. Under a pinned mode every generated file holds
# the pinned theme, so adopting them would overwrite the other appearances.
if [ ! -f "$canonical" ]; then
[ "$mode" = auto ] && cp "$generated" "$canonical"
continue
fi
# With no state yet we cannot tell a fresh choice from a file we wrote, so
# leave canonical alone; the checksums recorded below make the next run
# able to tell.
now=$(sum_of "$generated") || continue
was=$(recorded_sum "$appearance") || continue
[ "$now" = "$was" ] || cp "$generated" "$canonical"
done
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
if [ -f "$generated" ]; then
mode_bits=$(file_mode "$generated")
fi
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"
# Record what we ended up with, after the repair step, so the next run can tell
# an externally-saved theme from one of ours.
tmp_state=$(mktemp "$state_file.tmp.XXXXXX")
trap 'rm -f "$tmp_state"' EXIT HUP INT TERM
for appearance in dark light no-preference; do
generated="$config_dir/$appearance-theme.auto.conf"
[ -f "$generated" ] || continue
printf '%s %s\n' "$appearance" "$(sum_of "$generated")" >> "$tmp_state"
done
chmod 600 "$tmp_state"
mv -f "$tmp_state" "$state_file"
trap - EXIT HUP INT TERM