diff --git a/kitty/actions.py b/kitty/actions.py index 262750348..0bb992a52 100644 --- a/kitty/actions.py +++ b/kitty/actions.py @@ -7,7 +7,7 @@ from typing import Dict, List, NamedTuple from .boss import Boss from .tabs import Tab -from .types import run_once +from .types import run_once, ActionGroup, ActionSpec from .window import Window @@ -18,7 +18,7 @@ class Action(NamedTuple): long_help: str -groups = { +groups: Dict[ActionGroup, str] = { 'cp': 'Copy/paste', 'sc': 'Scrolling', 'win': 'Window management', @@ -34,29 +34,20 @@ group_title = groups.__getitem__ @run_once def get_all_actions() -> Dict[str, List[Action]]: ' test docstring ' - if not get_all_actions.__doc__: - raise RuntimeError( - 'This build of kitty does not have docstrings, which' - ' are needed for get_all_actions(). If you are using a' - ' kitty binary build, setup KITTY_DEVELOP_FROM' - ' as described here: https://sw.kovidgoyal.net/kitty/build/' - ) ans: Dict[str, List[Action]] = {} def is_action(x: object) -> bool: - doc = getattr(x, '__doc__', '') - return bool(doc and doc.strip().startswith('@ac:')) + return isinstance(getattr(x, 'action_spec', None), ActionSpec) def as_action(x: object) -> Action: - doc = inspect.cleandoc(x.__doc__ or '') + spec: ActionSpec = getattr(x, 'action_spec') + doc = inspect.cleandoc(spec.doc) lines = doc.splitlines() first = lines.pop(0) - parts = first.split(':', 2) - grp = parts[1].strip() - short_help = parts[2].strip() + short_help = first long_help = '\n'.join(lines).strip() - return Action(getattr(x, '__name__'), grp, short_help, long_help) + return Action(getattr(x, '__name__'), spec.group, short_help, long_help) seen = set() for cls in (Window, Tab, Boss): diff --git a/kitty/boss.py b/kitty/boss.py index 39de2c857..998067410 100755 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -50,7 +50,7 @@ from .session import Session, create_sessions, get_os_window_sizing_data from .tabs import ( SpecialWindow, SpecialWindowInstance, Tab, TabDict, TabManager ) -from .types import SingleKey +from .types import SingleKey, ac from .typing import PopenType, TypedDict from .utils import ( func_name, get_editor, get_new_os_window_size, get_primary_selection, @@ -333,8 +333,8 @@ class Boss: startup_session = next(create_sessions(get_options(), special_window=sw, cwd_from=cwd_from)) return self.add_os_window(startup_session) + @ac('win', 'New OS Window') def new_os_window(self, *args: str) -> None: - '@ac:win: New OS Window' self._new_os_window(args) @property @@ -343,8 +343,8 @@ class Boss: if t is not None: return t.active_window_for_cwd + @ac('win', 'New OS Window with the same working directory as the currently active window') def new_os_window_with_cwd(self, *args: str) -> None: - '@ac:win: New OS Window with the same working directory as the currently active window' w = self.active_window_for_cwd cwd_from = w.child.pid_for_cwd if w is not None else None self._new_os_window(args, cwd_from) @@ -380,16 +380,16 @@ class Boss: response = {'ok': False, 'error': 'Remote control is disabled. Add allow_remote_control to your kitty.conf'} return response - def remote_control(self, *args: str) -> None: - ''' - @ac:misc: Run a remote control command + @ac('misc', ''' + Run a remote control command For example:: map F1 remote_control set-spacing margin=30 See :ref:`rc_mapping` for details. - ''' + ''') + def remote_control(self, *args: str) -> None: from .rc.base import ( PayloadGetter, command_for_name, parse_subcommand_cli ) @@ -501,8 +501,8 @@ class Boss: if window: self.child_monitor.mark_for_close(window.id) + @ac('tab', 'Close the current tab') def close_tab(self, tab: Optional[Tab] = None) -> None: - '@ac:tab: Close the current tab' tab = tab or self.active_tab if tab: self.confirm_tab_close(tab) @@ -534,12 +534,12 @@ class Boss: for window in tab: self.close_window(window) + @ac('win', 'Toggle the fullscreen status of the active OS Window') def toggle_fullscreen(self, os_window_id: int = 0) -> None: - '@ac:win: Toggle the fullscreen status of the active OS Window' toggle_fullscreen(os_window_id) + @ac('win', 'Toggle the maximized status of the active OS Window') def toggle_maximized(self, os_window_id: int = 0) -> None: - '@ac:win: Toggle the maximized status of the active OS Window' toggle_maximized(os_window_id) def start(self, first_os_window_id: int) -> None: @@ -566,9 +566,8 @@ class Boss: if tm is not None: tm.resize() - def clear_terminal(self, action: str, only_active: bool) -> None: - ''' - @ac:misc: Clear the terminal + @ac('misc', ''' + Clear the terminal See :sc:`reset_terminal` for details. For example:: @@ -581,7 +580,8 @@ class Boss: # Scroll the contents of the screen into the scrollback map kitty_mod+f12 clear_terminal scroll active - ''' + ''') + def clear_terminal(self, action: str, only_active: bool) -> None: if only_active: windows = [] w = self.active_window @@ -615,12 +615,12 @@ class Boss: def set_font_size(self, new_size: float) -> None: # legacy self.change_font_size(True, None, new_size) - def change_font_size(self, all_windows: bool, increment_operation: Optional[str], amt: float) -> None: - ''' - @ac:win: Change the font size for the current or all OS Windows + @ac('win', ''' + Change the font size for the current or all OS Windows See :ref:`conf-kitty-shortcuts.fonts` for details. - ''' + ''') + def change_font_size(self, all_windows: bool, increment_operation: Optional[str], amt: float) -> None: def calc_new_size(old_size: float) -> float: new_size = old_size if amt == 0: @@ -676,16 +676,16 @@ class Boss: def _set_os_window_background_opacity(self, os_window_id: int, opacity: float) -> None: change_background_opacity(os_window_id, max(0.1, min(opacity, 1.0))) - def set_background_opacity(self, opacity: str) -> None: - ''' - @ac:win: Set the background opacity for the active OS Window + @ac('win', ''' + Set the background opacity for the active OS Window For example:: map f1 set_background_opacity +0.1 map f2 set_background_opacity -0.1 map f3 set_background_opacity 0.5 - ''' + ''') + def set_background_opacity(self, opacity: str) -> None: window = self.active_window if window is None or not opacity: return @@ -761,12 +761,12 @@ class Boss: if matched_action is not None: self.dispatch_action(matched_action) - def start_resizing_window(self) -> None: - ''' - @ac:win: Resize the active window interactively + @ac('win', ''' + Resize the active window interactively See :ref:`window_resizing` for details. - ''' + ''') + def start_resizing_window(self) -> None: w = self.active_window if w is None: return @@ -843,9 +843,8 @@ class Boss: return True return False - def combine(self, *actions: KeyAction) -> None: - ''' - @ac:misc: Combine multiple actions and map to a single keypress + @ac('misc', ''' + Combine multiple actions and map to a single keypress The syntax is:: @@ -854,7 +853,8 @@ class Boss: For example:: map kitty_mod+e combine : new_window : next_layout - ''' + ''') + def combine(self, *actions: KeyAction) -> None: for key_action in actions: self.dispatch_action(key_action) @@ -889,8 +889,8 @@ class Boss: text = '\n'.join(parse_uri_list(text)) w.paste(text) + @ac('win', 'Close the currently active OS Window') def close_os_window(self) -> None: - '@ac:win: Close the currently active OS Window' tm = self.active_tab_manager if tm is not None: self.confirm_os_window_close(tm.os_window_id) @@ -929,8 +929,8 @@ class Boss: if action is not None: action() + @ac('win', 'Quit, closing all windows') def quit(self, *args: Any) -> None: - '@ac:win: Quit, closing all windows' tm = self.active_tab num = 0 for q in self.os_window_map.values(): @@ -990,8 +990,8 @@ class Boss: copy_colors_from=self.active_window ) + @ac('misc', 'Edit the kitty.conf config file in your favorite text editor') def edit_config_file(self, *a: Any) -> None: - '@ac:misc: Edit the kitty.conf config file in your favorite text editor' confpath = prepare_config_file_for_editing() # On macOS vim fails to handle SIGWINCH if it occurs early, so add a # small delay. @@ -1081,8 +1081,8 @@ class Boss: overlay_window.action_on_removal = callback_wrapper return overlay_window + @ac('misc', 'Run the specified kitten. See :doc:`/kittens/custom` for details') def kitten(self, kitten: str, *args: str) -> None: - '@ac:misc: Run the specified kitten. See :doc:`/kittens/custom` for details' import shlex cmdline = args[0] if args else '' kargs = shlex.split(cmdline) if cmdline else [] @@ -1098,12 +1098,12 @@ class Boss: if data is not None: end_kitten(data, target_window_id, self) + @ac('misc', 'Input an arbitrary unicode character. See :doc:`/kittens/unicode-input` for details.') def input_unicode_character(self) -> None: - '@ac:misc: Input an arbitrary unicode character. See :doc:`/kittens/unicode-input` for details.' self._run_kitten('unicode_input') + @ac('tab', 'Change the title of the active tab') def set_tab_title(self) -> None: - '@ac:tab: Change the title of the active tab' tab = self.active_tab if tab: args = ['--name=tab-title', '--message', _('Enter the new title for this tab below.'), 'do_set_tab_title', str(tab.id)] @@ -1121,8 +1121,8 @@ class Boss: def show_error(self, title: str, msg: str) -> None: self._run_kitten('show_error', args=['--title', title], input_data=msg) + @ac('mk', 'Create a new marker') def create_marker(self) -> None: - '@ac:mk: Create a new marker' w = self.active_window if w: spec = None @@ -1145,8 +1145,8 @@ class Boss: ], custom_callback=done, action_on_removal=done2) + @ac('misc', 'Run the kitty shell to control kitty with commands') def kitty_shell(self, window_type: str = 'window') -> None: - '@ac:misc: Run the kitty shell to control kitty with commands' kw: Dict[str, Any] = {} cmd = [kitty_exe(), '@'] aw = self.active_window @@ -1194,8 +1194,8 @@ class Boss: if not found_action: open_url(url, program or get_options().open_url_with, cwd=cwd) + @ac('misc', 'Click a URL using the keyboard') def open_url_with_hints(self) -> None: - '@ac:misc: Click a URL using the keyboard' self._run_kitten('hints') def drain_actions(self, actions: List) -> None: @@ -1223,8 +1223,8 @@ class Boss: if w is not None: w.paste(text) + @ac('cp', 'Paste from the clipboard to the active window') def paste_from_clipboard(self) -> None: - '@ac:cp: Paste from the clipboard to the active window' text = get_clipboard_string() self.paste_to_active_window(text) @@ -1234,8 +1234,8 @@ class Boss: def current_primary_selection_or_clipboard(self) -> str: return get_primary_selection() if supports_primary_selection else get_clipboard_string() + @ac('cp', 'Paste from the clipboard to the active window') def paste_from_selection(self) -> None: - '@ac:cp: Paste from the clipboard to the active window' text = self.current_primary_selection_or_clipboard() self.paste_to_active_window(text) @@ -1248,12 +1248,12 @@ class Boss: if get_options().copy_on_select: self.copy_to_buffer(get_options().copy_on_select) - def copy_to_buffer(self, buffer_name: str) -> None: - ''' - @ac:cp: Copy the selection from the active window to the specified buffer + @ac('cp', ''' + Copy the selection from the active window to the specified buffer See :ref:`cpbuf` for details. - ''' + ''') + def copy_to_buffer(self, buffer_name: str) -> None: w = self.active_window if w is not None and not w.destroyed: text = w.text_for_selection() @@ -1265,12 +1265,12 @@ class Boss: else: self.clipboard_buffers[buffer_name] = text - def paste_from_buffer(self, buffer_name: str) -> None: - ''' - @ac:cp: Paste from the specified buffer to the active window + @ac('cp', ''' + Paste from the specified buffer to the active window See :ref:`cpbuf` for details. - ''' + ''') + def paste_from_buffer(self, buffer_name: str) -> None: if buffer_name == 'clipboard': text: Optional[str] = get_clipboard_string() elif buffer_name == 'primary': @@ -1280,12 +1280,12 @@ class Boss: if text: self.paste_to_active_window(text) - def goto_tab(self, tab_num: int) -> None: - ''' - @ac:tab: Go to the specified tab, by number, starting with 1 + @ac('tab', ''' + Go to the specified tab, by number, starting with 1 Zero and negative numbers go to previously active tabs - ''' + ''') + def goto_tab(self, tab_num: int) -> None: tm = self.active_tab_manager if tm is not None: tm.goto_tab(tab_num - 1) @@ -1296,14 +1296,14 @@ class Boss: return tm.set_active_tab(tab) return False + @ac('tab', 'Make the next tab active') def next_tab(self) -> None: - '@ac:tab: Make the next tab active' tm = self.active_tab_manager if tm is not None: tm.next_tab() + @ac('tab', 'Make the previous tab active') def previous_tab(self) -> None: - '@ac:tab: Make the previous tab active' tm = self.active_tab_manager if tm is not None: tm.next_tab(-1) @@ -1462,12 +1462,12 @@ class Boss: args = args[1:] self._new_tab(args, as_neighbor=as_neighbor, cwd_from=cwd_from) + @ac('tab', 'Create a new tab') def new_tab(self, *args: str) -> None: - '@ac:tab: Create a new tab' self._create_tab(list(args)) + @ac('tab', 'Create a new tab with working directory for the window in it set to the same as the active window') def new_tab_with_cwd(self, *args: str) -> None: - '@ac:tab: Create a new tab with working directory for the window in it set to the same as the active window' w = self.active_window_for_cwd cwd_from = w.child.pid_for_cwd if w is not None else None self._create_tab(list(args), cwd_from=cwd_from) @@ -1494,46 +1494,46 @@ class Boss: else: return tab.new_window(cwd_from=cwd_from, location=location, allow_remote_control=allow_remote_control) + @ac('win', 'Create a new window') def new_window(self, *args: str) -> None: - '@ac:win: Create a new window' self._new_window(list(args)) + @ac('win', 'Create a new window with working directory same as that of the active window') def new_window_with_cwd(self, *args: str) -> None: - '@ac:win: Create a new window with working directory same as that of the active window' w = self.active_window_for_cwd if w is None: return self.new_window(*args) cwd_from = w.child.pid_for_cwd self._new_window(list(args), cwd_from=cwd_from) - def launch(self, *args: str) -> None: - ''' - @ac:misc: Launch the specified program in a new window/tab/etc. + @ac('misc', ''' + Launch the specified program in a new window/tab/etc. See :doc:`launch` for details - ''' + ''') + def launch(self, *args: str) -> None: from kitty.launch import launch, parse_launch_args opts, args_ = parse_launch_args(args) launch(self, opts, args_) + @ac('tab', 'Move the active tab forward') def move_tab_forward(self) -> None: - '@ac:tab: Move the active tab forward' tm = self.active_tab_manager if tm is not None: tm.move_tab(1) + @ac('tab', 'Move the active tab backward') def move_tab_backward(self) -> None: - '@ac:tab: Move the active tab backward' tm = self.active_tab_manager if tm is not None: tm.move_tab(-1) - def disable_ligatures_in(self, where: Union[str, Iterable[Window]], strategy: int) -> None: - ''' - @ac:misc: Turn on/off ligatures in the specified window + @ac('misc', ''' + Turn on/off ligatures in the specified window See :opt:`disable_ligatures` for details - ''' + ''') + def disable_ligatures_in(self, where: Union[str, Iterable[Window]], strategy: int) -> None: if isinstance(where, str): windows: List[Window] = [] if where == 'active': @@ -1590,16 +1590,16 @@ class Boss: self.default_bg_changed_for(w.id) w.refresh() - def load_config_file(self, *paths: str, apply_overrides: bool = True) -> None: - ''' - @ac:misc: Reload the config file + @ac('misc', ''' + Reload the config file If mapped without arguments reloads the default config file, otherwise loads the specified config files, in order. Loading a config file *replaces* all config options. For example:: map f5 load_config_file /path/to/some/kitty.conf - ''' + ''') + def load_config_file(self, *paths: str, apply_overrides: bool = True) -> None: from .config import load_config old_opts = get_options() paths = paths or old_opts.config_paths @@ -1656,14 +1656,14 @@ class Boss: msg = '\n'.join(map(format_bad_line, bad_lines)).rstrip() self.show_error(_('Errors in kitty.conf'), msg) - def set_colors(self, *args: str) -> None: - ''' - @ac:misc: Change colors in the specified windows + @ac('misc', ''' + Change colors in the specified windows For details, see :ref:`at_set-colors`. For example:: map f5 set_colors --configured /path/to/some/config/file/colors.conf - ''' + ''') + def set_colors(self, *args: str) -> None: from kitty.rc.base import ( PayloadGetter, command_for_name, parse_subcommand_cli ) @@ -1728,8 +1728,8 @@ class Boss: self._cleanup_tab_after_window_removal(tab) target_tab.make_active() + @ac('tab', 'Interactively select a tab to switch to') def select_tab(self) -> None: - '@ac:tab: Interactively select a tab to switch to' title = 'Choose a tab to switch to' lines = [title, ''] fmt = ': {1}' @@ -1761,12 +1761,12 @@ class Boss: ), input_data='\r\n'.join(lines).encode('utf-8'), custom_callback=done, action_on_removal=done2 ) - def detach_window(self, *args: str) -> None: - ''' - @ac:win: Detach a window, moving it to another tab or OS Window + @ac('win', ''' + Detach a window, moving it to another tab or OS Window See :ref:`detaching windows ` for details. - ''' + ''') + def detach_window(self, *args: str) -> None: if not args or args[0] == 'new': return self._move_window_to(target_os_window_id='new') if args[0] in ('new-tab', 'tab-prev', 'tab-left', 'tab-right'): @@ -1817,12 +1817,12 @@ class Boss: ), input_data='\r\n'.join(lines).encode('utf-8'), custom_callback=done, action_on_removal=done2 ) - def detach_tab(self, *args: str) -> None: - ''' - @ac:tab: Detach a tab, moving it to another OS Window + @ac('tab', ''' + Detach a tab, moving it to another OS Window See :ref:`detaching windows ` for details. - ''' + ''') + def detach_tab(self, *args: str) -> None: if not args or args[0] == 'new': return self._move_tab_to() @@ -1894,8 +1894,8 @@ class Boss: if report: w.report_notification_activated(identifier) + @ac('misc', 'Show the environment variables that the kitty process sees') def show_kitty_env_vars(self) -> None: - '@ac:misc: Show the environment variables that the kitty process sees' w = self.active_window if w: output = '\n'.join(f'{k}={v}' for k, v in os.environ.items()) @@ -1919,8 +1919,8 @@ class Boss: if w is not None: tab.remove_window(w) + @ac('misc', 'Show the effective configuration kitty is running with') def debug_config(self) -> None: - '@ac:misc: Show the effective configuration kitty is running with' from .debug_config import debug_config w = self.active_window if w is not None: @@ -1929,7 +1929,7 @@ class Boss: output += '\n\x1b[35mThis debug output has been copied to the clipboard\x1b[m' self.display_scrollback(w, output, title=_('Current kitty options')) + @ac('misc', 'Discard this event completely ignoring it') def discard_event(self) -> None: - '@ac:misc: Discard this event completely ignoring it' pass mouse_discard_event = discard_event diff --git a/kitty/tabs.py b/kitty/tabs.py index d45999f0a..b20b146c3 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -26,6 +26,7 @@ from .fast_data_types import ( from .layout.base import Layout, Rect from .layout.interface import create_layout_object_for, evict_cached_layouts from .tab_bar import TabBar, TabBarData +from .types import ac from .typing import EdgeLiteral, SessionTab, SessionType, TypedDict from .utils import log_error, platform_window_id, resolved_shell from .window import Watchers, Window, WindowDict @@ -222,8 +223,8 @@ class Tab: # {{{ def create_layout_object(self, name: str) -> Layout: return create_layout_object_for(name, self.os_window_id, self.id) + @ac('lay', 'Go to the next enabled layout') def next_layout(self) -> None: - '@ac:lay: Go to the next enabled layout' if len(self.enabled_layouts) > 1: for i, layout_name in enumerate(self.enabled_layouts): if layout_name == self.current_layout.full_name: @@ -235,20 +236,20 @@ class Tab: # {{{ self._set_current_layout(nl) self.relayout() + @ac('lay', 'Go to the previously used layout') def last_used_layout(self) -> None: - '@ac:lay: Go to the previously used layout' if len(self.enabled_layouts) > 1 and self._last_used_layout and self._last_used_layout != self._current_layout_name: self._set_current_layout(self._last_used_layout) self.relayout() - def goto_layout(self, layout_name: str, raise_exception: bool = False) -> None: - ''' - @ac:lay: Switch to the named layout + @ac('lay', ''' + Switch to the named layout For example:: map f1 goto_layout tall - ''' + ''') + def goto_layout(self, layout_name: str, raise_exception: bool = False) -> None: layout_name = layout_name.lower() if layout_name not in self.enabled_layouts: if raise_exception: @@ -258,16 +259,16 @@ class Tab: # {{{ self._set_current_layout(layout_name) self.relayout() - def toggle_layout(self, layout_name: str) -> None: - ''' - @ac:lay: Toggle the named layout + @ac('lay', ''' + Toggle the named layout Switches to the named layout if another layout is current, otherwise switches to the last used layout. Useful to "zoom" a window temporarily by switching to the stack layout. For example:: map f1 toggle_layout stack - ''' + ''') + def toggle_layout(self, layout_name: str) -> None: if self._current_layout_name == layout_name: self.last_used_layout() else: @@ -280,12 +281,12 @@ class Tab: # {{{ return None return 'Could not resize' - def resize_window(self, quality: str, increment: int) -> None: - ''' - @ac:win: Resize the active window by the specified amount + @ac('win', ''' + Resize the active window by the specified amount See :ref:`window_resizing` for details. - ''' + ''') + def resize_window(self, quality: str, increment: int) -> None: if increment < 1: raise ValueError(increment) is_horizontal = quality in ('wider', 'narrower') @@ -296,13 +297,13 @@ class Tab: # {{{ if get_options().enable_audio_bell: ring_bell() + @ac('win', 'Reset window sizes undoing any dynamic resizing of windows') def reset_window_sizes(self) -> None: - '@ac:win:Reset window sizes undoing any dynamic resizing of windows' if self.current_layout.remove_all_biases(): self.relayout() + @ac('lay', 'Perform a layout specific action. See :doc:`layouts` for details') def layout_action(self, action_name: str, args: Sequence[str]) -> None: - '@ac:lay: Perform a layout specific action. See :doc:`layouts` for details' ret = self.current_layout.layout_action(action_name, args, self.windows) if ret is None: ring_bell() @@ -424,14 +425,14 @@ class Tab: # {{{ allow_remote_control=allow_remote_control, watchers=special_window.watchers ) + @ac('win', 'Close the currently active window') def close_window(self) -> None: - '@ac:win: Close the currently active window' w = self.active_window if w is not None: self.remove_window(w) + @ac('win', 'Close all windows in the tab other than the currently active window') def close_other_windows_in_tab(self) -> None: - '@ac:win: Close all windows in the tab other than the currently active window' if len(self.windows) > 1: active_window = self.active_window for window in tuple(self.windows): @@ -469,14 +470,14 @@ class Tab: # {{{ if self.windows: return self.current_layout.nth_window(self.windows, n) - def nth_window(self, num: int = 0) -> None: - ''' - @ac:win: Focus the nth window if positive or the previously active windows if negative + @ac('win', ''' + Focus the nth window if positive or the previously active windows if negative For example, to ficus the previously active window:: map ctrl+p nth_window -1 - ''' + ''') + def nth_window(self, num: int = 0) -> None: if self.windows: if num < 0: self.windows.make_previous_group_active(-num) @@ -489,12 +490,12 @@ class Tab: # {{{ self.current_layout.next_window(self.windows, delta) self.relayout_borders() + @ac('win', 'Focus the next window in the current tab') def next_window(self) -> None: - '@ac:win: Focus the next window in the current tab' self._next_window() + @ac('win', 'Focus the previous window in the current tab') def previous_window(self) -> None: - '@ac:win: Focus the previous window in the current tab' self._next_window(-1) prev_window = previous_window @@ -516,28 +517,28 @@ class Tab: # {{{ if candidates: return self.most_recent_group(candidates) - def neighboring_window(self, which: EdgeLiteral) -> None: - ''' - @ac:win: Focus the neighboring window in the current tab + @ac('win', ''' + Focus the neighboring window in the current tab For example:: map ctrl+left neighboring_window left map ctrl+down neighboring_window bottom - ''' + ''') + def neighboring_window(self, which: EdgeLiteral) -> None: neighbor = self.neighboring_group_id(which) if neighbor: self.windows.set_active_group(neighbor) - def move_window(self, delta: Union[EdgeLiteral, int] = 1) -> None: - ''' - @ac:win: Move the window in the specified direction + @ac('win', ''' + Move the window in the specified direction For example:: map ctrl+left move_window left map ctrl+down move_window bottom - ''' + ''') + def move_window(self, delta: Union[EdgeLiteral, int] = 1) -> None: if isinstance(delta, int): if self.current_layout.move_window(self.windows, delta): self.relayout() @@ -547,18 +548,18 @@ class Tab: # {{{ if self.current_layout.move_window_to_group(self.windows, neighbor): self.relayout() + @ac('win', 'Move active window to the top (make it the first window)') def move_window_to_top(self) -> None: - '@ac:win: Move active window to the top (make it the first window)' n = self.windows.active_group_idx if n > 0: self.move_window(-n) + @ac('win', 'Move active window forward (swap it with the next window)') def move_window_forward(self) -> None: - '@ac:win: Move active window forward (swap it with the next window)' self.move_window() + @ac('win', 'Move active window backward (swap it with the previous window)') def move_window_backward(self) -> None: - '@ac:win: Move active window backward (swap it with the previous window)' self.move_window(-1) def list_windows(self, active_window: Optional[Window], self_window: Optional[Window] = None) -> Generator[WindowDict, None, None]: diff --git a/kitty/types.py b/kitty/types.py index 903cc4c61..13053a0c8 100644 --- a/kitty/types.py +++ b/kitty/types.py @@ -93,3 +93,22 @@ else: def run_once(f: Callable[[], _T]) -> RunOnce: return RunOnce(f) + + +if TYPE_CHECKING: + from typing import Literal + ActionGroup = Literal['cp', 'sc', 'win', 'tab', 'mouse', 'mk', 'lay', 'misc'] +else: + ActionGroup = str + + +class ActionSpec(NamedTuple): + group: str + doc: str + + +def ac(group: ActionGroup, doc: str) -> Callable[[_T], _T]: + def w(f: _T) -> _T: + setattr(f, 'action_spec', ActionSpec(group, doc)) + return f + return w diff --git a/kitty/window.py b/kitty/window.py index 7867629f5..d83bf46bb 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -38,7 +38,7 @@ from .notify import NotificationCommand, handle_notification_cmd from .options.types import Options from .rgb import to_color from .terminfo import get_capabilities -from .types import MouseEvent, ScreenGeometry, WindowGeometry +from .types import MouseEvent, ScreenGeometry, WindowGeometry, ac from .typing import BossType, ChildType, EdgeLiteral, TabType, TypedDict from .utils import ( color_as_int, get_primary_selection, load_shaders, log_error, open_cmd, @@ -529,12 +529,12 @@ class Window: def close(self) -> None: get_boss().close_window(self) - def send_text(self, *args: str) -> bool: - ''' - @ac:misc: Send the specified text to the active window + @ac('misc', ''' + Send the specified text to the active window For details, see :sc:`send_text`. - ''' + ''') + def send_text(self, *args: str) -> bool: mode = keyboard_mode_name(self.screen) required_mode_, text = args[-2:] required_mode = frozenset(required_mode_.split(',')) @@ -848,31 +848,31 @@ class Window: # }}} # mouse actions {{{ + @ac('mouse', 'Click the URL under the mouse') def mouse_click_url(self) -> None: - '@ac:mouse: Click the URL under the mouse' click_mouse_url(self.os_window_id, self.tab_id, self.id) + @ac('mouse', 'Click the URL under the mouse only if the screen has no selection') def mouse_click_url_or_select(self) -> None: - '@ac:mouse: Click the URL under the mouse only if the screen has no selection' if not self.screen.has_selection(): self.mouse_click_url() - def mouse_selection(self, code: int) -> None: - ''' - @ac:mouse: Manipulate the selection based on the current mouse position + @ac('mouse', ''' + Manipulate the selection based on the current mouse position For examples, see :ref:`conf-kitty-mouse.mousemap` - ''' + ''') + def mouse_selection(self, code: int) -> None: mouse_selection(self.os_window_id, self.tab_id, self.id, code, self.current_mouse_event_button) + @ac('mouse', 'Paste the current primary selection') def paste_selection(self) -> None: - '@ac:mouse: Paste the current primary selection' txt = get_boss().current_primary_selection() if txt: self.paste(txt) + @ac('mouse', 'Paste the current primary selection or the clipboard if no selection is present') def paste_selection_or_clipboard(self) -> None: - '@ac:mouse: Paste the current primary selection or the clipboard if no selection is present' txt = get_boss().current_primary_selection_or_clipboard() if txt: self.paste(txt) @@ -935,8 +935,8 @@ class Window: # actions {{{ + @ac('cp', 'Show scrollback in a pager like less') def show_scrollback(self) -> None: - '@ac:cp: Show scrollback in a pager like less' text = self.as_text(as_ansi=True, add_history=True, add_wrap_markers=True) data = self.pipe_data(text, has_wrap_markers=True) get_boss().display_scrollback(self, data['text'], data['input_line_number']) @@ -947,8 +947,8 @@ class Window: text = text.encode('utf-8') self.screen.paste_bytes(text) + @ac('cp', 'Paste the specified text into the current window') def paste(self, text: Union[str, bytes]) -> None: - '@ac:cp: Paste the specified text into the current window' if text and not self.destroyed: if isinstance(text, str): text = text.encode('utf-8') @@ -964,8 +964,8 @@ class Window: text = text.replace(b'\r\n', b'\n').replace(b'\n', b'\r') self.screen.paste(text) + @ac('cp', 'Copy the selected text from the active window to the clipboard') def copy_to_clipboard(self) -> None: - '@ac:cp: Copy the selected text from the active window to the clipboard' text = self.text_for_selection() if text: set_clipboard_string(text) @@ -978,21 +978,21 @@ class Window: cursor_key_mode=self.screen.cursor_key_mode, ).encode('ascii') + @ac('cp', 'Copy the selected text from the active window to the clipboard, if no selection, send Ctrl-C') def copy_or_interrupt(self) -> None: - '@ac:cp: Copy the selected text from the active window to the clipboard, if no selection, send Ctrl-C' text = self.text_for_selection() if text: set_clipboard_string(text) else: self.write_to_child(self.encoded_key(KeyEvent(key=ord('c'), mods=GLFW_MOD_CONTROL))) + @ac('cp', 'Copy the selected text from the active window to the clipboard and clear selection, if no selection, send Ctrl-C') def copy_and_clear_or_interrupt(self) -> None: - '@ac:cp: Copy the selected text from the active window to the clipboard and clear selection, if no selection, send Ctrl-C' self.copy_or_interrupt() self.screen.clear_selection() + @ac('cp', 'Pass the selected text from the active window to the specified program') def pass_selection_to_program(self, *args: str) -> None: - '@ac:cp: Pass the selected text from the active window to the specified program' cwd = self.cwd_of_child text = self.text_for_selection() if text: @@ -1001,38 +1001,38 @@ class Window: else: open_url(text, cwd=cwd) + @ac('sc', 'Scroll up by one line') def scroll_line_up(self) -> None: - '@ac:sc: Scroll up by one line' if self.screen.is_main_linebuf(): self.screen.scroll(SCROLL_LINE, True) + @ac('sc', 'Scroll down by one line') def scroll_line_down(self) -> None: - '@ac:sc: Scroll down by one line' if self.screen.is_main_linebuf(): self.screen.scroll(SCROLL_LINE, False) + @ac('sc', 'Scroll up by one page') def scroll_page_up(self) -> None: - '@ac:sc: Scroll up by one page' if self.screen.is_main_linebuf(): self.screen.scroll(SCROLL_PAGE, True) + @ac('sc', 'Scroll down by one page') def scroll_page_down(self) -> None: - '@ac:sc: Scroll down by one page' if self.screen.is_main_linebuf(): self.screen.scroll(SCROLL_PAGE, False) + @ac('sc', 'Scroll to the top of the scrollback buffer') def scroll_home(self) -> None: - '@ac:sc: Scroll to the top of the scrollback buffer' if self.screen.is_main_linebuf(): self.screen.scroll(SCROLL_FULL, True) + @ac('sc', 'Scroll to the bottom of the scrollback buffer') def scroll_end(self) -> None: - '@ac:sc: Scroll to the bottom of the scrollback buffer' if self.screen.is_main_linebuf(): self.screen.scroll(SCROLL_FULL, False) + @ac('mk', 'Toggle the current marker on/off') def toggle_marker(self, ftype: str, spec: Union[str, Tuple[Tuple[int, str], ...]], flags: int) -> None: - '@ac:mk: Toggle the current marker on/off' from .marks import marker_from_spec key = ftype, spec if key == self.current_marker_spec: @@ -1052,24 +1052,24 @@ class Window: self.screen.set_marker(marker_from_spec(ftype, spec_, flags)) self.current_marker_spec = key + @ac('mk', 'Remove a previously created marker') def remove_marker(self) -> None: - '@ac:mk: Remove a previously created marker' if self.current_marker_spec is not None: self.screen.set_marker() self.current_marker_spec = None + @ac('mk', 'Scroll to the next or previous mark of the specified type') def scroll_to_mark(self, prev: bool = True, mark: int = 0) -> None: - '@ac:mk: Scroll to the next or previous mark of the specified type' self.screen.scroll_to_next_mark(mark, prev) - def signal_child(self, *signals: int) -> None: - ''' - @ac:misc: Send the specified SIGNAL to the foreground process in the active window + @ac('misc', ''' + Send the specified SIGNAL to the foreground process in the active window For example:: map F1 signal_child SIGTERM - ''' + ''') + def signal_child(self, *signals: int) -> None: pid = self.child.pid_for_cwd if pid is not None: for sig in signals: