From 79ccdb59728c0dd71b4f6118009dd7103f1e7b48 Mon Sep 17 00:00:00 2001 From: Peter Adams <18162810+Maxteabag@users.noreply.github.com> Date: Sun, 23 Aug 2026 18:29:25 +0200 Subject: [PATCH] Fix contrast in light themes --- .../domains/explorer/ui/mixins/tree_filter.py | 4 +- sqlit/domains/shell/ui/mixins/ui_status.py | 3 +- tests/ui/explorer/test_tree_filter_search.py | 11 ++++ tests/ui/test_light_theme_contrast.py | 60 +++++++++++++++++++ 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 tests/ui/test_light_theme_contrast.py diff --git a/sqlit/domains/explorer/ui/mixins/tree_filter.py b/sqlit/domains/explorer/ui/mixins/tree_filter.py index 2c7dfb33..3a92727e 100644 --- a/sqlit/domains/explorer/ui/mixins/tree_filter.py +++ b/sqlit/domains/explorer/ui/mixins/tree_filter.py @@ -344,8 +344,10 @@ def _find_matching_nodes( matches.append(node) # Store original label and apply highlighting self._tree_original_labels[id(node)] = str(node.label) + theme = getattr(self, "current_theme", None) + match_color = getattr(theme, "primary", "#1565C0") highlighted = highlight_matches( - escape_markup(label_text), indices, style="bold #FFFF00" + escape_markup(label_text), indices, style=f"bold {match_color}" ) # Preserve any existing markup prefix (like icons, colors) node.set_label(self._rebuild_label_with_highlight(node, highlighted)) diff --git a/sqlit/domains/shell/ui/mixins/ui_status.py b/sqlit/domains/shell/ui/mixins/ui_status.py index bfc32937..f2f9d820 100644 --- a/sqlit/domains/shell/ui/mixins/ui_status.py +++ b/sqlit/domains/shell/ui/mixins/ui_status.py @@ -200,7 +200,8 @@ def _update_status_bar(self: UINavigationMixinHost) -> None: from rich.markup import escape as escape_markup cmd_buffer = escape_markup(getattr(self, "_command_buffer", "")) - conn_info = f"[bold cyan]:{cmd_buffer}[/]" + command_color = getattr(self.current_theme, "primary", "#1565C0") + conn_info = f"[bold {command_color}]:{cmd_buffer}[/]" # Build status indicators status_parts = [] diff --git a/tests/ui/explorer/test_tree_filter_search.py b/tests/ui/explorer/test_tree_filter_search.py index f4a4a2a4..d4e44f92 100644 --- a/tests/ui/explorer/test_tree_filter_search.py +++ b/tests/ui/explorer/test_tree_filter_search.py @@ -12,6 +12,7 @@ from __future__ import annotations +from types import SimpleNamespace from unittest.mock import MagicMock from sqlit.domains.explorer.domain.tree_nodes import ( @@ -127,6 +128,7 @@ class _FilterHost(TreeFilterMixin): def __init__(self, connection_names: list[str]): self._connection_names = connection_names + self.current_theme = SimpleNamespace(primary="#1565C0") self.object_tree = MockTree() self.tree_filter_input = MockFilterInput() self._populate() @@ -211,6 +213,15 @@ def test_typing_t_filters_to_t_matches(self): f"Expected only 't'-matching connections visible, got {visible}" ) + def test_match_highlight_uses_current_theme_primary_color(self): + host = _FilterHost(["user-primary", "production"]) + + self._open_filter(host) + self._type(host, "user") + + assert len(host._tree_filter_matches) == 1 + assert "[bold #1565C0]" in str(host._tree_filter_matches[0].label) + def test_typing_tt_filters_out_everything(self): host = _FilterHost(self.CONNECTION_NAMES) diff --git a/tests/ui/test_light_theme_contrast.py b/tests/ui/test_light_theme_contrast.py new file mode 100644 index 00000000..7dc8e48a --- /dev/null +++ b/tests/ui/test_light_theme_contrast.py @@ -0,0 +1,60 @@ +"""Contrast regressions for command mode across Sqlit's light themes.""" + +from __future__ import annotations + +import pytest + +from sqlit.domains.shell.app.main import SSMSTUI + +from .mocks import MockConnectionStore, MockSettingsStore, build_test_services + +LIGHT_THEMES = [ + "sqlit-light", + "textual-light", + "solarized-light", + "catppuccin-latte", + "rose-pine-dawn", + "gruvbox-light", +] + + +def _relative_luminance(color: str) -> float: + channels = [int(color[index : index + 2], 16) / 255 for index in (1, 3, 5)] + + def linearize(channel: float) -> float: + if channel <= 0.04045: + return channel / 12.92 + return ((channel + 0.055) / 1.055) ** 2.4 + + red, green, blue = (linearize(channel) for channel in channels) + return 0.2126 * red + 0.7152 * green + 0.0722 * blue + + +def _contrast_ratio(foreground: str, background: str) -> float: + lighter, darker = sorted( + (_relative_luminance(foreground), _relative_luminance(background)), + reverse=True, + ) + return (lighter + 0.05) / (darker + 0.05) + + +@pytest.mark.parametrize("theme_name", LIGHT_THEMES) +@pytest.mark.asyncio +async def test_command_mode_uses_contrasting_theme_primary(theme_name: str) -> None: + services = build_test_services( + connection_store=MockConnectionStore(), + settings_store=MockSettingsStore({"theme": theme_name}), + ) + app = SSMSTUI(services=services) + + async with app.run_test() as pilot: + await pilot.pause() + app._command_mode = True + app._command_buffer = "theme" + app._update_status_bar() + await pilot.pause() + + rendered = app.status_bar.render() + assert rendered.plain == ":theme" + assert rendered.spans[0].style == f"bold {app.current_theme.primary}" + assert _contrast_ratio(app.current_theme.primary, app.current_theme.surface) >= 3