diff --git a/docs/changelog.rst b/docs/changelog.rst index b1ccd6935..c2420ad0c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -57,6 +57,8 @@ Detailed list of changes - Allow using the cwd of the original process for :option:`launch --cwd` (:iss:`5672`) +- Session files: Expand environment variables (:disc:`5917`) + - Pass key events mapped to scroll actions to the program running in the terminal when the terminal is in alternate screen mode (:iss:`5839`) - Implement :ref:`edit-in-kitty ` using the new ``kitten`` static executable (:iss:`5546`, :iss:`5630`) diff --git a/docs/overview.rst b/docs/overview.rst index 0577460f3..55554d214 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -173,6 +173,11 @@ option in :file:`kitty.conf`. An example, showing all available commands: The :doc:`launch ` command when used in a session file cannot create new OS windows, or tabs. +.. note:: + Environment variables of the for :code:`${NAME}` or :code:`$NAME` are + expanded in the session file, except in the *arguments* (not options) to the + launch command. + Creating tabs/windows ------------------------------- diff --git a/kitty/boss.py b/kitty/boss.py index cb6083163..49f028059 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -760,9 +760,11 @@ class Boss: return None args.args = rest opts = create_opts(args) - if args.session == '-': + if data['session_data']: from .session import PreReadSession - args.session = PreReadSession(data['stdin']) + args.session = PreReadSession(data['session_data'], data['environ']) + else: + args.session = '' if not os.path.isabs(args.directory): args.directory = os.path.join(data['cwd'], args.directory) focused_os_window = os_window_id = 0 diff --git a/kitty/cli.py b/kitty/cli.py index e14254c86..117a5b416 100644 --- a/kitty/cli.py +++ b/kitty/cli.py @@ -890,8 +890,8 @@ Detach from the controlling terminal, if any. completion=type:file ext:session relative:conf group:"Session files" Path to a file containing the startup :italic:`session` (tabs, windows, layout, programs). Use - to read from STDIN. See the :file:`README` file for details and -an example. Environment variables are expanded, relative paths are resolved relative -to the kitty configuration directory. +an example. Environment variables in the file name are expanded, +relative paths are resolved relative to the kitty configuration directory. --hold diff --git a/kitty/main.py b/kitty/main.py index 753290398..93ac4c9c0 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -80,12 +80,16 @@ def set_custom_ibeam_cursor() -> None: def talk_to_instance(args: CLIOptions) -> None: import json import socket - stdin = '' + session_data = '' if args.session == '-': - stdin = sys.stdin.read() + session_data = sys.stdin.read() + elif args.session: + with open(args.session) as f: + session_data = f.read() + data = {'cmd': 'new_instance', 'args': tuple(sys.argv), 'cmdline_args_for_open': getattr(sys, 'cmdline_args_for_open', []), 'startup_id': os.environ.get('DESKTOP_STARTUP_ID'), 'activation_token': os.environ.get('XDG_ACTIVATION_TOKEN'), - 'cwd': os.getcwd(), 'stdin': stdin} + 'cwd': os.getcwd(), 'session_data': session_data, 'environ': dict(os.environ)} notify_socket = None if args.wait_for_single_instance_window_close: address = f'\0{appname}-os-window-close-notify-{os.getpid()}-{os.geteuid()}' @@ -461,7 +465,7 @@ def _main() -> None: if cli_opts.detach: if cli_opts.session == '-': from .session import PreReadSession - cli_opts.session = PreReadSession(sys.stdin.read()) + cli_opts.session = PreReadSession(sys.stdin.read(), os.environ) detach() if cli_opts.replay_commands: from kitty.client import main as client_main diff --git a/kitty/session.py b/kitty/session.py index f1f4a25a6..586ed3d7d 100644 --- a/kitty/session.py +++ b/kitty/session.py @@ -1,9 +1,12 @@ #!/usr/bin/env python3 # License: GPL v3 Copyright: 2016, Kovid Goyal +import os import shlex import sys -from typing import TYPE_CHECKING, Generator, Iterator, List, Optional, Tuple, Union +from contextlib import suppress +from functools import partial +from typing import TYPE_CHECKING, Callable, Generator, Iterator, List, Mapping, Optional, Tuple, Union from .cli_stub import CLIOptions from .constants import kitten_exe @@ -12,7 +15,7 @@ from .options.types import Options from .options.utils import resize_window, to_layout_names, window_size from .os_window_size import WindowSize, WindowSizeData, WindowSizes from .typing import SpecialWindowInstance -from .utils import log_error, resolve_custom_file, resolved_shell +from .utils import expandvars, log_error, resolve_custom_file, resolved_shell if TYPE_CHECKING: from .launch import LaunchSpec @@ -73,11 +76,22 @@ class Session: raise ValueError(f'{val} is not a valid layout') 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]], expand: Callable[[str], str] = lambda x: x) -> None: from .launch import parse_launch_args + needs_expandvars = False if isinstance(cmd, str): + needs_expandvars = True cmd = shlex.split(cmd) spec = parse_launch_args(cmd) + if needs_expandvars: + assert isinstance(cmd, list) + limit = len(cmd) + if len(spec.args): + with suppress(ValueError): + limit = cmd.index(spec.args[0]) + cmd = [(expand(x) if i < limit else x) for i, x in enumerate(cmd)] + spec = parse_launch_args(cmd) + t = self.tabs[-1] if t.next_title and not spec.opts.window_title: spec.opts.window_title = t.next_title @@ -113,7 +127,7 @@ class Session: self.tabs[-1].cwd = val -def parse_session(raw: str, opts: Options) -> Generator[Session, None, None]: +def parse_session(raw: str, opts: Options, environ: Optional[Mapping[str, str]] = None) -> Generator[Session, None, None]: def finalize_session(ans: Session) -> Session: from .tabs import SpecialWindow @@ -122,6 +136,9 @@ def parse_session(raw: str, opts: Options) -> Generator[Session, None, None]: t.windows.append(WindowSpec(SpecialWindow(cmd=resolved_shell(opts)))) return ans + if environ is None: + environ = os.environ + expand = partial(expandvars, env=environ, fallback_to_os_env=False) ans = Session() ans.add_tab(opts) for line in raw.splitlines(): @@ -133,6 +150,8 @@ def parse_session(raw: str, opts: Options) -> Generator[Session, None, None]: else: cmd, rest = parts cmd, rest = cmd.strip(), rest.strip() + if cmd != 'launch': + rest = expand(rest) if cmd == 'new_tab': ans.add_tab(opts, rest) elif cmd == 'new_os_window': @@ -142,7 +161,7 @@ def parse_session(raw: str, opts: Options) -> Generator[Session, None, None]: elif cmd == 'layout': ans.set_layout(rest) elif cmd == 'launch': - ans.add_window(rest) + ans.add_window(rest, expand) elif cmd == 'focus': ans.focus() elif cmd == 'focus_os_window': @@ -167,9 +186,10 @@ def parse_session(raw: str, opts: Options) -> Generator[Session, None, None]: class PreReadSession(str): - def __new__(cls, val: str) -> 'PreReadSession': + def __new__(cls, val: str, associated_environ: Mapping[str, str]) -> 'PreReadSession': ans: PreReadSession = str.__new__(cls, val) ans.pre_read = True # type: ignore + ans.associated_environ = associated_environ # type: ignore return ans @@ -179,11 +199,13 @@ def create_sessions( special_window: Optional['SpecialWindowInstance'] = None, cwd_from: Optional['CwdRequest'] = None, respect_cwd: bool = False, - default_session: Optional[str] = None + default_session: Optional[str] = None, ) -> Iterator[Session]: if args and args.session: + environ: Optional[Mapping[str, str]] = None if isinstance(args.session, PreReadSession): session_data = '' + str(args.session) + environ = args.session.associated_environ # type: ignore else: if args.session == '-': f = sys.stdin @@ -191,7 +213,7 @@ def create_sessions( f = open(resolve_custom_file(args.session)) with f: session_data = f.read() - yield from parse_session(session_data, opts) + yield from parse_session(session_data, opts, environ=environ) return if default_session and default_session != 'none': try: