From 8a1f4bda3b8c349853c56c6e44b75dd2a60687de Mon Sep 17 00:00:00 2001 From: Arsenii Kvachan Date: Wed, 12 Nov 2025 17:37:13 +0100 Subject: [PATCH] Allow browsing a directory with sessions - interpret a directory argument by listing only *.kitty-session and similar files - reuse the existing sorting logic for the directory chooser and document the workflow --- docs/sessions.rst | 7 +++++ kitty/session.py | 69 ++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/docs/sessions.rst b/docs/sessions.rst index b173dee5a..a1be3bc39 100644 --- a/docs/sessions.rst +++ b/docs/sessions.rst @@ -57,12 +57,19 @@ easily swap between them, kitty has you covered. You can use the map f7>/ goto_session # Same as above, but the sessions are listed alphabetically instead of by most recent map f7>/ goto_session --sort-by=alphabetical + # Browse session files inside a directory and pick one + map f7>p goto_session ~/.local/share/kitty/sessions # Go to the previously active session (larger negative numbers jump further back in history) map f7>- goto_session -1 In this manner you can define as many projects/sessions as you like and easily switch between them with a keypress. +When a directory path is supplied to :ac:`goto_session`, kitty scans it for +files ending in ``.kitty-session``, ``.kitty_session`` or ``.session`` and +presents an interactive list. The ``--sort-by`` option controls the ordering of that list just like it does +for globally known sessions. + You can also close sessions using the :ac:`close_session` action, which closes all windows in the session with a single keypress. diff --git a/kitty/session.py b/kitty/session.py index 4922f352c..031502411 100644 --- a/kitty/session.py +++ b/kitty/session.py @@ -196,15 +196,30 @@ class Session: self.tabs[-1].cwd = session_base_dir +SESSION_FILE_EXTENSIONS = {'session', 'kitty-session', 'kitty_session'} + + +def has_session_extension(path: str) -> bool: + name = os.path.basename(path) + return name.rpartition('.')[2] in SESSION_FILE_EXTENSIONS + + def session_arg_to_name(session_arg: str) -> str: if session_arg in ('-', '/dev/stdin', 'none'): session_arg = '' session_name = os.path.basename(session_arg) - if session_name.rpartition('.')[2] in ('session', 'kitty-session', 'kitty_session'): + if has_session_extension(session_name): session_name = session_name.rpartition('.')[0] return session_name +def resolve_session_arg_path(path: str) -> str: + path = os.path.expanduser(path) + if not os.path.isabs(path): + path = os.path.join(config_dir, path) + return os.path.abspath(path) + + def parse_session( raw: str, opts: Options, environ: Mapping[str, str] | None = None, session_arg: str = '', session_path: str = '' ) -> Generator[Session, None, None]: @@ -424,10 +439,7 @@ def switch_to_session(boss: BossType, session_name: str) -> bool: def resolve_session_path_and_name(path: str) -> tuple[str, str]: - path = os.path.expanduser(path) - if not os.path.isabs(path): - path = os.path.join(config_dir, path) - path = os.path.abspath(path) + path = resolve_session_arg_path(path) return path, session_arg_to_name(path) @@ -503,8 +515,11 @@ def close_session_with_confirm(boss: BossType, cmdline: Sequence[str]) -> None: do_close(True) -def choose_session(boss: BossType, opts: GotoSessionOptions) -> None: - all_known_sessions = get_all_known_sessions() +def choose_session_from_map( + boss: BossType, opts: GotoSessionOptions, session_map: Mapping[str, str], title: str +) -> bool: + if not session_map: + return False hmap = {n: len(goto_session_history)-i for i, n in enumerate(goto_session_history)} if opts.sort_by == 'alphabetical': def skey(name: str) -> tuple[int, str]: @@ -512,13 +527,39 @@ def choose_session(boss: BossType, opts: GotoSessionOptions) -> None: else: def skey(name: str) -> tuple[int, str]: return hmap.get(name, len(goto_session_history)), name.lower() - names = sorted(all_known_sessions, key=skey) + names = sorted(session_map, key=skey) def chosen(name: str | None) -> None: if name: - goto_session(boss, (all_known_sessions[name],)) - boss.choose_entry( - _('Select a session to activate'), ((name, name) for name in names), chosen) + goto_session(boss, (session_map[name],)) + boss.choose_entry(title, ((name, name) for name in names), chosen) + return True + + +def choose_session(boss: BossType, opts: GotoSessionOptions) -> None: + all_known_sessions = get_all_known_sessions() + choose_session_from_map(boss, opts, all_known_sessions, _('Select a session to activate')) + + +def choose_session_in_directory(boss: BossType, opts: GotoSessionOptions, directory_path: str) -> None: + try: + with os.scandir(directory_path) as entries: + session_map = { + session_arg_to_name(entry.path): entry.path + for entry in entries + if entry.is_file() and has_session_extension(entry.name) + } + except OSError as e: + boss.show_error( + _('Failed to list sessions'), + _('Could not list session files in {0} with error: {1}').format(directory_path, e)) + return + session_map = {name: path for name, path in session_map.items() if name} + if not choose_session_from_map( + boss, opts, session_map, _('Select a session to activate from {0}').format(directory_path) + ): + boss.show_error( + _('No session files found'), _('No session files were found inside {0}').format(directory_path)) def parse_goto_session_cmdline(args: list[str]) -> tuple[GotoSessionOptions, list[str]]: @@ -571,7 +612,11 @@ def goto_session(boss: BossType, cmdline: Sequence[str]) -> None: idx = 0 if idx < 0: return goto_previous_session(boss, idx) - path, session_name = resolve_session_path_and_name(path) + resolved_path = resolve_session_arg_path(path) + if os.path.isdir(resolved_path): + choose_session_in_directory(boss, opts, resolved_path) + return + path, session_name = resolve_session_path_and_name(resolved_path) if not session_name: boss.show_error(_('Invalid session'), _('{} is not a valid path for a session').format(path)) return