diff --git a/private_dot_config/kitty/kitty.conf b/private_dot_config/kitty/kitty.conf index 5984230..9cc6ee3 100644 --- a/private_dot_config/kitty/kitty.conf +++ b/private_dot_config/kitty/kitty.conf @@ -1066,10 +1066,9 @@ tab_bar_style custom #: tab_title_template. # active_tab_foreground #000 - active_tab_background #2e84e6 +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. diff --git a/private_dot_config/kitty/tab_bar.py b/private_dot_config/kitty/tab_bar.py index ac37265..1ec7c5c 100644 --- a/private_dot_config/kitty/tab_bar.py +++ b/private_dot_config/kitty/tab_bar.py @@ -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: diff --git a/private_dot_config/private_fish/conf.d/private_tide_terminal_appearance.fish b/private_dot_config/private_fish/conf.d/private_tide_terminal_appearance.fish new file mode 100644 index 0000000..2ce9869 --- /dev/null +++ b/private_dot_config/private_fish/conf.d/private_tide_terminal_appearance.fish @@ -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 diff --git a/private_dot_config/private_fish/functions/private___tide_apply_terminal_appearance.fish b/private_dot_config/private_fish/functions/private___tide_apply_terminal_appearance.fish new file mode 100644 index 0000000..5f23781 --- /dev/null +++ b/private_dot_config/private_fish/functions/private___tide_apply_terminal_appearance.fish @@ -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 diff --git a/private_dot_config/private_fish/functions/private___tide_terminal_light_color.fish b/private_dot_config/private_fish/functions/private___tide_terminal_light_color.fish new file mode 100644 index 0000000..6b01de8 --- /dev/null +++ b/private_dot_config/private_fish/functions/private___tide_terminal_light_color.fish @@ -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 diff --git a/private_dot_config/private_fish/private_tests/private_assert_tide_light_contrast.mjs b/private_dot_config/private_fish/private_tests/private_assert_tide_light_contrast.mjs new file mode 100644 index 0000000..4e1110d --- /dev/null +++ b/private_dot_config/private_fish/private_tests/private_assert_tide_light_contrast.mjs @@ -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); +} diff --git a/private_dot_config/private_fish/private_tests/private_inspect_tide_terminal_handler.fish b/private_dot_config/private_fish/private_tests/private_inspect_tide_terminal_handler.fish new file mode 100644 index 0000000..bacf004 --- /dev/null +++ b/private_dot_config/private_fish/private_tests/private_inspect_tide_terminal_handler.fish @@ -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' diff --git a/private_dot_config/private_fish/private_tests/private_test_tide_terminal_appearance.fish b/private_dot_config/private_fish/private_tests/private_test_tide_terminal_appearance.fish new file mode 100644 index 0000000..089329a --- /dev/null +++ b/private_dot_config/private_fish/private_tests/private_test_tide_terminal_appearance.fish @@ -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' diff --git a/private_dot_config/private_fish/private_tests/private_verify_tide_terminal_events.exp b/private_dot_config/private_fish/private_tests/private_verify_tide_terminal_events.exp new file mode 100644 index 0000000..698557c --- /dev/null +++ b/private_dot_config/private_fish/private_tests/private_verify_tide_terminal_events.exp @@ -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)"