From 24e0bbd50e45ccad0d6b53e849e9990d191eeba0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 16 Aug 2025 06:32:42 +0530 Subject: [PATCH] Add save_as_session action Docs need to be updated. --- kitty/boss.py | 45 +++++++++++++++++++++++++++++++++++++++--- kitty/cli_stub.py | 5 ++++- kitty/entry_points.py | 30 ---------------------------- kitty/options/utils.py | 2 +- kitty/session.py | 35 +++++++++++++++++++++++++++++++- 5 files changed, 81 insertions(+), 36 deletions(-) diff --git a/kitty/boss.py b/kitty/boss.py index 7a584eb84..ca33512f5 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -122,7 +122,7 @@ from .notifications import NotificationManager from .options.types import Options, nullable_colors from .options.utils import MINIMUM_FONT_SIZE, KeyboardMode, KeyDefinition from .os_window_size import initial_window_size_func -from .session import Session, create_sessions, get_os_window_sizing_data, goto_session +from .session import Session, create_sessions, get_os_window_sizing_data, goto_session, save_as_session from .shaders import load_shader_programs from .simple_cli_definitions import grab_keyboard_docs from .tabs import SpecialWindow, SpecialWindowInstance, Tab, TabDict, TabManager @@ -1188,6 +1188,29 @@ class Boss: 'ask', cmd, window=window, custom_callback=callback_, default_data={'response': ''}, action_on_removal=on_popup_overlay_removal ) + def get_save_filepath( + self, msg: str, # can contain newlines and ANSI formatting + callback: Callable[..., None], # called with the answer or empty string when aborted + window: Window | None = None, # the window associated with the confirmation + prompt: str = '> ', + initial_value: str = '' + ) -> None: + result: str = '' + + def callback_(res: dict[str, Any], x: int, boss: Boss) -> None: + nonlocal result + result = res.get('response') or '' + + def on_popup_overlay_removal(wid: int, boss: Boss) -> None: + callback(result) + + cmd = ['--type', 'file', '--message', msg, '--prompt', prompt] + if initial_value: + cmd.append('--default=' + initial_value) + self.run_kitten_with_metadata( + 'ask', cmd, window=window, custom_callback=callback_, default_data={'response': ''}, action_on_removal=on_popup_overlay_removal + ) + def confirm_tab_close(self, tab: Tab) -> None: msg, num_active_windows = self.close_windows_with_confirmation_msg(tab, tab.active_window) x = get_options().confirm_os_window_close[0] @@ -1970,8 +1993,20 @@ class Boss: @ac('misc', 'Edit the kitty.conf config file in your favorite text editor') def edit_config_file(self, *a: Any) -> None: confpath = prepare_config_file_for_editing() - cmd = [kitty_exe(), '+edit'] + get_editor(get_options()) + [confpath] - self.new_os_window(*cmd) + self.edit_file(confpath) + + def edit_file(self, path: str) -> None: + editor_cmd = get_editor(get_options()) + exe = editor_cmd[0] + if not os.path.isabs(exe): + exe = which(exe) or '' + if not exe or not os.access(exe, os.X_OK): + self.show_error(_('Cannot find editor'), _( + 'Could not edit the file {0} because the editor {1} was not found.').format(editor_cmd[0])) + return + editor_cmd[0] = exe + path = os.path.abspath(os.path.expanduser(path)) + self.new_os_window(*editor_cmd, path) def run_kitten_with_metadata( self, @@ -2996,6 +3031,10 @@ class Boss: def goto_session(self, *cmdline: str) -> None: goto_session(self, cmdline) + @ac('misc', 'Save the current kitty state as a session file') + def save_as_session(self, *cmdline: str) -> None: + save_as_session(self, cmdline) + @ac('tab', 'Interactively select a tab to switch to') def select_tab(self) -> None: diff --git a/kitty/cli_stub.py b/kitty/cli_stub.py index 5944b8555..2a528aee6 100644 --- a/kitty/cli_stub.py +++ b/kitty/cli_stub.py @@ -13,7 +13,7 @@ class CLIOptions: LaunchCLIOptions = AskCLIOptions = ClipboardCLIOptions = DiffCLIOptions = CLIOptions HintsCLIOptions = IcatCLIOptions = PanelCLIOptions = ResizeCLIOptions = CLIOptions ErrorCLIOptions = UnicodeCLIOptions = RCOptions = RemoteFileCLIOptions = CLIOptions -BroadcastCLIOptions = ShowKeyCLIOptions = CLIOptions +BroadcastCLIOptions = ShowKeyCLIOptions = SaveAsSessionOptions = CLIOptions ThemesCLIOptions = TransferCLIOptions = LoadConfigRCOptions = ActionRCOptions = CLIOptions @@ -74,6 +74,9 @@ def generate_stub() -> None: from kittens.transfer.main import option_text do(option_text(), 'TransferCLIOptions') + from kitty.session import save_as_session_options + do(save_as_session_options(), 'SaveAsSessionOptions') + from kitty.rc.base import all_command_names, command_for_name for cmd_name in all_command_names(): cmd = command_for_name(cmd_name) diff --git a/kitty/entry_points.py b/kitty/entry_points.py index a9f3241ed..d18d05132 100644 --- a/kitty/entry_points.py +++ b/kitty/entry_points.py @@ -72,26 +72,6 @@ def launch(args: list[str]) -> None: runpy.run_path(exe, run_name='__main__') -def edit(args: list[str]) -> None: - import shutil - - from .constants import is_macos - if is_macos: - # On macOS vim fails to handle SIGWINCH if it occurs early, so add a small delay. - import time - time.sleep(0.05) - exe = args[1] - if not os.path.isabs(exe): - exe = shutil.which(exe) or '' - if not exe or not os.access(exe, os.X_OK): - print('Cannot find an editor on your system. Set the \x1b[33meditor\x1b[39m value in kitty.conf' - ' to the absolute path of your editor of choice.', file=sys.stderr) - from kitty.utils import hold_till_enter - hold_till_enter() - raise SystemExit(1) - os.execv(exe, args[1:]) - - def shebang(args: list[str]) -> None: from kitty.constants import kitten_exe os.execvp(kitten_exe(), ['kitten', '__shebang__', 'confirm-if-needed'] + args[1:]) @@ -109,14 +89,6 @@ def run_kitten(args: list[str]) -> None: rk(kitten) -def edit_config_file(args: list[str]) -> None: - from kitty.cli import create_default_opts - from kitty.fast_data_types import set_options - from kitty.utils import edit_config_file as f - set_options(create_default_opts()) - f() - - def namespaced(args: list[str]) -> None: try: func = namespaced_entry_points[args[1]] @@ -144,9 +116,7 @@ namespaced_entry_points['runpy'] = runpy namespaced_entry_points['launch'] = launch namespaced_entry_points['open'] = open_urls namespaced_entry_points['kitten'] = run_kitten -namespaced_entry_points['edit-config'] = edit_config_file namespaced_entry_points['shebang'] = shebang -namespaced_entry_points['edit'] = edit def setup_openssl_environment(ext_dir: str) -> None: diff --git a/kitty/options/utils.py b/kitty/options/utils.py index 516921925..2197f01e8 100644 --- a/kitty/options/utils.py +++ b/kitty/options/utils.py @@ -77,7 +77,7 @@ class InvalidMods(ValueError): @func_with_args( 'pass_selection_to_program', 'new_window', 'new_tab', 'new_os_window', 'new_window_with_cwd', 'new_tab_with_cwd', 'new_os_window_with_cwd', - 'launch', 'mouse_handle_click', 'show_error', 'goto_session', + 'launch', 'mouse_handle_click', 'show_error', 'goto_session', 'save_as_session', ) def shlex_parse(func: str, rest: str) -> FuncArgsType: return func, to_cmdline(rest) diff --git a/kitty/session.py b/kitty/session.py index 4252b1193..f534a0adc 100644 --- a/kitty/session.py +++ b/kitty/session.py @@ -11,7 +11,7 @@ from functools import partial from gettext import gettext as _ from typing import TYPE_CHECKING, Any, Optional, Sequence, Union -from .cli_stub import CLIOptions +from .cli_stub import CLIOptions, SaveAsSessionOptions from .constants import config_dir from .fast_data_types import get_options from .layout.interface import all_layouts @@ -446,3 +446,36 @@ def goto_session(boss: BossType, cmdline: Sequence[str]) -> None: boss.show_error(_('Failed to create session'), _('Could not create session from {0} with error:\n{1}').format(path, tb)) else: append_to_session_history(session_name) + + +def save_as_session_options() -> str: + return ''' +--save-only +type=bool-set +Only save the specified session file, dont open it in an editor to review after saving. +''' + + +def save_as_session_part2(boss: BossType, opts: SaveAsSessionOptions, path: str) -> None: + if not path: + return + from .config import atomic_save + path = os.path.abspath(os.path.expanduser(path)) + session = '\n'.join(boss.serialize_state_as_session()) + atomic_save(session.encode(), path) + if not opts.save_only: + boss.edit_file(path) + + +def save_as_session(boss: BossType, cmdline: Sequence[str]) -> None: + from kitty.cli import parse_args + opts: SaveAsSessionOptions + opts, args = parse_args( + list(cmdline), save_as_session_options, result_class=SaveAsSessionOptions) + path = args[0] if args else '' + if path: + save_as_session_part2(boss, opts, path) + else: + boss.get_save_filepath(_( + 'Enter the path at which to save the session, usually session files are given the .kitty-session file extension'), + partial(save_as_session_part2, boss, opts))