diff --git a/docs/changelog.rst b/docs/changelog.rst index 2c9caf9aa..9f7d05088 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,6 +11,11 @@ To update |kitty|, :doc:`follow the instructions `. used by full screen terminal programs and even games, see :doc:`keyboard-protocol` (:iss:`3248`) +- **Backward incompatibility**: Session files now use the full :doc:`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`) - ssh kitten: Allow using python instead of the shell on the server, useful if diff --git a/docs/index.rst b/docs/index.rst index 4373ca226..926e0c1a1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -340,7 +340,7 @@ For example: launch zsh # Create a window with some environment variables set and run # vim in it - launch env FOO=BAR vim + launch --env FOO=BAR vim # Set the title for the next window title Chat with x launch irssi --profile x @@ -367,14 +367,9 @@ 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 +.. note:: + The :doc:`launch ` command when used in a session file + cannot create new OS windows, or tabs. Mouse features diff --git a/kitty/boss.py b/kitty/boss.py index bc83d173c..5514470a5 100755 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -177,12 +177,8 @@ 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': diff --git a/kitty/launch.py b/kitty/launch.py index 2b4d500d8..532652a4b 100644 --- a/kitty/launch.py +++ b/kitty/launch.py @@ -4,7 +4,7 @@ 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 .child import Child @@ -13,7 +13,7 @@ from .cli_stub import LaunchCLIOptions from .constants import resolve_custom_file from .fast_data_types import set_clipboard_string 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 try: @@ -22,6 +22,11 @@ except ImportError: TypedDict = Dict[str, Any] +class LaunchSpec(NamedTuple): + opts: LaunchCLIOptions + args: List[str] + + @lru_cache(maxsize=2) def options_spec() -> str: return ''' @@ -159,13 +164,13 @@ Set the WM_NAME property on X11 for the newly created OS Window when using ''' + 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 ()) try: opts, args = parse_args(result_class=LaunchCLIOptions, args=args, ospec=options_spec) except SystemExit as e: raise ValueError from e - return opts, args + return LaunchSpec(opts, args) def get_env(opts: LaunchCLIOptions, active_child: Child) -> Dict[str, str]: @@ -234,7 +239,13 @@ class LaunchKwds(TypedDict): 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_child = getattr(active, 'child', None) env = get_env(opts, active_child) @@ -311,7 +322,10 @@ def launch(boss: Boss, opts: LaunchCLIOptions, args: List[str], target_tab: Opti else: set_primary_selection(stdin) else: - tab = tab_for_window(boss, opts, target_tab) + if force_target_tab: + tab = target_tab + else: + tab = tab_for_window(boss, opts, target_tab) if tab is not None: watchers = load_watch_modules(opts.watcher) new_window: Window = tab.new_window(env=env or None, watchers=watchers or None, **kw) diff --git a/kitty/session.py b/kitty/session.py index e2bca8a2d..1f2ab8fc7 100644 --- a/kitty/session.py +++ b/kitty/session.py @@ -4,7 +4,7 @@ import shlex import sys -from typing import Generator, List, Optional, Union +from typing import Generator, List, Optional, Sequence, Union from .cli_stub import CLIOptions 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 .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: @@ -27,44 +26,31 @@ def get_os_window_sizing_data(opts: Options, session: Optional['Session'] = None class Tab: - def __init__(self, opts: Options, name: str, watchers: Watchers): - self.windows: List['SpecialWindowInstance'] = [] + def __init__(self, opts: Options, name: str): + from .launch import LaunchSpec + self.windows: List[Union[LaunchSpec, '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: - 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.default_watchers = list(default_watchers) self.active_tab_idx = 0 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] - if self.tabs: - self.tabs[-1].watchers = self.watchers.copy() - self.tabs.append(Tab(opts, name, self.watchers)) + self.tabs.append(Tab(opts, name)) def set_next_title(self, title: str) -> None: self.tabs[-1].next_title = title.strip() @@ -75,22 +61,18 @@ class Session: self.tabs[-1].layout = val def add_window(self, cmd: Union[None, str, List[str]]) -> None: - if cmd: - cmd = shlex.split(cmd) if isinstance(cmd, str) else cmd - else: - cmd = None - from .tabs import SpecialWindow + from .launch import parse_launch_args + 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] - 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) + spec.opts.cwd = spec.opts.cwd or t.cwd + t.windows.append(spec) 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: @@ -112,10 +94,7 @@ def parse_session(raw: str, opts: Options, default_title: Optional[str] = None) from .tabs import SpecialWindow for t in ans.tabs: if not t.windows: - w: Optional[Watchers] = None - if t.watchers.has_watchers: - w = t.watchers.copy() - t.windows.append(SpecialWindow(cmd=resolved_shell(opts), watchers=w, override_title=default_title)) + t.windows.append(SpecialWindow(cmd=resolved_shell(opts), override_title=default_title)) return ans 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) 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) @@ -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)) 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) @@ -194,7 +163,8 @@ def create_sessions( else: yield from parse_session(session_data, opts, getattr(args, 'title', None)) 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' ans.add_tab(opts) ans.tabs[-1].layout = current_layout diff --git a/kitty/tabs.py b/kitty/tabs.py index 98db16f0d..71b8fee82 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -137,7 +137,11 @@ class Tab: # {{{ def startup(self, session_tab: 'SessionTab') -> None: for cmd in session_tab.windows: - self.new_special_window(cmd) + if isinstance(cmd, SpecialWindowInstance): + 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]) def serialize_state(self) -> Dict[str, Any]: