Make get_all_actions() work in binary builds

This commit is contained in:
Kovid Goyal 2021-08-05 06:10:41 +05:30
parent 57ced9bc83
commit 276a82d1f7
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
5 changed files with 183 additions and 172 deletions

View File

@ -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):

View File

@ -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 <detach_window>` 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 <detach_window>` 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

View File

@ -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]:

View File

@ -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

View File

@ -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: