From a68b138d389095788026daedce576ea348a8a8c7 Mon Sep 17 00:00:00 2001 From: Anton Volnuhin Date: Tue, 28 Jul 2026 04:53:48 +0300 Subject: [PATCH] fix(kitty): preserve tab styling across themes --- .../private_executable_repair-auto-themes.sh | 28 ++++++ ...vate_executable_test_repair_auto_themes.sh | 65 +++++++++++++ .../private_test_tab_bar_colors.py | 93 +++++++++++++++++++ private_dot_config/kitty/tab_bar.py | 29 +++++- .../functions/private_kitten.fish | 23 +++++ 5 files changed, 233 insertions(+), 5 deletions(-) create mode 100644 private_dot_config/kitty/private_executable_repair-auto-themes.sh create mode 100644 private_dot_config/kitty/private_tests/private_executable_test_repair_auto_themes.sh create mode 100644 private_dot_config/kitty/private_tests/private_test_tab_bar_colors.py create mode 100644 private_dot_config/private_fish/functions/private_kitten.fish diff --git a/private_dot_config/kitty/private_executable_repair-auto-themes.sh b/private_dot_config/kitty/private_executable_repair-auto-themes.sh new file mode 100644 index 0000000..666492f --- /dev/null +++ b/private_dot_config/kitty/private_executable_repair-auto-themes.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +set -eu + +config_dir=${1:-"$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"} + +for appearance in dark light no-preference; do + config_file="$config_dir/$appearance-theme.auto.conf" + [ -f "$config_file" ] || continue + + if awk ' + $1 == "macos_titlebar_color" { + count++ + if ($2 == "background") correct++ + } + END { exit !(count == 1 && correct == 1) } + ' "$config_file"; then + continue + fi + + tmp=$(mktemp "$config_file.tmp.XXXXXX") + trap 'rm -f "$tmp"' EXIT HUP INT TERM + awk '$1 != "macos_titlebar_color"' "$config_file" > "$tmp" + printf '\nmacos_titlebar_color background\n' >> "$tmp" + chmod "$(stat -f '%Lp' "$config_file")" "$tmp" + mv -f "$tmp" "$config_file" + trap - EXIT HUP INT TERM +done diff --git a/private_dot_config/kitty/private_tests/private_executable_test_repair_auto_themes.sh b/private_dot_config/kitty/private_tests/private_executable_test_repair_auto_themes.sh new file mode 100644 index 0000000..0ce31ea --- /dev/null +++ b/private_dot_config/kitty/private_tests/private_executable_test_repair_auto_themes.sh @@ -0,0 +1,65 @@ +#!/bin/sh + +set -eu + +root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +tmpdir=$(mktemp -d) +trap 'rm -rf "$tmpdir"' EXIT HUP INT TERM +config_dir="$tmpdir/config" +fake_bin="$tmpdir/bin" +log="$tmpdir/commands.log" +mkdir -p "$config_dir" "$fake_bin" + +printf 'background #111111\n' > "$config_dir/dark-theme.auto.conf" +printf 'background #eeeeee\nmacos_titlebar_color system\n' \ + > "$config_dir/light-theme.auto.conf" +printf 'background #222222\nmacos_titlebar_color background\n' \ + > "$config_dir/no-preference-theme.auto.conf" + +"$root/repair-auto-themes.sh" "$config_dir" +"$root/tests/verify_auto_themes.sh" "$config_dir" + +for appearance in dark light no-preference; do + count=$(grep -c '^macos_titlebar_color background$' \ + "$config_dir/$appearance-theme.auto.conf") + [ "$count" -eq 1 ] +done + +before=$(shasum "$config_dir"/*-theme.auto.conf) +"$root/repair-auto-themes.sh" "$config_dir" +after=$(shasum "$config_dir"/*-theme.auto.conf) +[ "$before" = "$after" ] + +cat > "$fake_bin/kitten" <<'EOF' +#!/bin/sh +printf 'kitten %s\n' "$*" >> "$TEST_LOG" +exit 0 +EOF +cat > "$fake_bin/kitty" <<'EOF' +#!/bin/sh +printf 'kitty %s\n' "$*" >> "$TEST_LOG" +exit 0 +EOF +chmod +x "$fake_bin/kitten" "$fake_bin/kitty" + +printf 'background #fafafa\n' > "$config_dir/light-theme.auto.conf" +TEST_LOG="$log" PATH="$fake_bin:$PATH" \ + KITTY_CONFIG_DIRECTORY="$config_dir" KITTY_WINDOW_ID=1 \ + fish --no-config -c \ + 'source "$HOME/.config/fish/functions/kitten.fish"; kitten themes Tomorrow' +grep -q '^kitten themes Tomorrow$' "$log" +grep -q '^kitty @ load-config$' "$log" +"$root/tests/verify_auto_themes.sh" "$config_dir" + +: > "$log" +TEST_LOG="$log" PATH="$fake_bin:$PATH" \ + KITTY_CONFIG_DIRECTORY="$config_dir" KITTY_WINDOW_ID=1 \ + fish --no-config -c \ + 'source "$HOME/.config/fish/functions/kitten.fish"; kitten icat image.png' +grep -q '^kitten icat image.png$' "$log" +if grep -q '^kitty ' "$log"; then + echo "non-theme kitten command triggered a Kitty reload" >&2 + exit 1 +fi + +echo "PASS: theme repair is idempotent and the Fish shim only intercepts themes" diff --git a/private_dot_config/kitty/private_tests/private_test_tab_bar_colors.py b/private_dot_config/kitty/private_tests/private_test_tab_bar_colors.py new file mode 100644 index 0000000..1468b8a --- /dev/null +++ b/private_dot_config/kitty/private_tests/private_test_tab_bar_colors.py @@ -0,0 +1,93 @@ +from importlib.util import module_from_spec, spec_from_file_location +from pathlib import Path + +from kitty.fast_data_types import Color +from kitty.tab_bar import DrawData, ExtraData, TabBarData, as_rgb + + +class Cursor: + x = 0 + fg = as_rgb(0xFEDCBA) + bg = as_rgb(0xABCDEF) + + +class RecordingScreen: + def __init__(self) -> None: + self.cursor = Cursor() + self.draws: list[tuple[str, int, int]] = [] + + def draw(self, text: str) -> None: + self.draws.append((text, self.cursor.fg, self.cursor.bg)) + self.cursor.x += len(text) + + +def load_renderer(): + path = Path.cwd() / "tab_bar.py" + spec = spec_from_file_location("kitty_user_tab_bar", path) + if spec is None or spec.loader is None: + raise RuntimeError(f"cannot load {path}") + module = module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def draw_data(theme_bg: int) -> DrawData: + return DrawData( + 0, + "", + 0, + "", + (), + Color(0x11, 0x11, 0x11), + Color(0xAB, 0xCD, 0xEF), + Color(0xFE, 0xDC, 0xBA), + Color(0x22, 0x22, 0x22), + Color((theme_bg >> 16) & 0xFF, (theme_bg >> 8) & 0xFF, theme_bg & 0xFF), + "", + None, + "", + "default", + "top", + 0, + 0, + ) + + +renderer = load_renderer() +for theme_bg in (0xF8F8F8, 0x1C2836): + roles = renderer.tab_colors(draw_data(theme_bg).default_bg) + actual_roles = tuple(int(value) for value in roles) + expected_roles = (theme_bg, 0x2E84E6, 0x2E84E6, theme_bg) + if actual_roles != expected_roles: + raise RuntimeError( + f"theme #{theme_bg:06x}: expected roles {expected_roles!r}, " + f"got {actual_roles!r}" + ) + + for is_active, expected in ( + (True, (as_rgb(theme_bg), as_rgb(0x2E84E6))), + (False, (as_rgb(0x2E84E6), as_rgb(theme_bg))), + ): + screen = RecordingScreen() + renderer.draw_tab( + draw_data(theme_bg), + screen, + TabBarData("tab", is_active=is_active), + 0, + 1, + 1, + True, + ExtraData(), + ) + _, actual_fg, actual_bg = screen.draws[0] + if (actual_fg, actual_bg) != expected: + raise RuntimeError( + f"theme #{theme_bg:06x}, active={is_active}: " + f"expected cursor colors {expected!r}, " + f"got {(actual_fg, actual_bg)!r}" + ) + +print( + "PASS: tab colors ignore theme-provided tab roles " + "for light and dark backgrounds" +) diff --git a/private_dot_config/kitty/tab_bar.py b/private_dot_config/kitty/tab_bar.py index 1ec7c5c..0e61082 100644 --- a/private_dot_config/kitty/tab_bar.py +++ b/private_dot_config/kitty/tab_bar.py @@ -1,16 +1,35 @@ -from kitty.fast_data_types import Screen +from typing import NamedTuple + +from kitty.fast_data_types import Color, Screen from kitty.tab_bar import DrawData, ExtraData, TabBarData, as_rgb, draw_title + +class TabColors(NamedTuple): + active_fg: Color + active_bg: Color + inactive_fg: Color + inactive_bg: Color + + +TAB_ACCENT = Color(0x2e, 0x84, 0xe6) + + +def tab_colors(theme_bg: Color) -> TabColors: + return TabColors(theme_bg, TAB_ACCENT, TAB_ACCENT, theme_bg) + + def draw_tab( draw_data: DrawData, screen: Screen, tab: TabBarData, before: int, max_title_length: int, index: int, is_last: bool, extra_data: ExtraData ) -> int: - active_fg = draw_data.default_bg + colors = tab_colors(draw_data.default_bg) + fg = colors.active_fg if tab.is_active else colors.inactive_fg + bg = colors.active_bg if tab.is_active else colors.inactive_bg default_bg = as_rgb(int(draw_data.default_bg)) - orig_fg = as_rgb(int(active_fg)) if tab.is_active else screen.cursor.fg - orig_bg = screen.cursor.bg if tab.is_active else default_bg - title_draw_data = draw_data._replace(active_fg=active_fg) if tab.is_active else draw_data + orig_fg = as_rgb(int(fg)) + orig_bg = as_rgb(int(bg)) + title_draw_data = draw_data._replace(**colors._asdict()) screen.cursor.fg = orig_fg screen.cursor.bg = orig_bg left_sep, right_sep = ('', '') diff --git a/private_dot_config/private_fish/functions/private_kitten.fish b/private_dot_config/private_fish/functions/private_kitten.fish new file mode 100644 index 0000000..2a06e8d --- /dev/null +++ b/private_dot_config/private_fish/functions/private_kitten.fish @@ -0,0 +1,23 @@ +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" + if set -q KITTY_CONFIG_DIRECTORY + set config_dir "$KITTY_CONFIG_DIRECTORY" + end + + "$HOME/.config/kitty/repair-auto-themes.sh" "$config_dir" + or return $status + + if set -q KITTY_WINDOW_ID + command kitty @ load-config + or return $status + end + end + + return $kitten_status +end