Allow using the full launch command in session files

Note this is slightly backward incompatible.
This commit is contained in:
Kovid Goyal 2021-01-31 11:37:00 +05:30
parent e9e8ef7210
commit 35517d3e6f
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 52 additions and 68 deletions

View File

@ -11,6 +11,11 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
used by full screen terminal programs and even games, see used by full screen terminal programs and even games, see
:doc:`keyboard-protocol` (:iss:`3248`) :doc:`keyboard-protocol` (:iss:`3248`)
- **Backward incompatibility**: Session files now use the full :doc:`launch <launch>`
command with all its capabilities. However, the syntax of the command is
slightly different from before. In particular watchers are now specified
directly on launch and env vars are set using ``--env``.
- diff kitten: Implement recursive diff over SSH (:iss:`3268`) - diff kitten: Implement recursive diff over SSH (:iss:`3268`)
- ssh kitten: Allow using python instead of the shell on the server, useful if - ssh kitten: Allow using python instead of the shell on the server, useful if

View File

@ -340,7 +340,7 @@ For example:
launch zsh launch zsh
# Create a window with some environment variables set and run # Create a window with some environment variables set and run
# vim in it # vim in it
launch env FOO=BAR vim launch --env FOO=BAR vim
# Set the title for the next window # Set the title for the next window
title Chat with x title Chat with x
launch irssi --profile x launch irssi --profile x
@ -367,14 +367,9 @@ For example:
focus focus
launch emacs launch emacs
# Add a watcher that will be called with various events that occur .. note::
# on all subsequent windows. See the documentation of the launch command The :doc:`launch <launch>` command when used in a session file
# for details on watchers. cannot create new OS windows, or tabs.
watcher /some/python/file.py
launch mpd
launch irssi
# Remove the watcher for further windows
watcher clear
Mouse features Mouse features

View File

@ -177,12 +177,8 @@ class Boss:
cocoa_set_notification_activated_callback(notification_activated) cocoa_set_notification_activated_callback(notification_activated)
def startup_first_child(self, os_window_id: Optional[int]) -> None: 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) 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: 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) self.add_os_window(startup_session, os_window_id=os_window_id)
os_window_id = None os_window_id = None
if self.args.start_as != 'normal': if self.args.start_as != 'normal':

View File

@ -4,7 +4,7 @@
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, NamedTuple, Optional, Sequence
from .boss import Boss from .boss import Boss
from .child import Child from .child import Child
@ -13,7 +13,7 @@ from .cli_stub import LaunchCLIOptions
from .constants import resolve_custom_file from .constants import resolve_custom_file
from .fast_data_types import set_clipboard_string from .fast_data_types import set_clipboard_string
from .tabs import Tab from .tabs import Tab
from .utils import find_exe, set_primary_selection, read_shell_environment from .utils import find_exe, read_shell_environment, set_primary_selection
from .window import Watchers, Window from .window import Watchers, Window
try: try:
@ -22,6 +22,11 @@ except ImportError:
TypedDict = Dict[str, Any] TypedDict = Dict[str, Any]
class LaunchSpec(NamedTuple):
opts: LaunchCLIOptions
args: List[str]
@lru_cache(maxsize=2) @lru_cache(maxsize=2)
def options_spec() -> str: def options_spec() -> str:
return ''' return '''
@ -159,13 +164,13 @@ Set the WM_NAME property on X11 for the newly created OS Window when using
''' + WATCHER_DEFINITION ''' + WATCHER_DEFINITION
def parse_launch_args(args: Optional[Sequence[str]] = None) -> Tuple[LaunchCLIOptions, List[str]]: def parse_launch_args(args: Optional[Sequence[str]] = None) -> LaunchSpec:
args = list(args or ()) args = list(args or ())
try: try:
opts, args = parse_args(result_class=LaunchCLIOptions, args=args, ospec=options_spec) opts, args = parse_args(result_class=LaunchCLIOptions, args=args, ospec=options_spec)
except SystemExit as e: except SystemExit as e:
raise ValueError from e raise ValueError from e
return opts, args return LaunchSpec(opts, args)
def get_env(opts: LaunchCLIOptions, active_child: Child) -> Dict[str, str]: def get_env(opts: LaunchCLIOptions, active_child: Child) -> Dict[str, str]:
@ -234,7 +239,13 @@ class LaunchKwds(TypedDict):
stdin: Optional[bytes] stdin: Optional[bytes]
def launch(boss: Boss, opts: LaunchCLIOptions, args: List[str], target_tab: Optional[Tab] = None) -> Optional[Window]: def launch(
boss: Boss,
opts: LaunchCLIOptions,
args: List[str],
target_tab: Optional[Tab] = None,
force_target_tab: bool = False
) -> Optional[Window]:
active = boss.active_window_for_cwd active = boss.active_window_for_cwd
active_child = getattr(active, 'child', None) active_child = getattr(active, 'child', None)
env = get_env(opts, active_child) env = get_env(opts, active_child)
@ -310,6 +321,9 @@ def launch(boss: Boss, opts: LaunchCLIOptions, args: List[str], target_tab: Opti
set_clipboard_string(stdin) set_clipboard_string(stdin)
else: else:
set_primary_selection(stdin) set_primary_selection(stdin)
else:
if force_target_tab:
tab = target_tab
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:

View File

@ -4,7 +4,7 @@
import shlex import shlex
import sys import sys
from typing import Generator, List, Optional, Union from typing import Generator, List, Optional, Sequence, Union
from .cli_stub import CLIOptions from .cli_stub import CLIOptions
from .config_data import to_layout_names from .config_data import to_layout_names
@ -14,7 +14,6 @@ from .options_stub import Options
from .os_window_size import WindowSize, WindowSizeData, WindowSizes from .os_window_size import WindowSize, WindowSizeData, WindowSizes
from .typing import SpecialWindowInstance from .typing import SpecialWindowInstance
from .utils import log_error, resolved_shell from .utils import log_error, resolved_shell
from .window import Watchers
def get_os_window_sizing_data(opts: Options, session: Optional['Session'] = None) -> WindowSizeData: def get_os_window_sizing_data(opts: Options, session: Optional['Session'] = None) -> WindowSizeData:
@ -27,44 +26,31 @@ def get_os_window_sizing_data(opts: Options, session: Optional['Session'] = None
class Tab: class Tab:
def __init__(self, opts: Options, name: str, watchers: Watchers): def __init__(self, opts: Options, name: str):
self.windows: List['SpecialWindowInstance'] = [] from .launch import LaunchSpec
self.windows: List[Union[LaunchSpec, 'SpecialWindowInstance']] = []
self.name = name.strip() self.name = name.strip()
self.active_window_idx = 0 self.active_window_idx = 0
self.enabled_layouts = opts.enabled_layouts self.enabled_layouts = opts.enabled_layouts
self.layout = (self.enabled_layouts or ['tall'])[0] self.layout = (self.enabled_layouts or ['tall'])[0]
self.cwd: Optional[str] = None self.cwd: Optional[str] = None
self.next_title: Optional[str] = None self.next_title: Optional[str] = None
self.watchers: Watchers = watchers.copy()
class Session: class Session:
def __init__(self, default_title: Optional[str] = None): def __init__(self, default_title: Optional[str] = None, default_watchers: Sequence[str] = ()):
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
self.os_window_class: Optional[str] = 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: def add_tab(self, opts: Options, name: str = '') -> None:
if self.tabs and not self.tabs[-1].windows: if self.tabs and not self.tabs[-1].windows:
del self.tabs[-1] del self.tabs[-1]
if self.tabs: self.tabs.append(Tab(opts, name))
self.tabs[-1].watchers = self.watchers.copy()
self.tabs.append(Tab(opts, name, self.watchers))
def set_next_title(self, title: str) -> None: def set_next_title(self, title: str) -> None:
self.tabs[-1].next_title = title.strip() self.tabs[-1].next_title = title.strip()
@ -75,22 +61,18 @@ class Session:
self.tabs[-1].layout = val self.tabs[-1].layout = val
def add_window(self, cmd: Union[None, str, List[str]]) -> None: def add_window(self, cmd: Union[None, str, List[str]]) -> None:
if cmd: from .launch import parse_launch_args
cmd = shlex.split(cmd) if isinstance(cmd, str) else cmd if isinstance(cmd, str):
else: cmd = shlex.split(cmd)
cmd = None spec = parse_launch_args(cmd)
from .tabs import SpecialWindow if self.default_watchers:
spec.opts.watcher = list(spec.opts.watcher) + self.default_watchers
t = self.tabs[-1] t = self.tabs[-1]
watchers: Optional[Watchers] = None spec.opts.cwd = spec.opts.cwd or t.cwd
if self.watchers.has_watchers: t.windows.append(spec)
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 t.next_title = None
def add_special_window(self, sw: 'SpecialWindowInstance') -> 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) self.tabs[-1].windows.append(sw)
def focus(self) -> None: def focus(self) -> None:
@ -112,10 +94,7 @@ def parse_session(raw: str, opts: Options, default_title: Optional[str] = None)
from .tabs import SpecialWindow from .tabs import SpecialWindow
for t in ans.tabs: for t in ans.tabs:
if not t.windows: if not t.windows:
w: Optional[Watchers] = None t.windows.append(SpecialWindow(cmd=resolved_shell(opts), override_title=default_title))
if t.watchers.has_watchers:
w = t.watchers.copy()
t.windows.append(SpecialWindow(cmd=resolved_shell(opts), watchers=w, override_title=default_title))
return ans return ans
ans = Session(default_title) ans = Session(default_title)
@ -133,9 +112,7 @@ def parse_session(raw: str, opts: Options, default_title: Optional[str] = None)
ans.add_tab(opts, rest) ans.add_tab(opts, rest)
elif cmd == 'new_os_window': elif cmd == 'new_os_window':
yield finalize_session(ans) yield finalize_session(ans)
wt = ans.watchers
ans = Session(default_title) ans = Session(default_title)
ans.watchers = wt.copy()
ans.add_tab(opts, rest) ans.add_tab(opts, rest)
elif cmd == 'layout': elif cmd == 'layout':
ans.set_layout(rest) ans.set_layout(rest)
@ -155,14 +132,6 @@ def parse_session(raw: str, opts: Options, default_title: Optional[str] = None)
ans.os_window_size = WindowSizes(WindowSize(*w), WindowSize(*h)) ans.os_window_size = WindowSizes(WindowSize(*w), WindowSize(*h))
elif cmd == 'os_window_class': elif cmd == 'os_window_class':
ans.os_window_class = rest 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: else:
raise ValueError('Unknown command in session file: {}'.format(cmd)) raise ValueError('Unknown command in session file: {}'.format(cmd))
yield finalize_session(ans) yield finalize_session(ans)
@ -194,7 +163,8 @@ def create_sessions(
else: else:
yield from parse_session(session_data, opts, getattr(args, 'title', None)) yield from parse_session(session_data, opts, getattr(args, 'title', None))
return return
ans = Session() default_watchers = args.watcher if args else ()
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

@ -137,7 +137,11 @@ class Tab: # {{{
def startup(self, session_tab: 'SessionTab') -> None: def startup(self, session_tab: 'SessionTab') -> None:
for cmd in session_tab.windows: for cmd in session_tab.windows:
if isinstance(cmd, SpecialWindowInstance):
self.new_special_window(cmd) self.new_special_window(cmd)
else:
from .launch import launch
launch(get_boss(), cmd.opts, cmd.args, target_tab=self, force_target_tab=True)
self.windows.set_active_window_group_for(self.windows.all_windows[session_tab.active_window_idx]) self.windows.set_active_window_group_for(self.windows.all_windows[session_tab.active_window_idx])
def serialize_state(self) -> Dict[str, Any]: def serialize_state(self) -> Dict[str, Any]: