diff --git a/docs/changelog.rst b/docs/changelog.rst index 9a1e2a659..f3d1884a4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -18,6 +18,11 @@ To update |kitty|, :doc:`follow the instructions `. - Allow the user to supply a custom Python function to draw tab bar. See :opt:`tab_bar_style` +- **Backward incompatibility**: The command line option ``--watcher`` has been + removed in favor of the :opt:`watcher` option in :file:`kitty.conf`. It can be set + from the command line as: ``kitty -o watcher=/path/to/watcher``. It has the + advantage of applying to all windows, not just the initially created ones. + - Add support for reporting mouse events with pixel co-ordinates using the ``SGR_PIXEL_PROTOCOL`` introduced in xterm 359 diff --git a/docs/launch.rst b/docs/launch.rst index b58fa375e..27a2498e6 100644 --- a/docs/launch.rst +++ b/docs/launch.rst @@ -89,6 +89,8 @@ For example:: map f1 launch my-program @active-kitty-window-id +.. _watchers: + Watching launched windows --------------------------- diff --git a/kitty/cli.py b/kitty/cli.py index c48bf1caf..03a29d230 100644 --- a/kitty/cli.py +++ b/kitty/cli.py @@ -571,15 +571,6 @@ def parse_cmdline(oc: Options, disabled: OptionSpecSeq, ans: Any, args: Optional return leftover_args -WATCHER_DEFINITION = ''' ---watcher -w -type=list -Path to a python file. Appropriately named functions in this file will be called -for various events, such as when the window is resized, focused or closed. See the section -on watchers in the launch command documentation :doc:`launch`. Relative paths are -resolved relative to the kitty config directory.''' - - def options_spec() -> str: if not hasattr(options_spec, 'ans'): OPTIONS = ''' @@ -627,11 +618,6 @@ Path to a file containing the startup :italic:`session` (tabs, windows, layout, Use - to read from STDIN. See the README file for details and an example. -{watcher} -Note that this watcher will be added only to all initially created windows, not new windows -created after startup. - - --hold type=bool-set Remain open after child process exits. Note that this only affects the first @@ -731,7 +717,6 @@ type=bool-set ''' setattr(options_spec, 'ans', OPTIONS.format( appname=appname, config_help=CONFIG_HELP.format(appname=appname, conf_name=appname), - watcher=WATCHER_DEFINITION )) ans: str = getattr(options_spec, 'ans') return ans diff --git a/kitty/conf/types.py b/kitty/conf/types.py index c0acee516..93825e810 100644 --- a/kitty/conf/types.py +++ b/kitty/conf/types.py @@ -44,6 +44,7 @@ def expand_opt_references(conf_name: str, text: str) -> str: def remove_markup(text: str) -> str: ref_map = { 'layouts': f'{website_url("overview")}#layouts', + 'watchers': f'{website_url("launch")}#watchers', 'sessions': f'{website_url("overview")}#startup-sessions', 'functional': f'{website_url("keyboard-protocol")}#functional-key-definitions', 'action-select_tab': f'{website_url("actions")}#select-tab', diff --git a/kitty/launch.py b/kitty/launch.py index e22c5e4f9..48173e27c 100644 --- a/kitty/launch.py +++ b/kitty/launch.py @@ -3,21 +3,21 @@ # License: GPLv3 Copyright: 2019, Kovid Goyal -from typing import Any, Dict, List, NamedTuple, Optional, Sequence +from typing import Any, Dict, Iterable, List, NamedTuple, Optional, Sequence from .boss import Boss from .child import Child -from .cli import WATCHER_DEFINITION, parse_args +from .cli import parse_args from .cli_stub import LaunchCLIOptions from .constants import resolve_custom_file from .fast_data_types import ( get_options, patch_color_profiles, set_clipboard_string ) +from .options.utils import env as parse_env from .tabs import Tab from .types import run_once -from .utils import find_exe, read_shell_environment, set_primary_selection +from .utils import find_exe, read_shell_environment, set_primary_selection, log_error from .window import Watchers, Window -from .options.utils import env as parse_env try: from typing import TypedDict @@ -178,7 +178,14 @@ file with the same syntax as kitty.conf to read the colors from, or specify them individually, for example: ``--color background=white`` ``--color foreground=red`` -''' + WATCHER_DEFINITION +--watcher -w +type=list +Path to a python file. Appropriately named functions in this file will be called +for various events, such as when the window is resized, focused or closed. See the section +on watchers in the launch command documentation: :ref:`watchers`. Relative paths are +resolved relative to the kitty config directory. Global watchers for all windows can be +specified with :opt:`watcher`. +''' def parse_launch_args(args: Optional[Sequence[str]] = None) -> LaunchSpec: @@ -221,14 +228,29 @@ def tab_for_window(boss: Boss, opts: LaunchCLIOptions, target_tab: Optional[Tab] return tab -def load_watch_modules(watchers: Sequence[str]) -> Optional[Watchers]: +watcher_modules: Dict[str, Any] = {} + + +def load_watch_modules(watchers: Iterable[str]) -> Optional[Watchers]: if not watchers: return None import runpy ans = Watchers() for path in watchers: path = resolve_custom_file(path) - m = runpy.run_path(path, run_name='__kitty_watcher__') + m = watcher_modules.get(path, None) + if m is None: + try: + m = runpy.run_path(path, run_name='__kitty_watcher__') + except Exception as err: + import traceback + log_error(traceback.format_exc()) + log_error(f'Failed to load watcher from {path} with error: {err}') + watcher_modules[path] = False + continue + watcher_modules[path] = m + if m is False: + continue w = m.get('on_close') if callable(w): ans.on_close.append(w) diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 07012325d..75c2a198f 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -2532,6 +2532,18 @@ will delete the variable from the child process' environment. ''' ) +opt('+watcher', '', + option_type='watcher', + add_to_default=False, + long_text=''' +Path to python file which will be loaded for :ref:`watchers`. +Can be specified more than once to load multiple watchers. +The watchers will be added to every kitty window. Relative +paths are resolved relative to the kitty config directory. +Note that reloading the config will only affect windows +created after the reload. +''') + opt('update_check_interval', '24', option_type='float', long_text=''' diff --git a/kitty/options/parse.py b/kitty/options/parse.py index 0941f8033..83484fbbd 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -16,7 +16,7 @@ from kitty.options.utils import ( parse_mouse_map, resize_draw_strategy, scrollback_lines, scrollback_pager_history_size, symbol_map, tab_activity_symbol, tab_bar_edge, tab_bar_margin_height, tab_bar_min_tabs, tab_fade, tab_font_style, tab_separator, tab_title_template, to_cursor_shape, to_font_size, to_layout_names, - to_modifiers, url_prefixes, url_style, window_border_width, window_size + to_modifiers, url_prefixes, url_style, watcher, window_border_width, window_size ) @@ -1241,6 +1241,10 @@ class Parser: def visual_bell_duration(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: ans['visual_bell_duration'] = positive_float(val) + def watcher(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: + for k, v in watcher(val, ans["watcher"]): + ans["watcher"][k] = v + def wayland_titlebar_color(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: ans['wayland_titlebar_color'] = macos_titlebar_color(val) @@ -1292,6 +1296,7 @@ def create_result_dict() -> typing.Dict[str, typing.Any]: 'font_features': {}, 'kitten_alias': {}, 'symbol_map': {}, + 'watcher': {}, 'map': [], 'mouse_map': [], } diff --git a/kitty/options/types.py b/kitty/options/types.py index f6ac7d0fb..123580c2d 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -429,6 +429,7 @@ option_names = ( # {{{ 'url_prefixes', 'url_style', 'visual_bell_duration', + 'watcher', 'wayland_titlebar_color', 'wheel_scroll_multiplier', 'window_alert_on_bell', @@ -577,6 +578,7 @@ class Options: font_features: typing.Dict[str, typing.Tuple[kitty.fonts.FontFeature, ...]] = {} kitten_alias: typing.Dict[str, typing.List[str]] = {} symbol_map: typing.Dict[typing.Tuple[int, int], str] = {} + watcher: typing.Dict[str, str] = {} map: typing.List[kitty.options.utils.KeyDefinition] = [] keymap: KeyMap = {} sequence_map: SequenceMap = {} @@ -691,6 +693,7 @@ defaults.env = {} defaults.font_features = {} defaults.kitten_alias = {} defaults.symbol_map = {} +defaults.watcher = {} defaults.map = [ # copy_to_clipboard KeyDefinition(False, KeyAction('copy_to_clipboard'), 1024, False, 99, ()), diff --git a/kitty/options/utils.py b/kitty/options/utils.py index 76911c75f..885801040 100644 --- a/kitty/options/utils.py +++ b/kitty/options/utils.py @@ -7,8 +7,8 @@ import os import re import sys from typing import ( - Any, Callable, Dict, Iterable, List, NamedTuple, Optional, Sequence, Tuple, - Union + Any, Callable, Container, Dict, Iterable, List, NamedTuple, Optional, + Sequence, Tuple, Union ) import kitty.fast_data_types as defines @@ -745,6 +745,12 @@ def env(val: str, current_val: Dict[str, str]) -> Iterable[Tuple[str, str]]: yield val, DELETE_ENV_VAR +def watcher(val: str, current_val: Container[str]) -> Iterable[Tuple[str, str]]: + val = val.strip() + if val not in current_val: + yield val, val + + def kitten_alias(val: str) -> Iterable[Tuple[str, List[str]]]: parts = val.split(maxsplit=2) if len(parts) >= 2: diff --git a/kitty/session.py b/kitty/session.py index 188bcc1a3..840910bc4 100644 --- a/kitty/session.py +++ b/kitty/session.py @@ -4,7 +4,7 @@ import shlex import sys -from typing import Generator, List, Optional, Sequence, Union +from typing import Generator, List, Optional, Union from .cli_stub import CLIOptions from .options.utils import to_layout_names, window_size @@ -39,9 +39,8 @@ class Tab: class Session: - def __init__(self, default_title: Optional[str] = None, default_watchers: Sequence[str] = ()): + def __init__(self, default_title: Optional[str] = None): self.tabs: List[Tab] = [] - self.default_watchers = list(default_watchers) self.active_tab_idx = 0 self.default_title = default_title self.os_window_size: Optional[WindowSizes] = None @@ -65,8 +64,6 @@ class Session: if isinstance(cmd, str): cmd = shlex.split(cmd) spec = parse_launch_args(cmd) - if self.default_watchers: - spec.opts.watcher = list(spec.opts.watcher) + self.default_watchers t = self.tabs[-1] if t.next_title and not spec.opts.window_title: spec.opts.window_title = t.next_title @@ -175,8 +172,7 @@ def create_sessions( else: yield from parse_session(session_data, opts) return - default_watchers = args.watcher if args else () - ans = Session(default_watchers=default_watchers) + ans = Session() current_layout = opts.enabled_layouts[0] if opts.enabled_layouts else 'tall' ans.add_tab(opts) ans.tabs[-1].layout = current_layout diff --git a/kitty/window.py b/kitty/window.py index d9511d1c9..0d780f7a6 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -13,7 +13,7 @@ from gettext import gettext as _ from itertools import chain from typing import ( TYPE_CHECKING, Any, Callable, Deque, Dict, Iterable, List, NamedTuple, - Optional, Pattern, Sequence, Tuple, Union + Optional, Pattern, Sequence, Tuple, Union, cast ) from .child import ProcessDesc @@ -315,6 +315,17 @@ class EdgeWidths: return {'left': self.left, 'right': self.right, 'top': self.top, 'bottom': self.bottom} +def global_watchers() -> Watchers: + spec = get_options().watcher + if getattr(global_watchers, 'options_spec', None) == spec: + return cast(Watchers, getattr(global_watchers, 'ans')) + from .launch import load_watch_modules + ans = load_watch_modules(spec) or Watchers() + setattr(global_watchers, 'ans', ans) + setattr(global_watchers, 'options_spec', spec) + return ans + + class Window: def __init__( @@ -326,7 +337,11 @@ class Window: copy_colors_from: Optional['Window'] = None, watchers: Optional[Watchers] = None ): - self.watchers = watchers or Watchers() + if watchers: + self.watchers = watchers + self.watchers.add(global_watchers()) + else: + self.watchers = global_watchers() self.current_mouse_event_button = 0 self.current_clipboard_read_ask: Optional[bool] = None self.prev_osc99_cmd = NotificationCommand()