fish, kitty: follow terminal light theme

This commit is contained in:
Anton Volnuhin 2026-07-27 20:20:38 +03:00
parent 781c86d80a
commit 73176c2bfb
9 changed files with 501 additions and 8 deletions

View File

@ -1068,8 +1068,7 @@ tab_bar_style custom
# active_tab_foreground #000
active_tab_background #2e84e6
# active_tab_font_style bold-italic
inactive_tab_foreground #4d82bf
inactive_tab_background #141c26
inactive_tab_foreground #2e84e6
# inactive_tab_font_style normal
#: Tab bar colors and styles.

View File

@ -1,14 +1,18 @@
from kitty.fast_data_types import Screen
from kitty.tab_bar import DrawData, ExtraData, TabBarData, draw_title
from kitty.tab_bar import DrawData, ExtraData, TabBarData, as_rgb, draw_title
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:
orig_fg = screen.cursor.fg
orig_bg = screen.cursor.bg
default_bg = int(draw_data.default_bg)
active_fg = draw_data.default_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
screen.cursor.fg = orig_fg
screen.cursor.bg = orig_bg
left_sep, right_sep = ('', '')
def draw_sep(which: str) -> None:
@ -29,7 +33,7 @@ def draw_tab(
else:
draw_sep(left_sep)
screen.draw(' ')
draw_title(draw_data, screen, tab, index)
draw_title(title_draw_data, screen, tab, index)
extra = screen.cursor.x - before - max_title_length
# print("extra:%d" %(extra))
if extra >= 0:

View File

@ -0,0 +1,26 @@
status is-interactive
or exit
type -q tide
or exit
function __tide_terminal_color_theme_changed --on-variable fish_terminal_color_theme
contains -- $fish_terminal_color_theme dark light
or return
__tide_apply_terminal_appearance $fish_terminal_color_theme
set -l apply_status $status
test $apply_status -eq 2
and return 2
test $apply_status -eq 1
and return 0
functions -q _tide_cache_variables
and _tide_cache_variables
functions -q _tide_sub_reload
and tide reload
commandline --function repaint
end
__tide_terminal_color_theme_changed

View File

@ -0,0 +1,41 @@
function __tide_apply_terminal_appearance --argument-names appearance
contains -- $appearance dark light
or return 2
if test "$__tide_terminal_appearance" = "$appearance"
return 1
end
if not set --query __tide_terminal_color_names
set --global __tide_terminal_color_names
set --global __tide_terminal_dark_colors
for name in (set --universal --names | string match -e -r '^tide_' | sort)
string match -q 'tide_*color*' -- $name
or continue
set -l current_value $$name
test (count $current_value) -eq 1
or continue
string match -qr '^[0-9A-Fa-f]{6}$' -- $current_value
or continue
set --append __tide_terminal_color_names $name
set --append __tide_terminal_dark_colors $current_value
end
end
test (count $__tide_terminal_color_names) -gt 0
or return 2
for index in (seq (count $__tide_terminal_color_names))
set -l name $__tide_terminal_color_names[$index]
set -l color $__tide_terminal_dark_colors[$index]
if test "$appearance" = light
set color (__tide_terminal_light_color $color)
end
set --global --export $name $color
end
set --global __tide_terminal_appearance $appearance
end

View File

@ -0,0 +1,76 @@
function __tide_terminal_light_color --argument-names dark_color
switch (string upper -- $dark_color)
case 303030
echo E4E4E4
case 0087AF
echo 006E8F
case 00ACD7
echo 006F8B
case 00AFAF
echo 007272
case 00AFFF
echo 006C9E
case 2496ED
echo 196AA8
case 326CE5
echo 2D60CD
case 4285F4
echo 3264B8
case 44883E
echo 397234
case 4E2A8E
echo 4E2A8E
case 5F8787
echo 4C6C6C
case 5FAF00
echo 3F7300
case 5FD700
echo 337400
case 613583
echo 613583
case 617CBE
echo 4F659B
case 6C6C6C 949494 EEEEEE FFFFFF
echo 666666
case 7EBAE4
echo 486A83
case 844FBA
echo 814DB5
case 87875F
echo 68684A
case 8787AF
echo 636381
case 87AF87
echo 546C54
case 87AFAF
echo 526A6A
case B31209
echo B31209
case D70000 FF0000
echo CF0000
case D7AF00
echo 7B6400
case D7AF87
echo 79624C
case ED8B00
echo 955700
case F74C00
echo B93900
case F7A41D
echo 8B5C10
case F7BF2A
echo 7F6216
case FBF0DF
echo 6A655E
case FF00FF
echo B600B6
case FF8700
echo 9C5300
case FF9900
echo 935800
case D78700
echo 905A00
case '*'
echo $dark_color
end
end

View File

@ -0,0 +1,50 @@
import fs from 'node:fs';
const input = fs.readFileSync(0, 'utf8').trim();
if (!input) {
console.error('no foreground color pairs received');
process.exit(1);
}
const channelLuminance = (value) => {
const channel = value / 255;
return channel <= 0.04045
? channel / 12.92
: ((channel + 0.055) / 1.055) ** 2.4;
};
const luminance = (hex) => {
const channels = hex.match(/../g).map((pair) => Number.parseInt(pair, 16));
return (
0.2126 * channelLuminance(channels[0])
+ 0.7152 * channelLuminance(channels[1])
+ 0.0722 * channelLuminance(channels[2])
);
};
const backgroundLuminance = luminance('E4E4E4');
const failures = [];
for (const line of input.split('\n')) {
const [dark, light] = line.trim().split(/\s+/);
if (!/^[0-9A-F]{6}$/.test(dark) || !/^[0-9A-F]{6}$/.test(light)) {
failures.push(`${line}: expected two six-digit uppercase colors`);
continue;
}
const foregroundLuminance = luminance(light);
const ratio = (
Math.max(backgroundLuminance, foregroundLuminance) + 0.05
) / (
Math.min(backgroundLuminance, foregroundLuminance) + 0.05
);
if (ratio < 4.5) {
failures.push(`${dark} -> ${light}: contrast ${ratio.toFixed(2)} is below 4.5`);
}
}
if (failures.length > 0) {
console.error(failures.join('\n'));
process.exit(1);
}

View File

@ -0,0 +1,16 @@
set -l project_root (path resolve (path dirname (status filename))/..)
source $project_root/functions/tide.fish
source $project_root/functions/__tide_terminal_light_color.fish
source $project_root/functions/__tide_apply_terminal_appearance.fish
source $project_root/conf.d/tide_terminal_appearance.fish
set -l theme_handler (
functions --handlers-type variable |
string match 'fish_terminal_color_theme *'
)
printf '\nHANDLER|%s\n' (string escape -- $theme_handler)
functions -q __tide_terminal_install_bindings
and printf 'LEGACY_BINDINGS|present\n'
or printf 'LEGACY_BINDINGS|absent\n'

View File

@ -0,0 +1,159 @@
#!/usr/bin/env fish
set -l project_root (path resolve (path dirname (status filename))/..)
function __test_fail --argument-names message
printf 'not ok - %s\n' "$message" >&2
exit 1
end
function __test_assert_equal --argument-names expected actual message
test "$expected" = "$actual"
or __test_fail "$message: expected <$expected>, got <$actual>"
end
function __test_is_tide_hex_color --argument-names name
string match -q 'tide_*color*' -- $name
and test (count $$name) -eq 1
and string match -qr '^[0-9A-Fa-f]{6}$' -- $$name
end
function __test_snapshot --argument-names include_colors
for name in (set --universal --names | string match -e -r '^tide_')
__test_is_tide_hex_color $name
set -l is_color $status
if test "$include_colors" = yes
test $is_color -eq 0; or continue
else
test $is_color -ne 0; or continue
end
printf '%s=%s\n' $name (string escape -- $$name)
end
end
set -l light_mapper $project_root/functions/__tide_terminal_light_color.fish
set -l appearance_applicator $project_root/functions/__tide_apply_terminal_appearance.fish
test -f $light_mapper
or __test_fail "missing light palette mapper"
test -f $appearance_applicator
or __test_fail "missing terminal appearance applicator"
source $light_mapper
source $appearance_applicator
__test_assert_equal E4E4E4 (__tide_terminal_light_color 303030) \
"dark segment background maps to the light segment background"
__test_assert_equal 006C9E (__tide_terminal_light_color 00AFFF) \
"bright blue maps to its contrast-safe light equivalent"
__test_assert_equal CF0000 (__tide_terminal_light_color FF0000) \
"bright red maps to its contrast-safe light equivalent"
__test_assert_equal glyph (__tide_terminal_light_color glyph) \
"non-color values are preserved"
set -l empty_palette_result (
fish --no-config -c '
source $argv[1]
source $argv[2]
__tide_apply_terminal_appearance light
printf "status=%s" $status
' $light_mapper $appearance_applicator 2>&1 |
string collect
)
__test_assert_equal status=2 $empty_palette_result \
"an unconfigured Tide palette is rejected without errors"
set -l expected_color_names
set -l expected_dark_colors
for name in (set --universal --names | string match -e -r '^tide_')
if __test_is_tide_hex_color $name
set --append expected_color_names $name
set --append expected_dark_colors $$name
end
end
test (count $expected_color_names) -gt 0
or __test_fail "the test fixture has no Tide colors"
set -l before_non_colors (string join \x1e (__test_snapshot no))
__tide_apply_terminal_appearance light
__test_assert_equal 0 $status "light appearance applies successfully"
__test_assert_equal E4E4E4 $tide_pwd_bg_color \
"light appearance replaces the existing prompt segment background"
__test_assert_equal 006C9E $tide_pwd_color_anchors \
"light appearance replaces the existing prompt foreground"
set -l pwd_scope (set --show tide_pwd_bg_color | string collect)
string match -q '*set in global scope, exported*' -- $pwd_scope
or __test_fail "Tide overrides are not global and exported"
set -l child_background (fish --no-config -c 'printf %s "$tide_pwd_bg_color"')
__test_assert_equal E4E4E4 $child_background \
"Tide renderer subprocess inherits the light background"
set -l after_non_colors (string join \x1e (__test_snapshot no))
__test_assert_equal "$before_non_colors" "$after_non_colors" \
"non-color Tide configuration remains unchanged"
set -l first_light_state
for name in $expected_color_names
set --append first_light_state "$name="(string escape -- $$name)
end
__tide_apply_terminal_appearance light
__test_assert_equal 1 $status "repeated light appearance is a no-op"
set -l second_light_state
for name in $expected_color_names
set --append second_light_state "$name="(string escape -- $$name)
end
__test_assert_equal (string join \x1e $first_light_state) \
(string join \x1e $second_light_state) \
"repeated light appearance preserves all color overrides"
begin
for index in (seq (count $expected_dark_colors))
set -l dark (string upper -- $expected_dark_colors[$index])
test "$dark" = 303030; and continue
printf '%s %s\n' $dark (__tide_terminal_light_color $dark)
end
end | node $project_root/tests/assert_tide_light_contrast.mjs
or __test_fail "light palette foreground contrast is below 4.5:1"
__tide_apply_terminal_appearance dark
__test_assert_equal 0 $status "dark appearance applies successfully"
for index in (seq (count $expected_color_names))
set -l name $expected_color_names[$index]
__test_assert_equal $expected_dark_colors[$index] $$name \
"dark appearance restores $name exactly"
end
__tide_apply_terminal_appearance invalid
__test_assert_equal 2 $status "invalid appearance is rejected"
__test_assert_equal dark $__tide_terminal_appearance \
"invalid appearance does not change active state"
set -l integration $project_root/conf.d/tide_terminal_appearance.fish
test -f $integration
or __test_fail "missing live terminal appearance integration"
set -l integration_output (
env TERM=xterm-kitty fish --interactive --no-config \
$project_root/tests/inspect_tide_terminal_handler.fish |
string collect
)
set -l handler_line (
string split \n -- $integration_output |
string match 'HANDLER|*'
)
string match -q \
'*fish_terminal_color_theme __tide_terminal_color_theme_changed*' \
-- $handler_line
or __test_fail "integration does not subscribe to Fish's terminal color theme"
string match -q '*LEGACY_BINDINGS|absent*' -- $integration_output
or __test_fail "integration still installs raw terminal response bindings"
printf 'ok - Tide terminal palette behavior\n'

View File

@ -0,0 +1,122 @@
#!/usr/bin/expect -f
set timeout 30
log_user 0
set path_label "direct"
set through_tmux 0
if {[llength $argv] > 0 && [lindex $argv 0] eq "tmux"} {
set path_label "tmux"
set through_tmux 1
spawn env TERM=xterm-kitty tmux -L tide-appearance-test \
-f "$env(HOME)/.tmux.conf" new-session fish --interactive
} else {
spawn env TERM=xterm-kitty fish --interactive
}
if {$through_tmux} {
expect {
-exact "\033\[?996n" {
send -- "\033\[?997;2n"
exp_continue
}
-exact "\033]10;?\033\\" {
send -- "\033]10;rgb:0000/0000/0000\033\\"
exp_continue
}
-exact "\033]11;?\033\\" {
send -- "\033]11;rgb:ffff/ffff/ffff\033\\"
exp_continue
}
-exact "\033\[0c" {
send -- "\033\[?1;2c"
exp_continue
}
-exact " fish " {
after 1000
}
timeout {
puts stderr "Fish did not start inside tmux"
exit 1
}
}
} else {
expect {
-exact "\033]11;?\033\\" {
send -- "\033]11;rgb:ffff/ffff/ffff\033\\"
exp_continue
}
-exact "\033\[0c" {
send -- "\033\[?1;2c"
exp_continue
}
-exact "\033]133;B\033\\" {}
timeout {
puts stderr "Fish did not reach its initial prompt"
exit 1
}
}
}
send -- "printf 'CHECK:%s:%s\\n' \$__tide_terminal_appearance \$tide_pwd_bg_color\r"
expect {
-re {CHECK:light:E4E4E4} {}
timeout {
puts stderr "The initial light terminal background did not apply the light Tide palette"
exit 1
}
}
if {!$through_tmux} {
expect {
-exact "\033\[?2031h" {}
timeout {
puts stderr "Fish did not re-enable theme notifications after the light check"
exit 1
}
}
}
send -- "\033\[?997;1n"
expect {
-exact "\033]11;?\033\\" {
send -- "\033]11;rgb:0000/0000/0000\033\\"
}
timeout {
puts stderr "Fish did not query the terminal background after a theme notification"
exit 1
}
}
if {$through_tmux} {
after 100
} else {
expect {
-exact "\033]133;B\033\\" {}
timeout {
puts stderr "Fish did not repaint after applying the dark terminal theme"
exit 1
}
}
}
send -- "printf 'CHECK:%s:%s\\n' \$__tide_terminal_appearance \$tide_pwd_bg_color\r"
expect {
-re {CHECK:dark:303030} {}
timeout {
puts stderr "The dark notification did not restore the dark Tide palette"
exit 1
}
}
send -- "exit\r"
expect {
-exact "\033\[?2031l" {}
timeout {
puts stderr "Fish did not disable appearance notifications on exit"
exit 1
}
}
expect eof
puts "ok - live terminal notification events ($path_label)"