mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-08 14:18:26 +02:00
Get automatic theme switching working
This commit is contained in:
@@ -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}"
|
||||
''' # }}}
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
124
kitty/colors.py
124
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
|
||||
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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user