Add an :option:launch --watcher option that allows defining callbacks that are called for various events in the window's life-cycle
Fixes #2440
This commit is contained in:
parent
8c23f9e526
commit
747ac85e7c
@ -7,6 +7,9 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
|
|||||||
0.17.2 [future]
|
0.17.2 [future]
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
- Add an :option:`launch --watcher` option that allows defining callbacks
|
||||||
|
that are called for various events in the window's life-cycle (:iss:`2440`)
|
||||||
|
|
||||||
- Fix a regression in 0.17 that broke drawing of borders with non-minimal
|
- Fix a regression in 0.17 that broke drawing of borders with non-minimal
|
||||||
borders (:iss:`2474`)
|
borders (:iss:`2474`)
|
||||||
|
|
||||||
|
|||||||
@ -61,6 +61,35 @@ currently active kitty window. For example::
|
|||||||
map f1 launch my-program @active-kitty-window-id
|
map f1 launch my-program @active-kitty-window-id
|
||||||
|
|
||||||
|
|
||||||
|
Watching launched windows
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
The :option:`launch --watcher` option allows you to specify python functions
|
||||||
|
that will be called at specific events, such as when the window is resized or
|
||||||
|
closed. Simply specify the path to a python module that specifies callback
|
||||||
|
functions for the events you are interested in, for example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def on_resize(boss, window, data):
|
||||||
|
# Here data will contain old_geometry and new_geometry
|
||||||
|
|
||||||
|
def on_close(boss, window, data):
|
||||||
|
# called when window is closed, typically when the program running in
|
||||||
|
# it exits.
|
||||||
|
|
||||||
|
|
||||||
|
Every callback is passed a reference to the global ``Boss`` object as well as
|
||||||
|
the ``Window`` object the action is occurring on. The ``data`` object is
|
||||||
|
mapping that contains event dependent data. Some useful methods and attributes
|
||||||
|
for the ``Window`` object are: ``as_text(as_ans=False, add_history=False,
|
||||||
|
add_wrap_markers=False, alternate_screen=False)`` with which you can get the
|
||||||
|
contents of the window and its scrollback buffer. Similarly,
|
||||||
|
``window.child.pid`` is the PID of the processes that was launched
|
||||||
|
in the window and ``window.id`` is the internal kitty ``id`` of the
|
||||||
|
window.
|
||||||
|
|
||||||
|
|
||||||
Syntax reference
|
Syntax reference
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
|||||||
@ -121,6 +121,10 @@ def file(x: str) -> str:
|
|||||||
return italic(x)
|
return italic(x)
|
||||||
|
|
||||||
|
|
||||||
|
def doc(x: str) -> str:
|
||||||
|
return f'https://sw.kovidgoyal.net/kitty/{x}.html'
|
||||||
|
|
||||||
|
|
||||||
OptionSpecSeq = List[Union[str, OptionDict]]
|
OptionSpecSeq = List[Union[str, OptionDict]]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -6,14 +6,15 @@
|
|||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
||||||
|
|
||||||
|
from .boss import Boss
|
||||||
from .child import Child
|
from .child import Child
|
||||||
from .cli import parse_args
|
from .cli import parse_args
|
||||||
from .cli_stub import LaunchCLIOptions
|
from .cli_stub import LaunchCLIOptions
|
||||||
|
from .constants import resolve_custom_file
|
||||||
from .fast_data_types import set_clipboard_string
|
from .fast_data_types import set_clipboard_string
|
||||||
from .utils import set_primary_selection
|
|
||||||
from .window import Window
|
|
||||||
from .boss import Boss
|
|
||||||
from .tabs import Tab
|
from .tabs import Tab
|
||||||
|
from .utils import set_primary_selection
|
||||||
|
from .window import Watchers, Window
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from typing import TypedDict
|
from typing import TypedDict
|
||||||
@ -148,6 +149,14 @@ defaults to :code:`kitty`.
|
|||||||
--os-window-name
|
--os-window-name
|
||||||
Set the WM_NAME property on X11 for the newly created OS Window when using
|
Set the WM_NAME property on X11 for the newly created OS Window when using
|
||||||
:option:`launch --type`=os-window. Defaults to :option:`launch --os-window-class`.
|
:option:`launch --type`=os-window. Defaults to :option:`launch --os-window-class`.
|
||||||
|
|
||||||
|
|
||||||
|
--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 or closed. See the section
|
||||||
|
on watchers in the launch command documentation :doc:`launch`. Relative paths are
|
||||||
|
resolved relative to the kitty config directory.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
@ -192,6 +201,23 @@ def tab_for_window(boss: Boss, opts: LaunchCLIOptions, target_tab: Optional[Tab]
|
|||||||
return tab
|
return tab
|
||||||
|
|
||||||
|
|
||||||
|
def load_watch_modules(opts: LaunchCLIOptions) -> Optional[Watchers]:
|
||||||
|
if not opts.watcher:
|
||||||
|
return None
|
||||||
|
import runpy
|
||||||
|
ans = Watchers()
|
||||||
|
for path in opts.watcher:
|
||||||
|
path = resolve_custom_file(path)
|
||||||
|
m = runpy.run_path(path, run_name='__kitty_watcher__')
|
||||||
|
w = m.get('on_close')
|
||||||
|
if callable(w):
|
||||||
|
ans.on_close.append(w)
|
||||||
|
w = m.get('on_resize')
|
||||||
|
if callable(w):
|
||||||
|
ans.on_resize.append(w)
|
||||||
|
return ans
|
||||||
|
|
||||||
|
|
||||||
class LaunchKwds(TypedDict):
|
class LaunchKwds(TypedDict):
|
||||||
|
|
||||||
allow_remote_control: bool
|
allow_remote_control: bool
|
||||||
@ -274,7 +300,8 @@ def launch(boss: Boss, opts: LaunchCLIOptions, args: List[str], target_tab: Opti
|
|||||||
else:
|
else:
|
||||||
tab = tab_for_window(boss, opts, target_tab)
|
tab = tab_for_window(boss, opts, target_tab)
|
||||||
if tab is not None:
|
if tab is not None:
|
||||||
new_window: Window = tab.new_window(env=env or None, **kw)
|
watchers = load_watch_modules(opts)
|
||||||
|
new_window: Window = tab.new_window(env=env or None, watchers=watchers or None, **kw)
|
||||||
if opts.keep_focus and active:
|
if opts.keep_focus and active:
|
||||||
boss.set_active_window(active, switch_os_window_if_needed=True)
|
boss.set_active_window(active, switch_os_window_if_needed=True)
|
||||||
return new_window
|
return new_window
|
||||||
|
|||||||
@ -26,7 +26,7 @@ from .layout import (
|
|||||||
from .options_stub import Options
|
from .options_stub import Options
|
||||||
from .tab_bar import TabBar, TabBarData
|
from .tab_bar import TabBar, TabBarData
|
||||||
from .utils import log_error, resolved_shell
|
from .utils import log_error, resolved_shell
|
||||||
from .window import Window, WindowDict
|
from .window import Window, WindowDict, Watchers
|
||||||
from .typing import TypedDict, SessionTab, SessionType
|
from .typing import TypedDict, SessionTab, SessionType
|
||||||
|
|
||||||
|
|
||||||
@ -345,11 +345,15 @@ class Tab: # {{{
|
|||||||
location: Optional[str] = None,
|
location: Optional[str] = None,
|
||||||
copy_colors_from: Optional[Window] = None,
|
copy_colors_from: Optional[Window] = None,
|
||||||
allow_remote_control: bool = False,
|
allow_remote_control: bool = False,
|
||||||
marker: Optional[str] = None
|
marker: Optional[str] = None,
|
||||||
|
watchers: Optional[Watchers] = None
|
||||||
) -> Window:
|
) -> Window:
|
||||||
child = self.launch_child(
|
child = self.launch_child(
|
||||||
use_shell=use_shell, cmd=cmd, stdin=stdin, cwd_from=cwd_from, cwd=cwd, env=env, allow_remote_control=allow_remote_control)
|
use_shell=use_shell, cmd=cmd, stdin=stdin, cwd_from=cwd_from, cwd=cwd, env=env, allow_remote_control=allow_remote_control)
|
||||||
window = Window(self, child, self.opts, self.args, override_title=override_title, copy_colors_from=copy_colors_from)
|
window = Window(
|
||||||
|
self, child, self.opts, self.args, override_title=override_title,
|
||||||
|
copy_colors_from=copy_colors_from, watchers=watchers
|
||||||
|
)
|
||||||
if overlay_for is not None:
|
if overlay_for is not None:
|
||||||
overlaid = next(w for w in self.windows if w.id == overlay_for)
|
overlaid = next(w for w in self.windows if w.id == overlay_for)
|
||||||
window.overlay_for = overlay_for
|
window.overlay_for = overlay_for
|
||||||
|
|||||||
@ -10,8 +10,8 @@ from collections import deque
|
|||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from typing import (
|
from typing import (
|
||||||
Any, Callable, Deque, Dict, List, Optional, Pattern, Sequence, Tuple,
|
Any, Callable, Deque, Dict, Iterable, List, Optional, Pattern, Sequence,
|
||||||
Union
|
Tuple, Union
|
||||||
)
|
)
|
||||||
|
|
||||||
from .child import ProcessDesc
|
from .child import ProcessDesc
|
||||||
@ -32,7 +32,7 @@ from .keys import defines, extended_key_event, keyboard_mode_name
|
|||||||
from .options_stub import Options
|
from .options_stub import Options
|
||||||
from .rgb import to_color
|
from .rgb import to_color
|
||||||
from .terminfo import get_capabilities
|
from .terminfo import get_capabilities
|
||||||
from .typing import ChildType, TabType, TypedDict
|
from .typing import BossType, ChildType, TabType, TypedDict
|
||||||
from .utils import (
|
from .utils import (
|
||||||
color_as_int, get_primary_selection, load_shaders, open_cmd, open_url,
|
color_as_int, get_primary_selection, load_shaders, open_cmd, open_url,
|
||||||
parse_color_set, read_shell_environment, sanitize_title,
|
parse_color_set, read_shell_environment, sanitize_title,
|
||||||
@ -77,6 +77,19 @@ DYNAMIC_COLOR_CODES = {
|
|||||||
DYNAMIC_COLOR_CODES.update({k+100: v for k, v in DYNAMIC_COLOR_CODES.items()})
|
DYNAMIC_COLOR_CODES.update({k+100: v for k, v in DYNAMIC_COLOR_CODES.items()})
|
||||||
|
|
||||||
|
|
||||||
|
class Watcher:
|
||||||
|
|
||||||
|
def __call__(self, boss: BossType, window: 'Window', data: Dict[str, Any]) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Watchers:
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.on_resize: List[Watcher] = []
|
||||||
|
self.on_close: List[Watcher] = []
|
||||||
|
|
||||||
|
|
||||||
def calculate_gl_geometry(window_geometry: WindowGeometry, viewport_width: int, viewport_height: int, cell_width: int, cell_height: int) -> ScreenGeometry:
|
def calculate_gl_geometry(window_geometry: WindowGeometry, viewport_width: int, viewport_height: int, cell_width: int, cell_height: int) -> ScreenGeometry:
|
||||||
dx, dy = 2 * cell_width / viewport_width, 2 * cell_height / viewport_height
|
dx, dy = 2 * cell_width / viewport_width, 2 * cell_height / viewport_height
|
||||||
xmargin = window_geometry.left / viewport_width
|
xmargin = window_geometry.left / viewport_width
|
||||||
@ -181,8 +194,10 @@ class Window:
|
|||||||
opts: Options,
|
opts: Options,
|
||||||
args: CLIOptions,
|
args: CLIOptions,
|
||||||
override_title: Optional[str] = None,
|
override_title: Optional[str] = None,
|
||||||
copy_colors_from: Optional['Window'] = None
|
copy_colors_from: Optional['Window'] = None,
|
||||||
|
watchers: Optional[Watchers] = None
|
||||||
):
|
):
|
||||||
|
self.watchers = watchers or Watchers()
|
||||||
self.action_on_close: Optional[Callable] = None
|
self.action_on_close: Optional[Callable] = None
|
||||||
self.action_on_removal: Optional[Callable] = None
|
self.action_on_removal: Optional[Callable] = None
|
||||||
self.current_marker_spec: Optional[Tuple[str, Union[str, Tuple[Tuple[int, str], ...]]]] = None
|
self.current_marker_spec: Optional[Tuple[str, Union[str, Tuple[Tuple[int, str], ...]]]] = None
|
||||||
@ -303,6 +318,7 @@ class Window:
|
|||||||
if not self.pty_resized_once:
|
if not self.pty_resized_once:
|
||||||
self.pty_resized_once = True
|
self.pty_resized_once = True
|
||||||
self.child.mark_terminal_ready()
|
self.child.mark_terminal_ready()
|
||||||
|
self.call_watchers(self.watchers.on_resize, {'old_geometry': self.geometry, 'new_geometry': new_geometry})
|
||||||
else:
|
else:
|
||||||
sg = self.update_position(new_geometry)
|
sg = self.update_position(new_geometry)
|
||||||
self.geometry = g = new_geometry
|
self.geometry = g = new_geometry
|
||||||
@ -536,7 +552,17 @@ class Window:
|
|||||||
return ''.join((l.rstrip() or '\n') for l in lines)
|
return ''.join((l.rstrip() or '\n') for l in lines)
|
||||||
return ''.join(lines)
|
return ''.join(lines)
|
||||||
|
|
||||||
|
def call_watchers(self, which: Iterable[Watcher], data: Dict[str, Any]) -> None:
|
||||||
|
boss = get_boss()
|
||||||
|
for w in which:
|
||||||
|
try:
|
||||||
|
w(boss, self, data)
|
||||||
|
except Exception:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
def destroy(self) -> None:
|
def destroy(self) -> None:
|
||||||
|
self.call_watchers(self.watchers.on_close, {})
|
||||||
self.destroyed = True
|
self.destroyed = True
|
||||||
if hasattr(self, 'screen'):
|
if hasattr(self, 'screen'):
|
||||||
# Remove cycles so that screen is de-allocated immediately
|
# Remove cycles so that screen is de-allocated immediately
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user