diff --git a/gen/go_code.py b/gen/go_code.py index 5610811c1..912a66c52 100755 --- a/gen/go_code.py +++ b/gen/go_code.py @@ -583,6 +583,7 @@ def load_ref_map() -> dict[str, dict[str, str]]: def generate_constants() -> str: from kittens.hints.main import DEFAULT_REGEX from kittens.query_terminal.main import all_queries + from kitty.colors import ThemeFile from kitty.config import option_names_for_completion from kitty.fast_data_types import FILE_TRANSFER_CODE from kitty.options.utils import allowed_shell_integration_values, url_style_map @@ -638,6 +639,9 @@ Select_by_word_characters: `{Options.select_by_word_characters}`, Wheel_scroll_m Shell: "{Options.shell}", Url_excluded_characters: "{Options.url_excluded_characters}", }} const OptionNames = {option_names} +const DarkThemeFileName = "{ThemeFile.dark.value}" +const LightThemeFileName = "{ThemeFile.light.value}" +const NoPreferenceThemeFileName = "{ThemeFile.no_preference.value}" ''' # }}} diff --git a/kittens/themes/ui.go b/kittens/themes/ui.go index 89e2d463b..c98385765 100644 --- a/kittens/themes/ui.go +++ b/kittens/themes/ui.go @@ -5,6 +5,7 @@ package themes import ( "fmt" "io" + "kitty" "maps" "path/filepath" "regexp" @@ -521,6 +522,24 @@ func (self *handler) on_accepting_key_event(ev *loop.KeyEvent) error { self.lp.Quit(0) return nil } + + scheme := func(name string) error { + ev.Handled = true + self.themes_list.CurrentTheme().SaveInFile(utils.ConfigDir(), name) + self.update_recent() + self.lp.Quit(0) + return nil + + } + if ev.MatchesCaseInsensitiveTextOrKey("d") || ev.MatchesPressOrRepeat("shift+d") { + return scheme(kitty.DarkThemeFileName) + } + if ev.MatchesCaseInsensitiveTextOrKey("l") || ev.MatchesPressOrRepeat("shift+l") { + return scheme(kitty.LightThemeFileName) + } + if ev.MatchesCaseInsensitiveTextOrKey("n") || ev.MatchesPressOrRepeat("shift+n") { + return scheme(kitty.NoPreferenceThemeFileName) + } return nil } @@ -558,6 +577,15 @@ func (self *handler) draw_accepting_screen() { self.lp.Printf(` %slace the theme file in %s but do not modify %s`, ac("P"), utils.ConfigDir(), kc) self.lp.Println() self.lp.Println() + self.lp.Printf(` Save as colors to use when the OS switches to:`) + self.lp.Println() + self.lp.Printf(` %sark mode`, ac("D")) + self.lp.Println() + self.lp.Printf(` %sight mode`, ac("L")) + self.lp.Println() + self.lp.Printf(` %so preference mode`, ac("N")) + self.lp.Println() + self.lp.Println() self.lp.Printf(` %sbort and return to list of themes`, ac("A")) self.lp.Println() self.lp.Println() diff --git a/kitty/boss.py b/kitty/boss.py index a70c9314e..217d394bb 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -19,7 +19,6 @@ from typing import ( TYPE_CHECKING, Any, Callable, - Literal, Optional, Union, ) @@ -36,7 +35,7 @@ from .clipboard import ( set_clipboard_string, set_primary_selection, ) -from .colors import ColorsSpec, TransparentBackgroundColors, patch_options_with_color_spec, theme_colors +from .colors import ColorSchemes, 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 ( @@ -93,7 +92,6 @@ from .fast_data_types import ( monotonic, os_window_focus_counters, os_window_font_size, - patch_global_colors, redirect_mouse_handling, ring_bell, run_with_activation_token, @@ -2638,20 +2636,6 @@ class Boss: window.screen.disable_ligatures = strategy window.refresh() - def patch_colors(self, spec: ColorsSpec, transparent_background_colors: TransparentBackgroundColors, configured: bool = False) -> None: - opts = get_options() - if configured: - patch_options_with_color_spec(opts, spec, transparent_background_colors) - for tm in self.all_tab_managers: - tm.tab_bar.patch_colors(spec) - tm.tab_bar.layout() - tm.mark_tab_bar_dirty() - t = tm.active_tab - if t is not None: - t.relayout_borders() - set_os_window_chrome(tm.os_window_id) - patch_global_colors(spec, configured) - def apply_new_options(self, opts: Options) -> None: from .fonts.box_drawing import set_scale # Update options storage @@ -3062,9 +3046,8 @@ class Boss: 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 not is_initial_value and appearance != 'no_preference': - theme_colors.on_system_color_scheme_change(appearance) + def on_system_color_scheme_change(self, appearance: ColorSchemes, is_initial_value: bool) -> None: + theme_colors.on_system_color_scheme_change(appearance, is_initial_value) @ac('win', ''' Toggle to the tab matching the specified expression diff --git a/kitty/colors.py b/kitty/colors.py index 8a989bf4d..931b05a35 100644 --- a/kitty/colors.py +++ b/kitty/colors.py @@ -3,35 +3,60 @@ import os from contextlib import suppress -from typing import Iterable, Literal, Optional, Union +from enum import Enum +from typing import Iterable, Literal, Optional, Sequence, Union from .config import parse_config from .constants import config_dir -from .fast_data_types import Color, get_boss, glfw_get_system_color_theme +from .fast_data_types import Color, get_boss, get_options, glfw_get_system_color_theme, patch_color_profiles, patch_global_colors, set_os_window_chrome from .options.types import Options, nullable_colors from .rgb import color_from_int +from .typing import WindowType ColorsSpec = dict[str, Optional[int]] TransparentBackgroundColors = tuple[tuple[Color, float], ...] +ColorSchemes = Literal['light', 'dark', 'no_preference'] + + +class ThemeFile(Enum): + dark: str = 'dark-theme.auto.conf' + light: str = 'light-theme.auto.conf' + no_preference: str = 'no-preference-theme.auto.conf' class ThemeColors: - dark_mtime: float = -1 - light_mtime: float = -1 - applied_theme: Literal['light', 'dark', ''] = '' + dark_mtime: int = -1 + light_mtime: int = -1 + no_preference_mtime: int = -1 + applied_theme: Literal['light', 'dark', 'no_preference', ''] = '' - 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 + def refresh(self) -> bool: + found = False + with suppress(FileNotFoundError): + for x in os.scandir(config_dir): + if x.name == ThemeFile.dark.value: + mtime = x.stat().st_mtime_ns + if mtime > self.dark_mtime: + with open(x.path) as f: + self.dark_spec, self.dark_tbc = parse_colors((f,)) + self.dark_mtime = mtime + found = True + elif x.name == ThemeFile.light.value: + mtime = x.stat().st_mtime_ns + if mtime > self.light_mtime: + with open(x.path) as f: + self.light_spec, self.light_tbc = parse_colors((f,)) + self.light_mtime = mtime + found = True + elif x.name == ThemeFile.no_preference.value: + mtime = x.stat().st_mtime_ns + if mtime > self.no_preference_mtime: + with open(x.path) as f: + self.no_preference_spec, self.no_preference_tbc = parse_colors((f,)) + self.no_preference_mtime = mtime + found = True + return found @property def has_dark_theme(self) -> bool: @@ -41,24 +66,56 @@ class ThemeColors: def has_light_theme(self) -> bool: return self.light_mtime > -1 - def patch_opts(self, opts: Options) -> None: + @property + def has_no_preference_theme(self) -> bool: + return self.no_preference_mtime > -1 + + def patch_opts(self, opts: Options, debug_rendering: bool = False) -> None: + from .utils import log_error + if debug_rendering: + log_error('Querying system for current color scheme') which = glfw_get_system_color_theme() + if debug_rendering: + log_error('Current system color scheme:', which) if which == 'dark' and self.has_dark_theme: patch_options_with_color_spec(opts, self.dark_spec, self.dark_tbc) - self.applied_theme = 'dark' + if debug_rendering: + log_error(f'Applied {which} color theme') + self.applied_theme = which elif which == 'light' and self.has_light_theme: patch_options_with_color_spec(opts, self.light_spec, self.light_tbc) - self.applied_theme = 'light' + if debug_rendering: + log_error(f'Applied {which} color theme') + self.applied_theme = which + elif which == 'no_preference' and self.has_no_preference_theme: + patch_options_with_color_spec(opts, self.no_preference_spec, self.no_preference_tbc) + if debug_rendering: + log_error(f'Applied {which} color theme') + self.applied_theme = which - def on_system_color_scheme_change(self, new_value: Literal['light', 'dark']) -> bool: + def on_system_color_scheme_change(self, new_value: ColorSchemes, is_initial_value: bool = False) -> bool: + if is_initial_value: + return False + from .utils import log_error + self.refresh() 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' + patch_colors(self.dark_spec, self.dark_tbc, True) + self.applied_theme = new_value + if boss.args.debug_rendering: + log_error(f'Applied color theme {new_value}') 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' + patch_colors(self.light_spec, self.light_tbc, True) + self.applied_theme = new_value + if boss.args.debug_rendering: + log_error(f'Applied color theme {new_value}') + return True + if new_value == 'no_preference' and self.has_no_preference_theme: + patch_colors(self.no_preference_spec, self.no_preference_tbc, True) + self.applied_theme = new_value + if boss.args.debug_rendering: + log_error(f'Applied color theme {new_value}') return True return False @@ -92,7 +149,6 @@ def parse_colors(args: Iterable[Union[str, Iterable[str]]]) -> tuple[ColorsSpec, 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): if v is None: @@ -101,3 +157,33 @@ def patch_options_with_color_spec(opts: Options, spec: ColorsSpec, transparent_b else: setattr(opts, k, color_from_int(v)) opts.transparent_background_colors = transparent_background_colors + + +def patch_colors( + spec: ColorsSpec, transparent_background_colors: TransparentBackgroundColors, configured: bool = False, + windows: Optional[Sequence[WindowType]] = None +) -> None: + boss = get_boss() + if windows is None: + windows = tuple(boss.all_windows) + profiles = tuple(w.screen.color_profile for w in windows if w) + patch_color_profiles(spec, transparent_background_colors, profiles, configured) + opts = get_options() + if configured: + patch_options_with_color_spec(opts, spec, transparent_background_colors) + for tm in get_boss().all_tab_managers: + tm.tab_bar.patch_colors(spec) + tm.tab_bar.layout() + tm.mark_tab_bar_dirty() + t = tm.active_tab + if t is not None: + t.relayout_borders() + set_os_window_chrome(tm.os_window_id) + patch_global_colors(spec, configured) + default_bg_changed = 'background' in spec + boss = get_boss() + for w in windows: + if w: + if default_bg_changed: + boss.default_bg_changed_for(w.id) + w.refresh() diff --git a/kitty/main.py b/kitty/main.py index e0e7ec32e..68c561eb2 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -249,8 +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) + if theme_colors.refresh(): + theme_colors.patch_opts(opts, args.debug_rendering) try: set_font_family(opts, add_builtin_nerd_font=True) _run_app(opts, args, bad_lines, talk_fd) diff --git a/kitty/rc/set_colors.py b/kitty/rc/set_colors.py index df2e770ca..93816a6b5 100644 --- a/kitty/rc/set_colors.py +++ b/kitty/rc/set_colors.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Dict, Optional from kitty.cli import emph -from kitty.fast_data_types import Color, patch_color_profiles +from kitty.fast_data_types import Color from .base import ( MATCH_TAB_OPTION, @@ -87,25 +87,18 @@ this option, any color arguments are ignored and :option:`kitten @ set-colors -- return ans def response_from_kitty(self, boss: Boss, window: Optional[Window], payload_get: PayloadGetType) -> ResponseType: + from kitty.colors import patch_colors windows = self.windows_for_payload(boss, window, payload_get) colors: Dict[str, int | None] = payload_get('colors') tbc = colors.get('transparent_background_colors') if payload_get('reset'): colors = {k: None if v is None else int(v) for k, v in boss.color_settings_at_startup.items()} - profiles = tuple(w.screen.color_profile for w in windows if w) if tbc: from kitty.options.utils import transparent_background_colors parsed_tbc = transparent_background_colors(str(tbc)) else: parsed_tbc = () - patch_color_profiles(colors, parsed_tbc, profiles, payload_get('configured')) - boss.patch_colors(colors, parsed_tbc, payload_get('configured')) - default_bg_changed = 'background' in colors - for w in windows: - if w: - if default_bg_changed: - boss.default_bg_changed_for(w.id) - w.refresh() + patch_colors(colors, parsed_tbc, bool(payload_get('configured')), windows=windows) return None diff --git a/tools/themes/collection.go b/tools/themes/collection.go index 587acd23e..891c65df3 100644 --- a/tools/themes/collection.go +++ b/tools/themes/collection.go @@ -586,15 +586,18 @@ func (self *Theme) SaveInDir(dirpath string) (err error) { return utils.AtomicUpdateFile(path, bytes.NewReader(utils.UnsafeStringToBytes(code)), 0o644) } -func (self *Theme) SaveInConf(config_dir, reload_in, config_file_name string) (err error) { +func (self *Theme) SaveInFile(config_dir, config_file_name string) (err error) { _ = os.MkdirAll(config_dir, 0o755) - path := filepath.Join(config_dir, `current-theme.conf`) + path := filepath.Join(config_dir, config_file_name) code, err := self.Code() if err != nil { return err } - err = utils.AtomicUpdateFile(path, bytes.NewReader(utils.UnsafeStringToBytes(code)), 0o644) - if err != nil { + return utils.AtomicUpdateFile(path, bytes.NewReader(utils.UnsafeStringToBytes(code)), 0o644) +} + +func (self *Theme) SaveInConf(config_dir, reload_in, config_file_name string) (err error) { + if err = self.SaveInFile(config_dir, `current-theme.conf`); err != nil { return err } confpath := config_file_name