A new watcher option for kitty.conf that replaces the old --watcher cli flag

Applies to all windows, not just initial ones.
This commit is contained in:
Kovid Goyal 2021-09-29 14:01:30 +05:30
parent 7a16ef2cc4
commit 166ea9deb9
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
11 changed files with 86 additions and 34 deletions

View File

@ -18,6 +18,11 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
- Allow the user to supply a custom Python function to draw tab bar. See - Allow the user to supply a custom Python function to draw tab bar. See
:opt:`tab_bar_style` :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 - Add support for reporting mouse events with pixel co-ordinates using the
``SGR_PIXEL_PROTOCOL`` introduced in xterm 359 ``SGR_PIXEL_PROTOCOL`` introduced in xterm 359

View File

@ -89,6 +89,8 @@ For example::
map f1 launch my-program @active-kitty-window-id map f1 launch my-program @active-kitty-window-id
.. _watchers:
Watching launched windows Watching launched windows
--------------------------- ---------------------------

View File

@ -571,15 +571,6 @@ def parse_cmdline(oc: Options, disabled: OptionSpecSeq, ans: Any, args: Optional
return leftover_args 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: def options_spec() -> str:
if not hasattr(options_spec, 'ans'): if not hasattr(options_spec, 'ans'):
OPTIONS = ''' 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. 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 --hold
type=bool-set type=bool-set
Remain open after child process exits. Note that this only affects the first 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( setattr(options_spec, 'ans', OPTIONS.format(
appname=appname, config_help=CONFIG_HELP.format(appname=appname, conf_name=appname), appname=appname, config_help=CONFIG_HELP.format(appname=appname, conf_name=appname),
watcher=WATCHER_DEFINITION
)) ))
ans: str = getattr(options_spec, 'ans') ans: str = getattr(options_spec, 'ans')
return ans return ans

View File

@ -44,6 +44,7 @@ def expand_opt_references(conf_name: str, text: str) -> str:
def remove_markup(text: str) -> str: def remove_markup(text: str) -> str:
ref_map = { ref_map = {
'layouts': f'{website_url("overview")}#layouts', 'layouts': f'{website_url("overview")}#layouts',
'watchers': f'{website_url("launch")}#watchers',
'sessions': f'{website_url("overview")}#startup-sessions', 'sessions': f'{website_url("overview")}#startup-sessions',
'functional': f'{website_url("keyboard-protocol")}#functional-key-definitions', 'functional': f'{website_url("keyboard-protocol")}#functional-key-definitions',
'action-select_tab': f'{website_url("actions")}#select-tab', 'action-select_tab': f'{website_url("actions")}#select-tab',

View File

@ -3,21 +3,21 @@
# License: GPLv3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net> # License: GPLv3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net>
from typing import Any, Dict, List, NamedTuple, Optional, Sequence from typing import Any, Dict, Iterable, List, NamedTuple, Optional, Sequence
from .boss import Boss from .boss import Boss
from .child import Child from .child import Child
from .cli import WATCHER_DEFINITION, parse_args from .cli import parse_args
from .cli_stub import LaunchCLIOptions from .cli_stub import LaunchCLIOptions
from .constants import resolve_custom_file from .constants import resolve_custom_file
from .fast_data_types import ( from .fast_data_types import (
get_options, patch_color_profiles, set_clipboard_string get_options, patch_color_profiles, set_clipboard_string
) )
from .options.utils import env as parse_env
from .tabs import Tab from .tabs import Tab
from .types import run_once 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 .window import Watchers, Window
from .options.utils import env as parse_env
try: try:
from typing import TypedDict 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`` 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: 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 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: if not watchers:
return None return None
import runpy import runpy
ans = Watchers() ans = Watchers()
for path in watchers: for path in watchers:
path = resolve_custom_file(path) path = resolve_custom_file(path)
m = watcher_modules.get(path, None)
if m is None:
try:
m = runpy.run_path(path, run_name='__kitty_watcher__') 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') w = m.get('on_close')
if callable(w): if callable(w):
ans.on_close.append(w) ans.on_close.append(w)

View File

@ -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', opt('update_check_interval', '24',
option_type='float', option_type='float',
long_text=''' long_text='''

View File

@ -16,7 +16,7 @@ from kitty.options.utils import (
parse_mouse_map, resize_draw_strategy, scrollback_lines, scrollback_pager_history_size, symbol_map, 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_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, 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: def visual_bell_duration(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['visual_bell_duration'] = positive_float(val) 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: def wayland_titlebar_color(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['wayland_titlebar_color'] = macos_titlebar_color(val) ans['wayland_titlebar_color'] = macos_titlebar_color(val)
@ -1292,6 +1296,7 @@ def create_result_dict() -> typing.Dict[str, typing.Any]:
'font_features': {}, 'font_features': {},
'kitten_alias': {}, 'kitten_alias': {},
'symbol_map': {}, 'symbol_map': {},
'watcher': {},
'map': [], 'map': [],
'mouse_map': [], 'mouse_map': [],
} }

View File

@ -429,6 +429,7 @@ option_names = ( # {{{
'url_prefixes', 'url_prefixes',
'url_style', 'url_style',
'visual_bell_duration', 'visual_bell_duration',
'watcher',
'wayland_titlebar_color', 'wayland_titlebar_color',
'wheel_scroll_multiplier', 'wheel_scroll_multiplier',
'window_alert_on_bell', 'window_alert_on_bell',
@ -577,6 +578,7 @@ class Options:
font_features: typing.Dict[str, typing.Tuple[kitty.fonts.FontFeature, ...]] = {} font_features: typing.Dict[str, typing.Tuple[kitty.fonts.FontFeature, ...]] = {}
kitten_alias: typing.Dict[str, typing.List[str]] = {} kitten_alias: typing.Dict[str, typing.List[str]] = {}
symbol_map: typing.Dict[typing.Tuple[int, int], str] = {} symbol_map: typing.Dict[typing.Tuple[int, int], str] = {}
watcher: typing.Dict[str, str] = {}
map: typing.List[kitty.options.utils.KeyDefinition] = [] map: typing.List[kitty.options.utils.KeyDefinition] = []
keymap: KeyMap = {} keymap: KeyMap = {}
sequence_map: SequenceMap = {} sequence_map: SequenceMap = {}
@ -691,6 +693,7 @@ defaults.env = {}
defaults.font_features = {} defaults.font_features = {}
defaults.kitten_alias = {} defaults.kitten_alias = {}
defaults.symbol_map = {} defaults.symbol_map = {}
defaults.watcher = {}
defaults.map = [ defaults.map = [
# copy_to_clipboard # copy_to_clipboard
KeyDefinition(False, KeyAction('copy_to_clipboard'), 1024, False, 99, ()), KeyDefinition(False, KeyAction('copy_to_clipboard'), 1024, False, 99, ()),

View File

@ -7,8 +7,8 @@ import os
import re import re
import sys import sys
from typing import ( from typing import (
Any, Callable, Dict, Iterable, List, NamedTuple, Optional, Sequence, Tuple, Any, Callable, Container, Dict, Iterable, List, NamedTuple, Optional,
Union Sequence, Tuple, Union
) )
import kitty.fast_data_types as defines 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 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]]]: def kitten_alias(val: str) -> Iterable[Tuple[str, List[str]]]:
parts = val.split(maxsplit=2) parts = val.split(maxsplit=2)
if len(parts) >= 2: if len(parts) >= 2:

View File

@ -4,7 +4,7 @@
import shlex import shlex
import sys import sys
from typing import Generator, List, Optional, Sequence, Union from typing import Generator, List, Optional, Union
from .cli_stub import CLIOptions from .cli_stub import CLIOptions
from .options.utils import to_layout_names, window_size from .options.utils import to_layout_names, window_size
@ -39,9 +39,8 @@ class Tab:
class Session: 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.tabs: List[Tab] = []
self.default_watchers = list(default_watchers)
self.active_tab_idx = 0 self.active_tab_idx = 0
self.default_title = default_title self.default_title = default_title
self.os_window_size: Optional[WindowSizes] = None self.os_window_size: Optional[WindowSizes] = None
@ -65,8 +64,6 @@ class Session:
if isinstance(cmd, str): if isinstance(cmd, str):
cmd = shlex.split(cmd) cmd = shlex.split(cmd)
spec = parse_launch_args(cmd) spec = parse_launch_args(cmd)
if self.default_watchers:
spec.opts.watcher = list(spec.opts.watcher) + self.default_watchers
t = self.tabs[-1] t = self.tabs[-1]
if t.next_title and not spec.opts.window_title: if t.next_title and not spec.opts.window_title:
spec.opts.window_title = t.next_title spec.opts.window_title = t.next_title
@ -175,8 +172,7 @@ def create_sessions(
else: else:
yield from parse_session(session_data, opts) yield from parse_session(session_data, opts)
return return
default_watchers = args.watcher if args else () ans = Session()
ans = Session(default_watchers=default_watchers)
current_layout = opts.enabled_layouts[0] if opts.enabled_layouts else 'tall' current_layout = opts.enabled_layouts[0] if opts.enabled_layouts else 'tall'
ans.add_tab(opts) ans.add_tab(opts)
ans.tabs[-1].layout = current_layout ans.tabs[-1].layout = current_layout

View File

@ -13,7 +13,7 @@ from gettext import gettext as _
from itertools import chain from itertools import chain
from typing import ( from typing import (
TYPE_CHECKING, Any, Callable, Deque, Dict, Iterable, List, NamedTuple, TYPE_CHECKING, Any, Callable, Deque, Dict, Iterable, List, NamedTuple,
Optional, Pattern, Sequence, Tuple, Union Optional, Pattern, Sequence, Tuple, Union, cast
) )
from .child import ProcessDesc from .child import ProcessDesc
@ -315,6 +315,17 @@ class EdgeWidths:
return {'left': self.left, 'right': self.right, 'top': self.top, 'bottom': self.bottom} 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: class Window:
def __init__( def __init__(
@ -326,7 +337,11 @@ class Window:
copy_colors_from: Optional['Window'] = None, copy_colors_from: Optional['Window'] = None,
watchers: Optional[Watchers] = 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_mouse_event_button = 0
self.current_clipboard_read_ask: Optional[bool] = None self.current_clipboard_read_ask: Optional[bool] = None
self.prev_osc99_cmd = NotificationCommand() self.prev_osc99_cmd = NotificationCommand()