From 67b2e859f5ab07bf55616b384d93158ec0f029b0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 17 Aug 2025 11:23:36 +0530 Subject: [PATCH] Allow using relative paths in cd in sessions files that get resolved relative to the directory containing the session file --- kitty/boss.py | 2 +- kitty/launcher/single-instance.c | 13 +++++++++++++ kitty/main.py | 2 +- kitty/session.py | 33 +++++++++++++++++++++++++------- kitty/types.py | 1 + 5 files changed, 42 insertions(+), 9 deletions(-) diff --git a/kitty/boss.py b/kitty/boss.py index e79d51f72..a3a768703 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -885,7 +885,7 @@ class Boss: args.session = 'none' else: from .session import PreReadSession - args.session = PreReadSession(data['session_data'], data['environ'], data['session_arg']) + args.session = PreReadSession(data['session_data'], data['environ'], data['session_arg'], data['session_path']) else: args.session = '' if not os.path.isabs(args.directory): diff --git a/kitty/launcher/single-instance.c b/kitty/launcher/single-instance.c index 4c57482f7..ae42371e6 100644 --- a/kitty/launcher/single-instance.c +++ b/kitty/launcher/single-instance.c @@ -19,6 +19,7 @@ #include #include #include +#include #define CHARSETS_STORAGE static inline #define NO_SINGLE_BYTE_CHARSETS @@ -241,15 +242,23 @@ static void talk_to_instance(int s, struct sockaddr_un *server_addr, int argc, char *argv[], const CLIOptions *opts) { cleanup_entries.si.path2[0] = 0; cleanup_entries.si.path1[0] = 0; membuf session_data = {0}; + char *session_path = NULL; if (opts->session && opts->session[0]) { if (strcmp(opts->session, "none") == 0) { session_data.data = "none"; session_data.used = 4; } else if (strcmp(opts->session, "-") == 0) { read_till_eof(stdin, &session_data); + session_path = malloc(PATH_MAX + 8); + if (!session_path) fail_on_errno("Failed to alloc space for session_path"); + if (getcwd(session_path, PATH_MAX + 1) == NULL) fail_on_errno("Failed to getcwd()"); + char *p = session_path + strlen(session_path); + *(p++) = '/'; *(p++) = '-'; *p = 0; } else { FILE *f = safe_fopen(opts->session, "r"); if (f == NULL) fail_on_errno("Failed to open session file for reading"); read_till_eof(f, &session_data); + session_path = realpath(opts->session, NULL); + if (session_path == NULL) fail_on_errno("Failed to call realpath() on session file"); } } membuf output = {0}; @@ -260,6 +269,10 @@ talk_to_instance(int s, struct sockaddr_un *server_addr, int argc, char *argv[], w(",\"session_arg\":"); if (opts->session && opts->session[0]) write_json_string(&output, opts->session, strlen(opts->session)); else write_json_string(&output, "", 0); + w(",\"session_path\":"); + if (session_path && session_path[0]) write_json_string(&output, session_path, strlen(session_path)); + else write_json_string(&output, "", 0); + free(session_path); w(",\"args\":"); write_json_string_array(&output, argc, argv); char cwd[4096]; if (!getcwd(cwd, sizeof(cwd))) fail_on_errno("Failed to get cwd"); diff --git a/kitty/main.py b/kitty/main.py index f5107070d..784c83d37 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -546,7 +546,7 @@ def kitty_main(called_from_panel: bool = False) -> None: if cli_opts.detach: if cli_opts.session == '-': from .session import PreReadSession - cli_opts.session = PreReadSession(sys.stdin.read(), os.environ, '-') + cli_opts.session = PreReadSession(sys.stdin.read(), os.environ, '-', os.path.join(os.getcwd(), '-')) if cli_opts.replay_commands: from kitty.client import main as client_main client_main(cli_opts.replay_commands) diff --git a/kitty/session.py b/kitty/session.py index 4cd8688df..b062af40c 100644 --- a/kitty/session.py +++ b/kitty/session.py @@ -177,8 +177,14 @@ class Session: if self.tabs[-1].layout not in self.tabs[-1].enabled_layouts: self.tabs[-1].layout = self.tabs[-1].enabled_layouts[0] - def set_cwd(self, val: str) -> None: - self.tabs[-1].cwd = val + def set_cwd(self, val: str, session_base_dir: str) -> None: + if val: + val = os.path.expanduser(val) + if not os.path.isabs(val): + val = os.path.abspath(os.path.join(session_base_dir, val)) + self.tabs[-1].cwd = val + elif session_base_dir: + self.tabs[-1].cwd = session_base_dir def session_arg_to_name(session_arg: str) -> str: @@ -191,8 +197,15 @@ def session_arg_to_name(session_arg: str) -> str: -def parse_session(raw: str, opts: Options, environ: Mapping[str, str] | None = None, session_arg: str = '') -> Generator[Session, None, None]: +def parse_session( + raw: str, opts: Options, environ: Mapping[str, str] | None = None, session_arg: str = '', session_path: str = '' +) -> Generator[Session, None, None]: session_name = session_arg_to_name(session_arg) + if session_path: + session_base_dir = os.path.dirname(os.path.abspath(session_path)) + else: + session_base_dir = os.getcwd() + def finalize_session(ans: Session) -> Session: ans.session_name = session_name ans.num_of_windows_in_definition = sum(len(t.windows) for t in ans.tabs) @@ -235,7 +248,7 @@ def parse_session(raw: str, opts: Options, environ: Mapping[str, str] | None = N elif cmd == 'enabled_layouts': ans.set_enabled_layouts(rest) elif cmd == 'cd': - ans.set_cwd(rest) + ans.set_cwd(rest, session_base_dir) elif cmd == 'title': ans.set_next_title(rest) elif cmd == 'os_window_size': @@ -262,11 +275,13 @@ class PreReadSession(str): associated_environ: Mapping[str, str] session_arg: str + session_path: str - def __new__(cls, val: str, associated_environ: Mapping[str, str], session_arg: str) -> 'PreReadSession': + def __new__(cls, val: str, associated_environ: Mapping[str, str], session_arg: str, session_path: str) -> 'PreReadSession': ans: PreReadSession = str.__new__(cls, val) ans.associated_environ = associated_environ ans.session_arg = session_arg + ans.session_path = session_path return ans @@ -284,11 +299,13 @@ def create_sessions( default_session = "none" else: session_arg = args.session + session_path = '' environ: Mapping[str, str] | None = None if isinstance(args.session, PreReadSession): session_data = '' + str(args.session) environ = args.session.associated_environ session_arg = args.session.session_arg + session_path = args.session.session_path else: if args.session == '-': f = sys.stdin @@ -296,17 +313,19 @@ def create_sessions( f = open(resolve_custom_file(args.session)) with f: session_data = f.read() - yield from parse_session(session_data, opts, environ=environ, session_arg=session_arg) + session_path = f.name + yield from parse_session(session_data, opts, environ=environ, session_arg=session_arg, session_path=session_path) return if default_session and default_session != 'none' and not getattr(args, 'args', None): session_arg = session_arg_to_name(default_session) try: with open(default_session) as f: session_data = f.read() + session_path = f.name except OSError: log_error(f'Failed to read from session file, ignoring: {default_session}') else: - yield from parse_session(session_data, opts, session_arg=session_arg) + yield from parse_session(session_data, opts, session_arg=session_arg, session_path=session_path) return ans = Session() current_layout = opts.enabled_layouts[0] if opts.enabled_layouts else 'tall' diff --git a/kitty/types.py b/kitty/types.py index 21fd767a2..fe45fe0a6 100644 --- a/kitty/types.py +++ b/kitty/types.py @@ -19,6 +19,7 @@ class SingleInstanceData(TypedDict): cwd: str session_data: str session_arg: str + session_path: str environ: Mapping[str, str] notify_on_os_window_death: str | None