diff --git a/kitty/boss.py b/kitty/boss.py index f3cd4a5ce..a70c9314e 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -36,6 +36,7 @@ from .clipboard import ( set_clipboard_string, set_primary_selection, ) +from .colors import ColorsSpec, TransparentBackgroundColors, patch_options_with_color_spec, theme_colors from .conf.utils import BadLine, KeyAction, to_cmdline from .config import common_opts_as_dict, prepare_config_file_for_editing from .constants import ( @@ -2637,10 +2638,9 @@ class Boss: window.screen.disable_ligatures = strategy window.refresh() - def patch_colors(self, spec: dict[str, Optional[int]], transparent_background_colors: tuple[tuple[Color, float], ...], configured: bool = False) -> None: + def patch_colors(self, spec: ColorsSpec, transparent_background_colors: TransparentBackgroundColors, configured: bool = False) -> None: opts = get_options() if configured: - from kitty.colors import patch_options_with_color_spec patch_options_with_color_spec(opts, spec, transparent_background_colors) for tm in self.all_tab_managers: tm.tab_bar.patch_colors(spec) @@ -3063,10 +3063,8 @@ class Boss: return sanitize_url_for_dispay_to_user(url) def on_system_color_scheme_change(self, appearance: Literal['light', 'dark', 'no_preference'], is_initial_value: bool) -> None: - if is_initial_value: - pass - else: - log_error('system color theme changed:', appearance) + if not is_initial_value and appearance != 'no_preference': + theme_colors.on_system_color_scheme_change(appearance) @ac('win', ''' Toggle to the tab matching the specified expression diff --git a/kitty/colors.py b/kitty/colors.py index 952a71c75..8a989bf4d 100644 --- a/kitty/colors.py +++ b/kitty/colors.py @@ -2,15 +2,71 @@ # License: GPLv3 Copyright: 2024, Kovid Goyal import os -from typing import Iterable, Optional, Union +from contextlib import suppress +from typing import Iterable, Literal, Optional, Union from .config import parse_config -from .fast_data_types import Color +from .constants import config_dir +from .fast_data_types import Color, get_boss, glfw_get_system_color_theme from .options.types import Options, nullable_colors from .rgb import color_from_int +ColorsSpec = dict[str, Optional[int]] +TransparentBackgroundColors = tuple[tuple[Color, float], ...] -def parse_colors(args: Iterable[Union[str, Iterable[str]]]) -> tuple[dict[str, Optional[int]], tuple[tuple[Color, float], ...]]: + +class ThemeColors: + + dark_mtime: float = -1 + light_mtime: float = -1 + applied_theme: Literal['light', 'dark', ''] = '' + + def refresh(self) -> None: + with suppress(FileNotFoundError), open(os.path.join(config_dir, 'dark-theme.conf')) as f: + mtime = os.stat(f.fileno()).st_mtime + if mtime > self.dark_mtime: + self.dark_spec, self.dark_tbc = parse_colors((f,)) + self.dark_mtime = mtime + with suppress(FileNotFoundError), open(os.path.join(config_dir, 'light-theme.conf')) as f: + mtime = os.stat(f.fileno()).st_mtime + if mtime > self.light_mtime: + self.light_spec, self.light_tbc = parse_colors((f,)) + self.light_mtime = mtime + + @property + def has_dark_theme(self) -> bool: + return self.dark_mtime > -1 + + @property + def has_light_theme(self) -> bool: + return self.light_mtime > -1 + + def patch_opts(self, opts: Options) -> None: + which = glfw_get_system_color_theme() + if which == 'dark' and self.has_dark_theme: + patch_options_with_color_spec(opts, self.dark_spec, self.dark_tbc) + self.applied_theme = 'dark' + elif which == 'light' and self.has_light_theme: + patch_options_with_color_spec(opts, self.light_spec, self.light_tbc) + self.applied_theme = 'light' + + def on_system_color_scheme_change(self, new_value: Literal['light', 'dark']) -> bool: + boss = get_boss() + if new_value == 'dark' and self.has_dark_theme: + boss.patch_colors(self.dark_spec, self.dark_tbc, True) + self.applied_theme = 'dark' + return True + if new_value == 'light' and self.has_light_theme: + boss.patch_colors(self.light_spec, self.light_tbc, True) + self.applied_theme = 'light' + return True + return False + + +theme_colors = ThemeColors() + + +def parse_colors(args: Iterable[Union[str, Iterable[str]]]) -> tuple[ColorsSpec, TransparentBackgroundColors]: colors: dict[str, Optional[Color]] = {} nullable_color_map: dict[str, Optional[int]] = {} transparent_background_colors = () @@ -35,7 +91,7 @@ def parse_colors(args: Iterable[Union[str, Iterable[str]]]) -> tuple[dict[str, O return ans, transparent_background_colors -def patch_options_with_color_spec(opts: Options, spec: dict[str, Optional[int]], transparent_background_colors: tuple[tuple[Color, float], ...]) -> None: +def patch_options_with_color_spec(opts: Options, spec: ColorsSpec, transparent_background_colors: TransparentBackgroundColors) -> None: for k, v in spec.items(): if hasattr(opts, k): diff --git a/kitty/main.py b/kitty/main.py index 7ab2d2bb4..e0e7ec32e 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -14,6 +14,7 @@ from .boss import Boss from .child import set_default_env, set_LANG_in_default_env from .cli import create_opts, parse_args from .cli_stub import CLIOptions +from .colors import theme_colors from .conf.utils import BadLine from .config import cached_values_for from .constants import ( @@ -248,6 +249,8 @@ class AppRunner: def __call__(self, opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = (), talk_fd: int = -1) -> None: set_scale(opts.box_drawing_scale) set_options(opts, is_wayland(), args.debug_rendering, args.debug_font_fallback) + theme_colors.refresh() + theme_colors.patch_opts(opts) try: set_font_family(opts, add_builtin_nerd_font=True) _run_app(opts, args, bad_lines, talk_fd)