Allow specifying watchers in session files and via a command line argument
This commit is contained in:
parent
f4ddaacb3c
commit
392c31f5fe
@ -21,6 +21,9 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
|
||||
|
||||
- Allow tracking focus change events in watchers (:iss:`2918`)
|
||||
|
||||
- Allow specifying watchers in session files and via a command line argument
|
||||
(:iss:`2933`)
|
||||
|
||||
|
||||
0.18.3 [2020-08-11]
|
||||
-------------------
|
||||
|
||||
@ -353,6 +353,15 @@ For example:
|
||||
focus
|
||||
launch emacs
|
||||
|
||||
# Add a watcher that will be called with various events that occur
|
||||
# on all subsequent windows. See the documentation of the launch command
|
||||
# for details on watchers.
|
||||
watcher /some/python/file.py
|
||||
launch mpd
|
||||
launch irssi
|
||||
# Remove the watcher for further windows
|
||||
watcher clear
|
||||
|
||||
|
||||
Mouse features
|
||||
-------------------
|
||||
|
||||
@ -171,8 +171,12 @@ class Boss:
|
||||
cocoa_set_notification_activated_callback(notification_activated)
|
||||
|
||||
def startup_first_child(self, os_window_id: Optional[int]) -> None:
|
||||
from kitty.launch import load_watch_modules
|
||||
startup_sessions = create_sessions(self.opts, self.args, default_session=self.opts.startup_session)
|
||||
watchers = load_watch_modules(self.args.watcher)
|
||||
for startup_session in startup_sessions:
|
||||
if watchers is not None and watchers.has_watchers:
|
||||
startup_session.add_watchers_to_all_windows(watchers)
|
||||
self.add_os_window(startup_session, os_window_id=os_window_id)
|
||||
os_window_id = None
|
||||
if self.args.start_as != 'normal':
|
||||
|
||||
18
kitty/cli.py
18
kitty/cli.py
@ -544,6 +544,15 @@ 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 = '''
|
||||
@ -593,6 +602,11 @@ 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 crated windows, not new windows
|
||||
created after startup.
|
||||
|
||||
|
||||
--hold
|
||||
type=bool-set
|
||||
Remain open after child process exits. Note that this only affects the first
|
||||
@ -695,8 +709,8 @@ type=bool-set
|
||||
!
|
||||
'''
|
||||
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')
|
||||
return ans
|
||||
|
||||
@ -254,6 +254,8 @@ def complete_kitty_cli_arg(ans: Completions, opt: Optional[OptionDict], prefix:
|
||||
complete_files_and_dirs(ans, prefix, files_group_name='Config files', predicate=is_conf_file)
|
||||
elif dest == 'session':
|
||||
complete_files_and_dirs(ans, prefix, files_group_name='Session files')
|
||||
elif dest == 'watcher':
|
||||
complete_files_and_dirs(ans, prefix, files_group_name='Watcher files')
|
||||
elif dest == 'directory':
|
||||
complete_files_and_dirs(ans, prefix, files_group_name='Directories', predicate=os.path.isdir)
|
||||
elif dest == 'start_as':
|
||||
|
||||
@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional, Sequence, Tuple
|
||||
|
||||
from .boss import Boss
|
||||
from .child import Child
|
||||
from .cli import parse_args
|
||||
from .cli import parse_args, WATCHER_DEFINITION
|
||||
from .cli_stub import LaunchCLIOptions
|
||||
from .constants import resolve_custom_file
|
||||
from .fast_data_types import set_clipboard_string
|
||||
@ -152,13 +152,7 @@ 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`.
|
||||
|
||||
|
||||
--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.
|
||||
'''
|
||||
''' + WATCHER_DEFINITION
|
||||
|
||||
|
||||
def parse_launch_args(args: Optional[Sequence[str]] = None) -> Tuple[LaunchCLIOptions, List[str]]:
|
||||
@ -202,12 +196,12 @@ def tab_for_window(boss: Boss, opts: LaunchCLIOptions, target_tab: Optional[Tab]
|
||||
return tab
|
||||
|
||||
|
||||
def load_watch_modules(opts: LaunchCLIOptions) -> Optional[Watchers]:
|
||||
if not opts.watcher:
|
||||
def load_watch_modules(watchers: Sequence[str]) -> Optional[Watchers]:
|
||||
if not watchers:
|
||||
return None
|
||||
import runpy
|
||||
ans = Watchers()
|
||||
for path in opts.watcher:
|
||||
for path in watchers:
|
||||
path = resolve_custom_file(path)
|
||||
m = runpy.run_path(path, run_name='__kitty_watcher__')
|
||||
w = m.get('on_close')
|
||||
@ -307,7 +301,7 @@ def launch(boss: Boss, opts: LaunchCLIOptions, args: List[str], target_tab: Opti
|
||||
else:
|
||||
tab = tab_for_window(boss, opts, target_tab)
|
||||
if tab is not None:
|
||||
watchers = load_watch_modules(opts)
|
||||
watchers = load_watch_modules(opts.watcher)
|
||||
new_window: Window = tab.new_window(env=env or None, watchers=watchers or None, **kw)
|
||||
if opts.keep_focus and active:
|
||||
boss.set_active_window(active, switch_os_window_if_needed=True)
|
||||
|
||||
@ -14,6 +14,7 @@ from .options_stub import Options
|
||||
from .os_window_size import WindowSize, WindowSizeData, WindowSizes
|
||||
from .typing import SpecialWindowInstance
|
||||
from .utils import log_error, resolved_shell
|
||||
from .window import Watchers
|
||||
|
||||
|
||||
def get_os_window_sizing_data(opts: Options, session: Optional['Session'] = None) -> WindowSizeData:
|
||||
@ -26,14 +27,15 @@ def get_os_window_sizing_data(opts: Options, session: Optional['Session'] = None
|
||||
|
||||
class Tab:
|
||||
|
||||
def __init__(self, opts: Options, name: str):
|
||||
self.windows: List[Union[List[str], 'SpecialWindowInstance']] = []
|
||||
def __init__(self, opts: Options, name: str, watchers: Watchers):
|
||||
self.windows: List['SpecialWindowInstance'] = []
|
||||
self.name = name.strip()
|
||||
self.active_window_idx = 0
|
||||
self.enabled_layouts = opts.enabled_layouts
|
||||
self.layout = (self.enabled_layouts or ['tall'])[0]
|
||||
self.cwd: Optional[str] = None
|
||||
self.next_title: Optional[str] = None
|
||||
self.watchers: Watchers = watchers.copy()
|
||||
|
||||
|
||||
class Session:
|
||||
@ -44,11 +46,25 @@ class Session:
|
||||
self.default_title = default_title
|
||||
self.os_window_size: Optional[WindowSizes] = None
|
||||
self.os_window_class: Optional[str] = None
|
||||
self.watchers = Watchers()
|
||||
|
||||
def add_watchers_to_all_windows(self, watchers: Watchers) -> None:
|
||||
def add(w: 'SpecialWindowInstance') -> 'SpecialWindowInstance':
|
||||
if w.watchers is None:
|
||||
return w._replace(watchers=watchers)
|
||||
wt = w.watchers.copy()
|
||||
wt.add(watchers)
|
||||
return w._replace(watchers=wt)
|
||||
|
||||
for tab in self.tabs:
|
||||
tab.windows = [add(w) for w in tab.windows]
|
||||
|
||||
def add_tab(self, opts: Options, name: str = '') -> None:
|
||||
if self.tabs and not self.tabs[-1].windows:
|
||||
del self.tabs[-1]
|
||||
self.tabs.append(Tab(opts, name))
|
||||
if self.tabs:
|
||||
self.tabs[-1].watchers = self.watchers.copy()
|
||||
self.tabs.append(Tab(opts, name, self.watchers))
|
||||
|
||||
def set_next_title(self, title: str) -> None:
|
||||
self.tabs[-1].next_title = title.strip()
|
||||
@ -65,10 +81,16 @@ class Session:
|
||||
cmd = None
|
||||
from .tabs import SpecialWindow
|
||||
t = self.tabs[-1]
|
||||
t.windows.append(SpecialWindow(cmd, cwd=t.cwd, override_title=t.next_title or self.default_title))
|
||||
watchers: Optional[Watchers] = None
|
||||
if self.watchers.has_watchers:
|
||||
watchers = self.watchers.copy()
|
||||
sw = SpecialWindow(cmd, cwd=t.cwd, override_title=t.next_title or self.default_title, watchers=watchers)
|
||||
t.windows.append(sw)
|
||||
t.next_title = None
|
||||
|
||||
def add_special_window(self, sw: 'SpecialWindowInstance') -> None:
|
||||
if self.watchers.has_watchers:
|
||||
sw = sw._replace(watchers=self.watchers.copy())
|
||||
self.tabs[-1].windows.append(sw)
|
||||
|
||||
def focus(self) -> None:
|
||||
@ -87,9 +109,13 @@ class Session:
|
||||
def parse_session(raw: str, opts: Options, default_title: Optional[str] = None) -> Generator[Session, None, None]:
|
||||
|
||||
def finalize_session(ans: Session) -> Session:
|
||||
from .tabs import SpecialWindow
|
||||
for t in ans.tabs:
|
||||
if not t.windows:
|
||||
t.windows.append(resolved_shell(opts))
|
||||
w: Optional[Watchers] = None
|
||||
if t.watchers.has_watchers:
|
||||
w = t.watchers.copy()
|
||||
t.windows.append(SpecialWindow(cmd=resolved_shell(opts), watchers=w))
|
||||
return ans
|
||||
|
||||
ans = Session(default_title)
|
||||
@ -107,7 +133,9 @@ def parse_session(raw: str, opts: Options, default_title: Optional[str] = None)
|
||||
ans.add_tab(opts, rest)
|
||||
elif cmd == 'new_os_window':
|
||||
yield finalize_session(ans)
|
||||
wt = ans.watchers
|
||||
ans = Session(default_title)
|
||||
ans.watchers = wt.copy()
|
||||
ans.add_tab(opts, rest)
|
||||
elif cmd == 'layout':
|
||||
ans.set_layout(rest)
|
||||
@ -127,6 +155,14 @@ def parse_session(raw: str, opts: Options, default_title: Optional[str] = None)
|
||||
ans.os_window_size = WindowSizes(WindowSize(*w), WindowSize(*h))
|
||||
elif cmd == 'os_window_class':
|
||||
ans.os_window_class = rest
|
||||
elif cmd == 'watcher':
|
||||
from .launch import load_watch_modules
|
||||
if rest == 'clear':
|
||||
ans.watchers = Watchers()
|
||||
else:
|
||||
watchers = load_watch_modules((rest,))
|
||||
if watchers is not None:
|
||||
ans.watchers.add(watchers)
|
||||
else:
|
||||
raise ValueError('Unknown command in session file: {}'.format(cmd))
|
||||
yield finalize_session(ans)
|
||||
|
||||
@ -48,6 +48,7 @@ class SpecialWindowInstance(NamedTuple):
|
||||
cwd: Optional[str]
|
||||
overlay_for: Optional[int]
|
||||
env: Optional[Dict[str, str]]
|
||||
watchers: Optional[Watchers]
|
||||
|
||||
|
||||
def SpecialWindow(
|
||||
@ -57,9 +58,10 @@ def SpecialWindow(
|
||||
cwd_from: Optional[int] = None,
|
||||
cwd: Optional[str] = None,
|
||||
overlay_for: Optional[int] = None,
|
||||
env: Optional[Dict[str, str]] = None
|
||||
env: Optional[Dict[str, str]] = None,
|
||||
watchers: Optional[Watchers] = None
|
||||
) -> SpecialWindowInstance:
|
||||
return SpecialWindowInstance(cmd, stdin, override_title, cwd_from, cwd, overlay_for, env)
|
||||
return SpecialWindowInstance(cmd, stdin, override_title, cwd_from, cwd, overlay_for, env, watchers)
|
||||
|
||||
|
||||
def add_active_id_to_history(items: Deque[int], item_id: int, maxlen: int = 64) -> None:
|
||||
@ -333,14 +335,14 @@ class Tab: # {{{
|
||||
special_window: SpecialWindowInstance,
|
||||
location: Optional[str] = None,
|
||||
copy_colors_from: Optional[Window] = None,
|
||||
allow_remote_control: bool = False
|
||||
allow_remote_control: bool = False,
|
||||
) -> Window:
|
||||
return self.new_window(
|
||||
use_shell=False, cmd=special_window.cmd, stdin=special_window.stdin,
|
||||
override_title=special_window.override_title,
|
||||
cwd_from=special_window.cwd_from, cwd=special_window.cwd, overlay_for=special_window.overlay_for,
|
||||
env=special_window.env, location=location, copy_colors_from=copy_colors_from,
|
||||
allow_remote_control=allow_remote_control
|
||||
allow_remote_control=allow_remote_control, watchers=special_window.watchers
|
||||
)
|
||||
|
||||
def close_window(self) -> None:
|
||||
|
||||
@ -87,10 +87,37 @@ class Watcher:
|
||||
|
||||
class Watchers:
|
||||
|
||||
on_resize: List[Watcher]
|
||||
on_close: List[Watcher]
|
||||
on_focus_change: List[Watcher]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.on_resize: List[Watcher] = []
|
||||
self.on_close: List[Watcher] = []
|
||||
self.on_focus_change: List[Watcher] = []
|
||||
self.on_resize = []
|
||||
self.on_close = []
|
||||
self.on_focus_change = []
|
||||
|
||||
def add(self, others: 'Watchers') -> None:
|
||||
def merge(base: List[Watcher], other: List[Watcher]) -> None:
|
||||
for x in other:
|
||||
if x not in base:
|
||||
base.append(x)
|
||||
merge(self.on_resize, others.on_resize)
|
||||
merge(self.on_close, others.on_close)
|
||||
merge(self.on_focus_change, others.on_focus_change)
|
||||
|
||||
def clear(self) -> None:
|
||||
del self.on_close[:], self.on_resize[:], self.on_focus_change[:]
|
||||
|
||||
def copy(self) -> 'Watchers':
|
||||
ans = Watchers()
|
||||
ans.on_close = self.on_close[:]
|
||||
ans.on_resize = self.on_resize[:]
|
||||
ans.on_focus_change = self.on_focus_change
|
||||
return ans
|
||||
|
||||
@property
|
||||
def has_watchers(self) -> bool:
|
||||
return bool(self.on_close or self.on_resize or self.on_focus_change)
|
||||
|
||||
|
||||
def call_watchers(windowref: Callable[[], Optional['Window']], which: str, data: Dict[str, Any]) -> None:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user