Have auto color scheme switching also control background image
Fixes #8603
This commit is contained in:
parent
7045632d2e
commit
80bb9404d5
8 changed files with 116 additions and 43 deletions
|
|
@ -116,6 +116,8 @@ Detailed list of changes
|
|||
- **Behavior change**: Now kitty does full grapheme segmentation following the
|
||||
Unicode 16 spec when splitting text into cells (:iss:`8533`)
|
||||
|
||||
- **Behavior change**: The :ref:`automatic color switching functionality <auto_color_scheme>` now also controls background image settings (:iss:`8603`)
|
||||
|
||||
- panel kitten: Allow using :option:`kitty +kitten panel --single-instance` to create multiple panels in one process (:iss:`8549`)
|
||||
|
||||
- launch: Allow creating desktop panels such as those created by the :doc:`panel kitten </kittens/panel>` (:iss:`8549`)
|
||||
|
|
@ -123,7 +125,7 @@ Detailed list of changes
|
|||
- Remote control: Allow modifying desktop panels and showing/hiding OS Windows
|
||||
using the `kitten @ resize-os-window` command (:iss:`8550`)
|
||||
|
||||
- Allow starting kitty with the OS window hidden via :option:`kitty --start-as`\=hidden useful for single instance mode (:iss:`3466`)
|
||||
- Allow starting kitty with the OS window hidden via :option:`kitty --start-as=hidden <kitty --start-as>`, useful for single instance mode (:iss:`3466`)
|
||||
|
||||
- Allow configuring the mouse unhide behavior when using :opt:`mouse_hide_wait` (:pull:`8508`)
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,8 @@ This works by creating three files: :file:`dark-theme.auto.conf`,
|
|||
:file:`light-theme.auto.conf` and :file:`no-preference-theme.auto.conf` in the
|
||||
kitty config directory. When these files exist, kitty queries the OS for its color scheme
|
||||
and uses the appropriate file. Note that the colors in these files override all other
|
||||
colors, even those specified using the :option:`kitty --override` command line flag.
|
||||
colors, and also all background image settings,
|
||||
even those specified using the :option:`kitty --override` command line flag.
|
||||
kitty will also automatically change colors when the OS color scheme changes,
|
||||
for example, during night/day transitions.
|
||||
|
||||
|
|
|
|||
|
|
@ -3025,8 +3025,11 @@ def chosen(ans: None | int | str) -> None:
|
|||
|
||||
self.choose_entry('Choose an OS window to move the tab to', items, chosen)
|
||||
|
||||
def set_background_image(self, path: str | None, os_windows: tuple[int, ...], configured: bool, layout: str | None, png_data: bytes = b'') -> None:
|
||||
set_background_image(path, os_windows, configured, layout, png_data)
|
||||
def set_background_image(
|
||||
self, path: str | None, os_windows: tuple[int, ...], configured: bool, layout: str | None, png_data: bytes = b'',
|
||||
linear_interpolation: bool | None = None, tint: float | None = None, tint_gaps: float | None = None
|
||||
) -> None:
|
||||
set_background_image(path, os_windows, configured, layout, png_data, linear_interpolation, tint, tint_gaps)
|
||||
|
||||
# Can be called with kitty -o "map f1 send_test_notification"
|
||||
def send_test_notification(self) -> None:
|
||||
|
|
@ -3050,6 +3053,19 @@ def show_kitty_env_vars(self) -> None:
|
|||
def close_shared_ssh_connections(self) -> None:
|
||||
cleanup_ssh_control_masters()
|
||||
|
||||
@ac('debug', '''Simulate a change in OS color scheme preference''')
|
||||
def simulate_color_scheme_preference_change(self, which: str) -> None:
|
||||
which = which.lower().replace('-', '_')
|
||||
match which:
|
||||
case 'light':
|
||||
self.on_system_color_scheme_change('light', False)
|
||||
case 'dark':
|
||||
self.on_system_color_scheme_change('dark', False)
|
||||
case 'no_preference':
|
||||
self.on_system_color_scheme_change('no_preference', False)
|
||||
case _:
|
||||
self.show_error(_('Unknown color scheme type'), _('{} is not a valid color scheme type').format(which))
|
||||
|
||||
def launch_urls(self, *urls: str, no_replace_window: bool = False) -> None:
|
||||
from .launch import force_window_launch
|
||||
from .open_actions import actions_for_launch
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
from collections.abc import Iterable, Sequence
|
||||
from contextlib import suppress
|
||||
from enum import Enum
|
||||
from typing import Literal, Optional
|
||||
from typing import Literal, Optional, TypedDict
|
||||
|
||||
from .config import parse_config
|
||||
from .constants import config_dir
|
||||
|
|
@ -20,6 +20,14 @@
|
|||
Colors = tuple[ColorsSpec, TransparentBackgroundColors]
|
||||
|
||||
|
||||
class BackgroundImageOptions(TypedDict, total=False):
|
||||
background_image: str | None
|
||||
background_image_layout: str | None
|
||||
background_image_linear: bool | None
|
||||
background_tint: float | None
|
||||
background_tint_gaps: float | None
|
||||
|
||||
|
||||
class ThemeFile(Enum):
|
||||
dark = 'dark-theme.auto.conf'
|
||||
light = 'light-theme.auto.conf'
|
||||
|
|
@ -33,6 +41,7 @@ class ThemeColors:
|
|||
no_preference_mtime: int = -1
|
||||
applied_theme: Literal['light', 'dark', 'no_preference', ''] = ''
|
||||
default_colors: ColorsSpec | None = None
|
||||
default_background_image_options: BackgroundImageOptions| None = None
|
||||
|
||||
def get_default_colors(self) -> ColorsSpec:
|
||||
if self.default_colors is None:
|
||||
|
|
@ -44,14 +53,19 @@ def get_default_colors(self) -> ColorsSpec:
|
|||
if isinstance(defval, Color):
|
||||
ans[name] = int(defval)
|
||||
self.default_colors = ans
|
||||
self.default_background_image_options: BackgroundImageOptions = {
|
||||
k: getattr(defaults, k) for k in BackgroundImageOptions.__optional_keys__} # type: ignore
|
||||
|
||||
return self.default_colors
|
||||
|
||||
def parse_colors(self, f: Iterable[str]) -> Colors:
|
||||
def parse_colors(self, f: Iterable[str], background_image_options: BackgroundImageOptions | None = None) -> Colors:
|
||||
# When parsing the theme file we first apply the default theme so that
|
||||
# all colors are reset to default values first. This is needed for themes
|
||||
# that don't specify all colors.
|
||||
spec, tbc = parse_colors((f,))
|
||||
dc_spec = self.get_default_colors()
|
||||
if background_image_options is not None and self.default_background_image_options:
|
||||
background_image_options.update(self.default_background_image_options)
|
||||
spec, tbc = parse_colors((f,), background_image_options)
|
||||
ans = dc_spec.copy()
|
||||
ans.update(spec)
|
||||
return ans, tbc
|
||||
|
|
@ -64,21 +78,27 @@ def refresh(self) -> bool:
|
|||
mtime = x.stat().st_mtime_ns
|
||||
if mtime > self.dark_mtime:
|
||||
with open(x.path) as f:
|
||||
self.dark_spec, self.dark_tbc = self.parse_colors(f)
|
||||
d: BackgroundImageOptions = {}
|
||||
self.dark_spec, self.dark_tbc = self.parse_colors(f, d)
|
||||
self.dark_background_image_options = d
|
||||
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 = self.parse_colors(f)
|
||||
d = {}
|
||||
self.light_spec, self.light_tbc = self.parse_colors(f, d)
|
||||
self.light_background_image_options = d
|
||||
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 = self.parse_colors(f)
|
||||
d = {}
|
||||
self.no_preference_spec, self.no_preference_tbc = self.parse_colors(f, d)
|
||||
self.no_preference_background_image_options = d
|
||||
self.no_preference_mtime = mtime
|
||||
found = True
|
||||
return found
|
||||
|
|
@ -115,14 +135,18 @@ def patch_opts(self, opts: Options, debug_rendering: bool = False) -> None:
|
|||
if debug_rendering:
|
||||
log_error('Current system color scheme:', which)
|
||||
cols: Colors | None = None
|
||||
bgo: BackgroundImageOptions | None = None
|
||||
if which == 'dark' and self.has_dark_theme:
|
||||
cols = self.dark_spec, self.dark_tbc
|
||||
bgo = self.dark_background_image_options
|
||||
elif which == 'light' and self.has_light_theme:
|
||||
cols = self.light_spec, self.light_tbc
|
||||
bgo = self.light_background_image_options
|
||||
elif which == 'no_preference' and self.has_no_preference_theme:
|
||||
cols = self.no_preference_spec, self.no_preference_tbc
|
||||
bgo = self.no_preference_background_image_options
|
||||
if cols is not None:
|
||||
patch_options_with_color_spec(opts, *cols)
|
||||
patch_options_with_color_spec(opts, *cols, background_image_options=bgo)
|
||||
patch_global_colors(cols[0], True)
|
||||
self.applied_theme = which
|
||||
if debug_rendering:
|
||||
|
|
@ -138,19 +162,23 @@ def apply_theme(self, new_value: ColorSchemes, notify_on_bg_change: bool = True)
|
|||
from .utils import log_error
|
||||
boss = get_boss()
|
||||
if new_value == 'dark' and self.has_dark_theme:
|
||||
patch_colors(self.dark_spec, self.dark_tbc, True, notify_on_bg_change=notify_on_bg_change)
|
||||
patch_colors(
|
||||
self.dark_spec, self.dark_tbc, True, notify_on_bg_change=notify_on_bg_change, background_image_options=self.dark_background_image_options)
|
||||
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:
|
||||
patch_colors(self.light_spec, self.light_tbc, True, notify_on_bg_change=notify_on_bg_change)
|
||||
patch_colors(
|
||||
self.light_spec, self.light_tbc, True, notify_on_bg_change=notify_on_bg_change, background_image_options=self.light_background_image_options)
|
||||
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, notify_on_bg_change=notify_on_bg_change)
|
||||
patch_colors(
|
||||
self.no_preference_spec, self.no_preference_tbc, True, notify_on_bg_change=notify_on_bg_change,
|
||||
background_image_options=self.no_preference_background_image_options)
|
||||
self.applied_theme = new_value
|
||||
if boss.args.debug_rendering:
|
||||
log_error(f'Applied color theme {new_value}')
|
||||
|
|
@ -161,7 +189,7 @@ def apply_theme(self, new_value: ColorSchemes, notify_on_bg_change: bool = True)
|
|||
theme_colors = ThemeColors()
|
||||
|
||||
|
||||
def parse_colors(args: Iterable[str | Iterable[str]]) -> Colors:
|
||||
def parse_colors(args: Iterable[str | Iterable[str]], background_image_options: BackgroundImageOptions | None = None) -> Colors:
|
||||
colors: dict[str, Color | None] = {}
|
||||
nullable_color_map: dict[str, int | None] = {}
|
||||
transparent_background_colors = ()
|
||||
|
|
@ -175,6 +203,10 @@ def parse_colors(args: Iterable[str | Iterable[str]]) -> Colors:
|
|||
else:
|
||||
conf = parse_config(spec)
|
||||
transparent_background_colors = conf.pop('transparent_background_colors', ())
|
||||
if background_image_options is not None:
|
||||
for key in BackgroundImageOptions.__optional_keys__:
|
||||
if key in conf:
|
||||
background_image_options.__setitem__(key, conf[key])
|
||||
colors.update(conf)
|
||||
for k in nullable_colors:
|
||||
q = colors.pop(k, False)
|
||||
|
|
@ -186,7 +218,10 @@ def parse_colors(args: Iterable[str | Iterable[str]]) -> Colors:
|
|||
return ans, transparent_background_colors
|
||||
|
||||
|
||||
def patch_options_with_color_spec(opts: Options, spec: ColorsSpec, transparent_background_colors: TransparentBackgroundColors) -> None:
|
||||
def patch_options_with_color_spec(
|
||||
opts: Options, spec: ColorsSpec, transparent_background_colors: TransparentBackgroundColors,
|
||||
background_image_options: BackgroundImageOptions | None = None
|
||||
) -> None:
|
||||
for k, v in spec.items():
|
||||
if hasattr(opts, k):
|
||||
if v is None:
|
||||
|
|
@ -195,11 +230,16 @@ 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
|
||||
if background_image_options is not None:
|
||||
for k, bv in background_image_options.items():
|
||||
if hasattr(opts, k):
|
||||
setattr(opts, k, bv)
|
||||
|
||||
|
||||
def patch_colors(
|
||||
spec: ColorsSpec, transparent_background_colors: TransparentBackgroundColors, configured: bool = False,
|
||||
windows: Sequence[WindowType] | None = None, notify_on_bg_change: bool = True,
|
||||
background_image_options: BackgroundImageOptions | None = None
|
||||
) -> None:
|
||||
boss = get_boss()
|
||||
if windows is None:
|
||||
|
|
@ -209,7 +249,8 @@ def patch_colors(
|
|||
patch_color_profiles(spec, transparent_background_colors, profiles, configured)
|
||||
opts = get_options()
|
||||
if configured:
|
||||
patch_options_with_color_spec(opts, spec, transparent_background_colors)
|
||||
patch_options_with_color_spec(opts, spec, transparent_background_colors, background_image_options)
|
||||
os_window_ids = set()
|
||||
for tm in get_boss().all_tab_managers:
|
||||
tm.tab_bar.patch_colors(spec)
|
||||
tm.tab_bar.layout()
|
||||
|
|
@ -218,10 +259,17 @@ def patch_colors(
|
|||
if t is not None:
|
||||
t.relayout_borders()
|
||||
set_os_window_chrome(tm.os_window_id)
|
||||
os_window_ids.add(tm.os_window_id)
|
||||
patch_global_colors(spec, configured)
|
||||
default_bg_changed = 'background' in spec
|
||||
notify_bg = notify_on_bg_change and default_bg_changed
|
||||
boss = get_boss()
|
||||
if background_image_options is not None:
|
||||
boss.set_background_image(
|
||||
background_image_options.get('background_image'), tuple(os_window_ids), configured,
|
||||
layout=background_image_options.get('background_image_layout'),
|
||||
linear_interpolation=background_image_options.get('background_image_linear'), tint=background_image_options.get('background_tint'),
|
||||
tint_gaps=background_image_options.get('background_tint_gaps'))
|
||||
for w in windows:
|
||||
if w:
|
||||
if notify_bg and w.screen.color_profile.default_bg != bg_colors_before.get(w.id):
|
||||
|
|
|
|||
|
|
@ -269,9 +269,9 @@ def __init__(self) -> None:
|
|||
self.initial_window_size_func = initial_window_size_func
|
||||
|
||||
def __call__(self, opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = (), talk_fd: int = -1) -> None:
|
||||
set_options(opts, is_wayland(), args.debug_rendering, args.debug_font_fallback)
|
||||
if theme_colors.refresh():
|
||||
theme_colors.patch_opts(opts, args.debug_rendering)
|
||||
set_options(opts, is_wayland(), args.debug_rendering, args.debug_font_fallback)
|
||||
try:
|
||||
set_font_family(opts, add_builtin_nerd_font=True)
|
||||
_run_app(opts, args, bad_lines, talk_fd)
|
||||
|
|
|
|||
|
|
@ -1597,27 +1597,6 @@
|
|||
on macOS and KDE.
|
||||
''')
|
||||
|
||||
opt('background_image', 'none',
|
||||
option_type='config_or_absolute_path', ctype='!background_image',
|
||||
long_text='Path to a background image. Must be in PNG/JPEG/WEBP/TIFF/GIF/BMP format.'
|
||||
)
|
||||
|
||||
opt('background_image_layout', 'tiled',
|
||||
choices=('mirror-tiled', 'scaled', 'tiled', 'clamped', 'centered', 'cscaled'),
|
||||
ctype='bglayout',
|
||||
long_text='''
|
||||
Whether to tile, scale or clamp the background image. The value can be one of
|
||||
:code:`tiled`, :code:`mirror-tiled`, :code:`scaled`, :code:`clamped`, :code:`centered`
|
||||
or :code:`cscaled`. The :code:`scaled` and :code:`cscaled` values scale the image to the
|
||||
window size, with :code:`cscaled` preserving the image aspect ratio.
|
||||
'''
|
||||
)
|
||||
|
||||
opt('background_image_linear', 'no',
|
||||
option_type='to_bool', ctype='bool',
|
||||
long_text='When background image is scaled, whether linear interpolation should be used.'
|
||||
)
|
||||
|
||||
opt('transparent_background_colors', '', option_type='transparent_background_colors', long_text='''
|
||||
A space separated list of upto 7 colors, with opacity. When the background color of a cell matches one of these colors,
|
||||
it is rendered semi-transparent using the specified opacity.
|
||||
|
|
@ -1645,6 +1624,31 @@
|
|||
'''
|
||||
)
|
||||
|
||||
|
||||
opt('background_image', 'none',
|
||||
option_type='config_or_absolute_path', ctype='!background_image',
|
||||
long_text='Path to a background image. Must be in PNG/JPEG/WEBP/TIFF/GIF/BMP format.'
|
||||
' Note that when using :ref:`auto_color_scheme` this option is overridden by the color scheme file and must be set inside it to take effect.'
|
||||
)
|
||||
|
||||
opt('background_image_layout', 'tiled',
|
||||
choices=('mirror-tiled', 'scaled', 'tiled', 'clamped', 'centered', 'cscaled'),
|
||||
ctype='bglayout',
|
||||
long_text='''
|
||||
Whether to tile, scale or clamp the background image. The value can be one of
|
||||
:code:`tiled`, :code:`mirror-tiled`, :code:`scaled`, :code:`clamped`, :code:`centered`
|
||||
or :code:`cscaled`. The :code:`scaled` and :code:`cscaled` values scale the image to the
|
||||
window size, with :code:`cscaled` preserving the image aspect ratio.
|
||||
Note that when using :ref:`auto_color_scheme` this option is overridden by the color scheme file and must be set inside it to take effect.
|
||||
'''
|
||||
)
|
||||
|
||||
opt('background_image_linear', 'no',
|
||||
option_type='to_bool', ctype='bool',
|
||||
long_text='When background image is scaled, whether linear interpolation should be used.'
|
||||
' Note that when using :ref:`auto_color_scheme` this option is overridden by the color scheme file and must be set inside it to take effect.'
|
||||
)
|
||||
|
||||
opt('background_tint', '0.0',
|
||||
option_type='unit_float', ctype='float',
|
||||
long_text='''
|
||||
|
|
@ -1652,6 +1656,7 @@
|
|||
makes it easier to read the text. Tinting is done using the current background
|
||||
color for each window. This option applies only if :opt:`background_opacity` is
|
||||
set and transparent windows are supported or :opt:`background_image` is set.
|
||||
Note that when using :ref:`auto_color_scheme` this option is overridden by the color scheme file and must be set inside it to take effect.
|
||||
'''
|
||||
)
|
||||
|
||||
|
|
@ -1662,6 +1667,7 @@
|
|||
color, after applying :opt:`background_tint`. Since this is multiplicative
|
||||
with :opt:`background_tint`, it can be used to lighten the tint over the window
|
||||
gaps for a *separated* look.
|
||||
Note that when using :ref:`auto_color_scheme` this option is overridden by the color scheme file and must be set inside it to take effect.
|
||||
'''
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ def detach_tab_parse(func: str, rest: str) -> FuncArgsType:
|
|||
|
||||
@func_with_args(
|
||||
'set_background_opacity', 'goto_layout', 'toggle_layout', 'toggle_tab', 'kitty_shell', 'show_kitty_doc',
|
||||
'set_tab_title', 'push_keyboard_mode', 'dump_lines_with_attrs', 'set_window_title',
|
||||
'set_tab_title', 'push_keyboard_mode', 'dump_lines_with_attrs', 'set_window_title', 'simulate_color_scheme_preference_change',
|
||||
)
|
||||
def simple_parse(func: str, rest: str) -> FuncArgsType:
|
||||
return func, (rest,)
|
||||
|
|
|
|||
|
|
@ -1248,9 +1248,9 @@ pyset_background_image(PyObject *self UNUSED, PyObject *args, PyObject *kw) {
|
|||
global_state.bgimage = bgimage;
|
||||
if (bgimage) bgimage->refcnt++;
|
||||
OPT(background_image_layout) = layout;
|
||||
if (pylinear) convert_from_python_background_image_linear(pylinear, &global_state.opts);
|
||||
if (pytint) convert_from_python_background_tint(pytint, &global_state.opts);
|
||||
if (pytint_gaps) convert_from_python_background_tint_gaps(pytint_gaps, &global_state.opts);
|
||||
if (pylinear && pylinear != Py_None) convert_from_python_background_image_linear(pylinear, &global_state.opts);
|
||||
if (pytint && pytint != Py_None) convert_from_python_background_tint(pytint, &global_state.opts);
|
||||
if (pytint_gaps && pytint_gaps != Py_None) convert_from_python_background_tint_gaps(pytint_gaps, &global_state.opts);
|
||||
}
|
||||
for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(os_window_ids); i++) {
|
||||
id_type os_window_id = PyLong_AsUnsignedLongLong(PyTuple_GET_ITEM(os_window_ids, i));
|
||||
|
|
|
|||
Loading…
Reference in a new issue