Get automatic theme switching working

This commit is contained in:
Kovid Goyal
2024-11-07 17:31:44 +05:30
parent e485b3b4a3
commit 96c1a5c4d1
7 changed files with 158 additions and 61 deletions

View File

@@ -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}"
''' # }}}

View File

@@ -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()

View File

@@ -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

View File

@@ -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
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
with suppress(FileNotFoundError), open(os.path.join(config_dir, 'light-theme.conf')) as f:
mtime = os.stat(f.fileno()).st_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()

View File

@@ -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)

View File

@@ -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

View File

@@ -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