Code to allow users to create dark and light themes that are automatically applied when the OS color scheme changes

This commit is contained in:
Kovid Goyal 2024-11-07 13:52:49 +05:30
parent 36074eabc8
commit e485b3b4a3
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
3 changed files with 67 additions and 10 deletions

View file

@ -36,6 +36,7 @@
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 @@ def disable_ligatures_in(self, where: Union[str, Iterable[Window]], strategy: in
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 @@ def sanitize_url_for_dispay_to_user(self, url: str) -> str:
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

View file

@ -2,15 +2,71 @@
# License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>
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):

View file

@ -14,6 +14,7 @@
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 @@ def __init__(self) -> None:
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)