Refactor remote control commands into individual modules

Also add type information
This commit is contained in:
Kovid Goyal 2020-03-08 08:39:26 +05:30
parent a0321376d5
commit 9b32f18109
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
41 changed files with 2044 additions and 1540 deletions

View File

@ -258,16 +258,17 @@ def write_cli_docs(all_kitten_names):
f.write(option_spec_as_rst(appname='kitty').replace(
'kitty --to', 'kitty @ --to'))
as_rst = partial(option_spec_as_rst, heading_char='_')
from kitty.remote_control import global_options_spec, cli_msg, cmap, all_commands
from kitty.rc.base import all_command_names, command_for_name
from kitty.remote_control import global_options_spec, cli_msg
with open('generated/cli-kitty-at.rst', 'w') as f:
p = partial(print, file=f)
p('kitty @\n' + '-' * 80)
p('.. program::', 'kitty @')
p('\n\n' + as_rst(
global_options_spec, message=cli_msg, usage='command ...', appname='kitty @'))
from kitty.cmds import cli_params_for
for cmd_name in all_commands:
func = cmap[cmd_name]
from kitty.rc.base import cli_params_for
for cmd_name in all_command_names():
func = command_for_name(cmd_name)
p(f'.. _at_{func.name}:\n')
p('kitty @', func.name + '\n' + '-' * 120)
p('.. program::', 'kitty @', func.name)
@ -286,8 +287,8 @@ def write_cli_docs(all_kitten_names):
# }}}
def write_remote_control_protocol_docs(): # {{{
from kitty.cmds import cmap
def write_remote_control_protocol_docs() -> None: # {{{
from kitty.rc.base import all_command_names, command_for_name
field_pat = re.compile(r'\s*([a-zA-Z0-9_+]+)\s*:\s*(.+)')
def format_cmd(p, name, cmd):
@ -319,8 +320,8 @@ def format_cmd(p, name, cmd):
with open(f'generated/rc.rst', 'w') as f:
p = partial(print, file=f)
for name in sorted(cmap):
cmd = cmap[name]
for name in sorted(all_command_names()):
cmd = command_for_name(name)
if not cmd.__doc__:
continue
name = name.replace('_', '-')

View File

@ -6,6 +6,7 @@
import shutil
import subprocess
import sys
from typing import List, Tuple
from kitty.cli import parse_args
from kitty.cli_stub import PanelCLIOptions
@ -43,19 +44,19 @@
'''.format
args = None
args = PanelCLIOptions()
help_text = 'Use a command line program to draw a GPU accelerated panel on your X11 desktop'
usage = 'program-to-run'
def parse_panel_args(args):
def parse_panel_args(args: List[str]) -> Tuple[PanelCLIOptions, List[str]]:
return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten panel', result_class=PanelCLIOptions)
def call_xprop(*cmd, silent=False):
cmd = ['xprop'] + list(cmd)
def call_xprop(*cmd: str, silent=False):
cmd_ = ['xprop'] + list(cmd)
try:
cp = subprocess.run(cmd, stdout=subprocess.DEVNULL if silent else None)
cp = subprocess.run(cmd_, stdout=subprocess.DEVNULL if silent else None)
except FileNotFoundError:
raise SystemExit('You must have the xprop program installed')
if cp.returncode != 0:
@ -94,29 +95,33 @@ def create_right_strut(win_id, width, height):
create_strut(win_id, right=width, right_end_y=height)
window_width = window_height = 0
def setup_x11_window(win_id):
call_xprop(
'-id', str(win_id), '-format', '_NET_WM_WINDOW_TYPE', '32a',
'-set', '_NET_WM_WINDOW_TYPE', '_NET_WM_WINDOW_TYPE_DOCK'
)
func = globals()['create_{}_strut'.format(args.edge)]
func(win_id, initial_window_size_func.width, initial_window_size_func.height)
func(win_id, window_width, window_height)
def initial_window_size_func(opts, *a):
from kitty.fast_data_types import glfw_primary_monitor_size, set_smallest_allowed_resize
def initial_window_size(cell_width, cell_height, dpi_x, dpi_y, xscale, yscale):
global window_width, window_height
monitor_width, monitor_height = glfw_primary_monitor_size()
if args.edge in {'top', 'bottom'}:
h = initial_window_size_func.height = cell_height * args.lines + 1
initial_window_size_func.width = monitor_width
h = window_height = cell_height * args.lines + 1
window_width = monitor_width
set_smallest_allowed_resize(100, h)
else:
w = initial_window_size_func.width = cell_width * args.columns + 1
initial_window_size_func.height = monitor_height
w = window_width = cell_width * args.columns + 1
window_height = monitor_height
set_smallest_allowed_resize(w, 100)
return initial_window_size_func.width, initial_window_size_func.height
return window_width, window_height
return initial_window_size

View File

@ -4,10 +4,11 @@
import sys
from typing import Optional
from kitty.cli import parse_args
from kitty.cli_stub import ResizeCLIOptions
from kitty.cmds import cmap, parse_subcommand_cli
from kitty.cli_stub import RCOptions, ResizeCLIOptions
from kitty.rc.base import parse_subcommand_cli, command_for_name
from kitty.constants import version
from kitty.key_encoding import CTRL, RELEASE, key_defs as K
from kitty.remote_control import encode_send, parse_rc_args
@ -16,7 +17,7 @@
from ..tui.loop import Loop
from ..tui.operations import styled
global_opts = None
global_opts = RCOptions()
ESCAPE = K['ESCAPE']
N = K['N']
S = K['S']
@ -26,7 +27,7 @@
class Resize(Handler):
print_on_fail = None
print_on_fail: Optional[str] = None
def __init__(self, opts):
self.opts = opts
@ -40,7 +41,7 @@ def initialize(self):
self.draw_screen()
def do_window_resize(self, is_decrease=False, is_horizontal=True, reset=False, multiplier=1):
resize_window = cmap['resize-window']
resize_window = command_for_name('resize_window')
increment = self.opts.horizontal_increment if is_horizontal else self.opts.vertical_increment
increment *= multiplier
if is_decrease:
@ -48,7 +49,7 @@ def do_window_resize(self, is_decrease=False, is_horizontal=True, reset=False, m
axis = 'reset' if reset else ('horizontal' if is_horizontal else 'vertical')
cmdline = [resize_window.name, '--self', '--increment={}'.format(increment), '--axis=' + axis]
opts, items = parse_subcommand_cli(resize_window, cmdline)
payload = resize_window(global_opts, opts, items)
payload = resize_window.message_to_kitty(global_opts, opts, items)
send = {'cmd': resize_window.name, 'version': version, 'payload': payload, 'no_response': False}
self.write(encode_send(send))

View File

@ -1164,10 +1164,11 @@ def format_bad_line(bad_line):
self.show_error(_('Errors in kitty.conf'), msg)
def set_colors(self, *args):
from .cmds import parse_subcommand_cli, cmd_set_colors, set_colors
opts, items = parse_subcommand_cli(cmd_set_colors, ['set-colors'] + list(args))
payload = cmd_set_colors(None, opts, items)
set_colors(self, self.active_window, payload)
from kitty.rc.base import parse_subcommand_cli, command_for_name
c = command_for_name('set_colors')
opts, items = parse_subcommand_cli(c, ['set-colors'] + list(args))
payload = c.message_to_kitty(None, opts, items)
c.response_from_kitty(self, self.active_window, payload)
def _move_window_to(self, window=None, target_tab_id=None, target_os_window_id=None):
window = window or self.active_window

View File

@ -9,7 +9,7 @@ class CLIOptions:
LaunchCLIOptions = AskCLIOptions = ClipboardCLIOptions = DiffCLIOptions = CLIOptions
HintsCLIOptions = IcatCLIOptions = PanelCLIOptions = ResizeCLIOptions = CLIOptions
ErrorCLIOptions = UnicodeCLIOptions = CLIOptions
ErrorCLIOptions = UnicodeCLIOptions = RCOptions = CLIOptions
def generate_stub() -> None:
@ -26,6 +26,9 @@ def do(otext=None, cls: str = 'CLIOptions'):
from .launch import options_spec
do(options_spec(), 'LaunchCLIOptions')
from .remote_control import global_options_spec
do(global_options_spec(), 'RCOptions')
from kittens.ask.main import option_text
do(option_text(), 'AskCLIOptions')
@ -38,8 +41,8 @@ def do(otext=None, cls: str = 'CLIOptions'):
from kittens.hints.main import OPTIONS
do(OPTIONS(), 'HintsCLIOptions')
from kittens.icat.main import OPTIONS
do(OPTIONS, 'IcatCLIOptions')
from kittens.icat.main import options_spec
do(options_spec(), 'IcatCLIOptions')
from kittens.panel.main import OPTIONS
do(OPTIONS(), 'PanelCLIOptions')
@ -53,6 +56,12 @@ def do(otext=None, cls: str = 'CLIOptions'):
from kittens.unicode_input.main import OPTIONS
do(OPTIONS(), 'UnicodeCLIOptions')
from kitty.rc.base import all_command_names, command_for_name
for cmd_name in all_command_names():
cmd = command_for_name(cmd_name)
if cmd.options_spec:
do(cmd.options_spec, cmd.__class__.__name__ + 'RCOptions')
save_type_stub(text, __file__)

File diff suppressed because it is too large Load Diff

View File

@ -5,11 +5,13 @@
import os
import shlex
import sys
from functools import lru_cache
from typing import Tuple
from kittens.runner import get_kitten_cli_docs, all_kitten_names
from kittens.runner import all_kitten_names, get_kitten_cli_docs
from .cli import options_for_completion, parse_option_spec
from .cmds import cmap
from .rc.base import all_command_names, command_for_name
from .shell import options_for_cmd
'''
@ -47,6 +49,11 @@ def __init__(self):
self.files_groups = set()
@lru_cache(maxsize=2)
def remote_control_command_names() -> Tuple[str, ...]:
return tuple(sorted(x.replace('_', '-') for x in all_command_names()))
# Shell specific code {{{
@ -164,7 +171,7 @@ def fish_output_serializer(ans):
def completions_for_first_word(ans, prefix, entry_points, namespaced_entry_points):
cmds = ['@' + c for c in cmap]
cmds = ['@' + c for c in remote_control_command_names()]
ans.match_groups['Entry points'] = {
k: None for k in
list(entry_points) + cmds + ['+' + k for k in namespaced_entry_points]
@ -266,9 +273,9 @@ def complete_remote_command(ans, cmd_name, words, new_word):
aliases, alias_map = options_for_cmd(cmd_name)
if not alias_map:
return
args_completion = cmap[cmd_name].args_completion
args_completer = None
if 'files' in args_completion:
args_completion = command_for_name(cmd_name).args_completion
if args_completion and 'files' in args_completion:
args_completer = remote_files_completer(args_completion['files'])
complete_alias_map(ans, words, new_word, alias_map, complete_args=args_completer)
@ -390,14 +397,14 @@ def find_completions(words, new_word, entry_points, namespaced_entry_points):
if words[0] == '@':
if len(words) == 1 or (len(words) == 2 and not new_word):
prefix = words[1] if len(words) > 1 else ''
ans.match_groups['Remote control commands'] = {c: None for c in cmap if c.startswith(prefix)}
ans.match_groups['Remote control commands'] = {c: None for c in remote_control_command_names() if c.startswith(prefix)}
else:
complete_remote_command(ans, words[1], words[2:], new_word)
return ans
if words[0].startswith('@'):
if len(words) == 1 and not new_word:
prefix = words[0]
ans.match_groups['Remote control commands'] = {'@' + c: None for c in cmap if c.startswith(prefix)}
ans.match_groups['Remote control commands'] = {'@' + c: None for c in remote_control_command_names() if c.startswith(prefix)}
else:
complete_remote_command(ans, words[0][1:], words[1:], new_word)
if words[0] == '+':

View File

@ -336,7 +336,10 @@ def save_type_stub(text: str, fpath: str) -> None:
import os
fpath += 'i'
preamble = '# Update this file by running: python {}\n\n'.format(os.path.relpath(os.path.abspath(fpath)))
existing = open(fpath).read()
try:
existing = open(fpath).read()
except FileNotFoundError:
existing = ''
current = preamble + text
if existing != current:
open(fpath, 'w').write(current)

View File

@ -2,14 +2,14 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import errno
import os
import pwd
import sys
import errno
from collections import namedtuple
from contextlib import suppress
from functools import lru_cache
from typing import Set
from typing import Optional, Set
appname = 'kitty'
version = (0, 16, 0)
@ -170,3 +170,9 @@ def is_wayland(opts=None):
supports_primary_selection = not is_macos
def running_in_kitty(set_val: Optional[bool] = None) -> bool:
if set_val is not None:
setattr(running_in_kitty, 'ans', set_val)
return getattr(running_in_kitty, 'ans', False)

View File

@ -489,6 +489,14 @@ def parse_font_feature(str) -> bytes:
pass
def glfw_primary_monitor_size() -> Tuple[int, int]:
pass
def set_smallest_allowed_resize(width: int, height: int) -> None:
pass
def set_default_window_icon(data: bytes, width: int, height: int) -> None:
pass

View File

@ -16,7 +16,7 @@
from .config import cached_values_for, initial_window_size_func
from .constants import (
appname, beam_cursor_data_file, config_dir, glfw_path, is_macos,
is_wayland, kitty_exe, logo_data_file
is_wayland, kitty_exe, logo_data_file, running_in_kitty
)
from .fast_data_types import (
GLFW_IBEAM_CURSOR, create_os_window, free_font_data, glfw_init,
@ -254,6 +254,7 @@ def set_locale():
def _main():
running_in_kitty(True)
with suppress(AttributeError): # python compiled without threading
sys.setswitchinterval(1000.0) # we have only a single python thread

0
kitty/rc/__init__.py Normal file
View File

179
kitty/rc/base.py Normal file
View File

@ -0,0 +1,179 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from contextlib import suppress
from typing import (
TYPE_CHECKING, Any, Callable, Dict, FrozenSet, Generator, List, NoReturn,
Optional, Tuple, Union
)
from kitty.cli import get_defaults_from_seq, parse_args, parse_option_spec
from kitty.cli_stub import RCOptions
from kitty.constants import appname, running_in_kitty
if TYPE_CHECKING:
from kitty.boss import Boss
from kitty.window import Window
Boss, Window
else:
Boss = Window = None
class NoResponse:
pass
class MatchError(ValueError):
hide_traceback = True
def __init__(self, expression, target='windows'):
ValueError.__init__(self, 'No matching {} for expression: {}'.format(target, expression))
class OpacityError(ValueError):
hide_traceback = True
class UnknownLayout(ValueError):
hide_traceback = True
no_response = NoResponse()
ResponseType = Optional[Union[bool, str]]
CmdReturnType = Union[Dict[str, Any], List, Tuple, str, int, float, bool]
CmdGenerator = Generator[CmdReturnType, None, None]
PayloadType = Optional[Union[CmdReturnType, CmdGenerator]]
PayloadGetType = Callable[[str, str], Any]
ArgsType = List[str]
MATCH_WINDOW_OPTION = '''\
--match -m
The window to match. Match specifications are of the form:
:italic:`field:regexp`. Where field can be one of: id, title, pid, cwd, cmdline, num, env.
You can use the :italic:`ls` command to get a list of windows. Note that for
numeric fields such as id, pid and num the expression is interpreted as a number,
not a regular expression. The field num refers to the window position in the current tab,
starting from zero and counting clockwise (this is the same as the order in which the
windows are reported by the :italic:`ls` command). The window id of the current window
is available as the KITTY_WINDOW_ID environment variable. When using the :italic:`env` field
to match on environment variables you can specify only the environment variable name or a name
and value, for example, :italic:`env:MY_ENV_VAR=2`
'''
MATCH_TAB_OPTION = '''\
--match -m
The tab to match. Match specifications are of the form:
:italic:`field:regexp`. Where field can be one of: id, title, pid, cwd, env, cmdline.
You can use the :italic:`ls` command to get a list of tabs. Note that for
numeric fields such as id and pid the expression is interpreted as a number,
not a regular expression. When using title or id, first a matching tab is
looked for and if not found a matching window is looked for, and the tab
for that window is used.
'''
def windows_for_payload(boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> Tuple['Window', ...]:
if payload_get('all'):
windows = tuple(boss.all_windows)
else:
windows = (window or boss.active_window,)
if payload_get('match_window'):
windows = tuple(boss.match_windows(payload_get('match_window')))
if not windows:
raise MatchError(payload_get('match_window'))
if payload_get('match_tab'):
tabs = tuple(boss.match_tabs(payload_get('match_tab')))
if not tabs:
raise MatchError(payload_get('match_tab'), 'tabs')
for tab in tabs:
windows += tuple(tab)
return windows
class RemoteCommand:
name: str = ''
short_desc: str = ''
desc: str = ''
argspec: str = '...'
options_spec: Optional[str] = None
no_response: bool = False
string_return_is_error: bool = False
args_count: Optional[int] = None
args_completion: Optional[Dict[str, Tuple[str, Tuple[str, ...]]]] = None
defaults: Optional[Dict[str, Any]] = None
def __init__(self):
self.desc = self.desc or self.short_desc
self.name = self.__class__.__module__.split('.')[-1].replace('_', '-')
self.args_count = 0 if not self.argspec else self.args_count
def fatal(self, msg: str) -> NoReturn:
if running_in_kitty():
raise Exception(msg)
raise SystemExit(msg)
def get_default(self, name: str, missing: Any = None) -> Any:
if self.options_spec:
if self.defaults is None:
self.defaults = get_defaults_from_seq(parse_option_spec(self.options_spec)[0])
return self.defaults.get(name, missing)
return missing
def payload_get(self, payload: Dict[str, Any], key: str, opt_name: Optional[str] = None) -> Any:
payload_get = object()
ans = payload.get(key, payload_get)
if ans is not payload_get:
return ans
return self.get_default(opt_name or key)
def message_to_kitty(self, global_opts: RCOptions, opts: Any, args: ArgsType) -> PayloadType:
raise NotImplementedError()
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
raise NotImplementedError()
def cli_params_for(command: RemoteCommand) -> Tuple[Callable[[], str], str, str, str]:
return (command.options_spec or '\n').format, command.argspec, command.desc, '{} @ {}'.format(appname, command.name)
def parse_subcommand_cli(command: RemoteCommand, args: ArgsType) -> Tuple[Any, ArgsType]:
opts, items = parse_args(args[1:], *cli_params_for(command))
if command.args_count is not None and command.args_count != len(items):
if command.args_count == 0:
raise SystemExit('Unknown extra argument(s) supplied to {}'.format(command.name))
raise SystemExit('Must specify exactly {} argument(s) for {}'.format(command.args_count, command.name))
return opts, items
def display_subcommand_help(func):
with suppress(SystemExit):
parse_args(['--help'], (func.options_spec or '\n').format, func.argspec, func.desc, func.name)
def command_for_name(cmd_name: str) -> RemoteCommand:
from importlib import import_module
cmd_name = cmd_name.replace('-', '_')
try:
m = import_module(f'kitty.rc.{cmd_name}')
except ImportError:
raise KeyError(f'{cmd_name} is not a known kitty remote control command')
return getattr(m, cmd_name)
def all_command_names() -> FrozenSet[str]:
try:
from importlib.resources import contents
except ImportError:
from importlib_resources import contents
def ok(name: str) -> bool:
root, _, ext = name.rpartition('.')
return ext in ('py', 'pyc', 'pyo') and root and root not in ('base', '__init__')
return frozenset({x.rpartition('.')[0] for x in filter(ok, contents('kitty.rc'))})

49
kitty/rc/close_tab.py Normal file
View File

@ -0,0 +1,49 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING
from .base import (
MATCH_TAB_OPTION, ArgsType, Boss, MatchError, PayloadGetType,
PayloadType, RCOptions, RemoteCommand, ResponseType, Window
)
if TYPE_CHECKING:
from kitty.cli_stub import CloseTabRCOptions as CLIOptions
class CloseTab(RemoteCommand):
'''
match: Which tab to close
self: Boolean indicating whether to close the window the command is run in
'''
short_desc = 'Close the specified tab(s)'
options_spec = MATCH_TAB_OPTION + '''\n
--self
type=bool-set
If specified close the tab this command is run in, rather than the active tab.
'''
argspec = ''
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
return {'match': opts.match, 'self': opts.self}
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
match = payload_get('match')
if match:
tabs = tuple(boss.match_tabs(match))
if not tabs:
raise MatchError(match, 'tabs')
else:
tabs = [boss.tab_for_window(window) if window and payload_get('self') else boss.active_tab]
for tab in tabs:
if window:
if tab:
boss.close_tab(tab)
close_tab = CloseTab()

47
kitty/rc/close_window.py Normal file
View File

@ -0,0 +1,47 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING
from .base import (
MATCH_WINDOW_OPTION, ArgsType, Boss, MatchError, PayloadGetType,
PayloadType, RCOptions, RemoteCommand, ResponseType, Window
)
if TYPE_CHECKING:
from kitty.cli_stub import CloseWindowRCOptions as CLIOptions
class CloseWindow(RemoteCommand):
'''
match: Which window to close
self: Boolean indicating whether to close the window the command is run in
'''
short_desc = 'Close the specified window(s)'
options_spec = MATCH_WINDOW_OPTION + '''\n
--self
type=bool-set
If specified close the window this command is run in, rather than the active window.
'''
argspec = ''
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
return {'match': opts.match, 'self': opts.self}
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
match = payload_get('match')
if match:
windows = tuple(boss.match_windows(match))
if not windows:
raise MatchError(match)
else:
windows = [window if window and payload_get('self') else boss.active_window]
for window in windows:
if window:
boss.close_window(window)
close_window = CloseWindow()

58
kitty/rc/create_marker.py Normal file
View File

@ -0,0 +1,58 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING
from kitty.config import parse_marker_spec
from .base import (
MATCH_WINDOW_OPTION, ArgsType, Boss, MatchError, PayloadGetType,
PayloadType, RCOptions, RemoteCommand, ResponseType, Window
)
if TYPE_CHECKING:
from kitty.cli_stub import CreateMarkerRCOptions as CLIOptions
class CreateMarker(RemoteCommand):
'''
match: Which window to detach
self: Boolean indicating whether to detach the window the command is run in
marker_spec: A list or arguments that define the marker specification, for example: ['text', '1', 'ERROR']
'''
short_desc = 'Create a marker that highlights specified text'
desc = (
'Create a marker which can highlight text in the specified window. For example: '
'create_marker text 1 ERROR. For full details see: https://sw.kovidgoyal.net/kitty/marks.html'
)
options_spec = MATCH_WINDOW_OPTION + '''\n
--self
type=bool-set
If specified apply marker to the window this command is run in, rather than the active window.
'''
argspec = 'MARKER SPECIFICATION'
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
if len(args) < 2:
self.fatal('Invalid marker specification: {}'.format(' '.join(args)))
parse_marker_spec(args[0], args[1:])
return {'match': opts.match, 'self': opts.self, 'marker_spec': args}
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
match = payload_get('match')
if match:
windows = tuple(boss.match_windows(match))
if not windows:
raise MatchError(match)
else:
windows = [window if window and payload_get('self') else boss.active_window]
args = payload_get('marker_spec')
for window in windows:
window.set_marker(args)
create_marker = CreateMarker()

59
kitty/rc/detach_tab.py Normal file
View File

@ -0,0 +1,59 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING
from .base import (
MATCH_TAB_OPTION, ArgsType, Boss, MatchError, PayloadGetType,
PayloadType, RCOptions, RemoteCommand, ResponseType, Window
)
if TYPE_CHECKING:
from kitty.cli_stub import DetachTabRCOptions as CLIOptions
class DetachTab(RemoteCommand):
'''
match: Which tab to detach
target: Which OS Window to move the detached tab to
self: Boolean indicating whether to detach the tab the command is run in
'''
short_desc = 'Detach a tab and place it in a different/new OS Window'
desc = (
'Detach the specified tab and either move it into a new OS window'
' or add it to the OS Window containing the tab specified by --target-tab'
)
options_spec = MATCH_TAB_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--target-tab -t') + '''\n
--self
type=bool-set
If specified detach the tab this command is run in, rather than the active tab.
'''
argspec = ''
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
return {'match': opts.match, 'target': opts.target_tab, 'self': opts.self}
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
match = payload_get('match')
if match:
tabs = tuple(boss.match_tabs(match))
if not tabs:
raise MatchError(match)
else:
tabs = [window.tabref() if payload_get('self') and window and window.tabref() else boss.active_tab]
match = payload_get('target_tab')
kwargs = {}
if match:
targets = tuple(boss.match_tabs(match))
if not targets:
raise MatchError(match, 'tabs')
kwargs['target_os_window_id'] = targets[0].os_window_id
for tab in tabs:
boss._move_tab_to(tab=tab, **kwargs)
detach_tab = DetachTab()

66
kitty/rc/detach_window.py Normal file
View File

@ -0,0 +1,66 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING
from .base import (
MATCH_TAB_OPTION, MATCH_WINDOW_OPTION, ArgsType, Boss, MatchError,
PayloadGetType, PayloadType, RCOptions, RemoteCommand, ResponseType,
Window
)
if TYPE_CHECKING:
from kitty.cli_stub import DetachWindowRCOptions as CLIOptions
class DetachWindow(RemoteCommand):
'''
match: Which window to detach
target: Which tab to move the detached window to
self: Boolean indicating whether to detach the window the command is run in
'''
short_desc = 'Detach a window and place it in a different/new tab'
desc = (
'Detach the specified window and either move it into a new tab, a new OS window'
' or add it to the specified tab. Use the special value :code:`new` for --target-tab'
' to move to a new tab. If no target tab is specified the window is moved to a new OS window.'
)
options_spec = MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--target-tab -t') + '''\n
--self
type=bool-set
If specified detach the window this command is run in, rather than the active window.
'''
argspec = ''
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
return {'match': opts.match, 'target': opts.target_tab, 'self': opts.self}
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
match = payload_get('match')
if match:
windows = tuple(boss.match_windows(match))
if not windows:
raise MatchError(match)
else:
windows = [window if window and payload_get('self') else boss.active_window]
match = payload_get('target_tab')
kwargs = {}
if match:
if match == 'new':
kwargs['target_tab_id'] = 'new'
else:
tabs = tuple(boss.match_tabs(match))
if not tabs:
raise MatchError(match, 'tabs')
kwargs['target_tab_id'] = tabs[0].id
if not kwargs:
kwargs['target_os_window_id'] = 'new'
for window in windows:
boss._move_window_to(window=window, **kwargs)
detach_window = DetachWindow()

View File

@ -0,0 +1,60 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING
from .base import (
MATCH_TAB_OPTION, MATCH_WINDOW_OPTION, ArgsType, Boss, PayloadGetType,
PayloadType, RCOptions, RemoteCommand, ResponseType, Window,
windows_for_payload
)
if TYPE_CHECKING:
from kitty.cli_stub import DisableLigaturesRCOptions as CLIOptions
class DisableLigatures(RemoteCommand):
'''
strategy+: One of :code:`never`, :code:`always` or :code:`cursor`
match_window: Window to change opacity in
match_tab: Tab to change opacity in
all: Boolean indicating operate on all windows
'''
short_desc = 'Control ligature rendering'
desc = (
'Control ligature rendering for the specified windows/tabs (defaults to active window). The STRATEGY'
' can be one of: never, always, cursor'
)
options_spec = '''\
--all -a
type=bool-set
By default, ligatures are only affected in the active window. This option will
cause ligatures to be changed in all windows.
''' + '\n\n' + MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t')
argspec = 'STRATEGY'
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
if not args:
self.fatal(
'You must specify the STRATEGY for disabling ligatures, must be one of'
' never, always or cursor')
strategy = args[0]
if strategy not in ('never', 'always', 'cursor'):
self.fatal('{} is not a valid disable_ligatures strategy'.format('strategy'))
return {
'strategy': strategy, 'match_window': opts.match, 'match_tab': opts.match_tab,
'all': opts.all,
}
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
windows = windows_for_payload(boss, window, payload_get)
boss.disable_ligatures_in(windows, payload_get('strategy'))
# }}}
disable_ligatures = DisableLigatures()

52
kitty/rc/focus_tab.py Normal file
View File

@ -0,0 +1,52 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING
from .base import (
MATCH_TAB_OPTION, ArgsType, Boss, MatchError, PayloadGetType,
PayloadType, RCOptions, RemoteCommand, ResponseType, Window
)
if TYPE_CHECKING:
from kitty.cli_stub import FocusTabRCOptions as CLIOptions
class FocusTab(RemoteCommand):
'''
match: The tab to focus
'''
short_desc = 'Focus the specified tab'
desc = 'The active window in the specified tab will be focused.'
options_spec = MATCH_TAB_OPTION + '''
--no-response
type=bool-set
default=false
Don't wait for a response indicating the success of the action. Note that
using this option means that you will not be notified of failures.
'''
argspec = ''
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
if opts.no_response:
global_opts.no_command_response = True
return {'match': opts.match}
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
match = payload_get('match')
if match:
tabs = tuple(boss.match_tabs(match))
else:
tabs = [boss.tab_for_window(window) if window else boss.active_tab]
if not tabs:
raise MatchError(match, 'tabs')
tab = tabs[0]
boss.set_active_tab(tab)
focus_tab = FocusTab()

55
kitty/rc/focus_window.py Normal file
View File

@ -0,0 +1,55 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING
from kitty.fast_data_types import focus_os_window
from .base import (
MATCH_WINDOW_OPTION, ArgsType, Boss, MatchError, PayloadGetType,
PayloadType, RCOptions, RemoteCommand, ResponseType, Window
)
if TYPE_CHECKING:
from kitty.cli_stub import FocusWindowRCOptions as CLIOptions
class FocusWindow(RemoteCommand):
'''
match: The window to focus
'''
short_desc = 'Focus the specified window'
desc = 'Focus the specified window, if no window is specified, focus the window this command is run inside.'
argspec = ''
options_spec = MATCH_WINDOW_OPTION + '''\n\n
--no-response
type=bool-set
default=false
Don't wait for a response from kitty. This means that even if no matching window is found,
the command will exit with a success code.
'''
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
if opts.no_response:
global_opts.no_command_response = True
return {'match': opts.match, 'no_response': opts.no_response}
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
windows = [window or boss.active_window]
match = payload_get('match')
if match:
windows = tuple(boss.match_windows(match))
if not windows:
raise MatchError(match)
for window in windows:
if window:
os_window_id = boss.set_active_window(window)
if os_window_id:
focus_os_window(os_window_id, True)
break
focus_window = FocusWindow()

57
kitty/rc/get_colors.py Normal file
View File

@ -0,0 +1,57 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING
from kitty.rgb import Color, color_as_sharp, color_from_int
from kitty.utils import natsort_ints
from .base import (
MATCH_WINDOW_OPTION, ArgsType, Boss, MatchError, PayloadGetType,
PayloadType, RCOptions, RemoteCommand, ResponseType, Window
)
if TYPE_CHECKING:
from kitty.cli_stub import GetColorsRCOptions as CLIOptions
class GetColors(RemoteCommand):
'''
match: The window to get the colors for
configured: Boolean indicating whether to get configured or current colors
'''
short_desc = 'Get terminal colors'
desc = (
'Get the terminal colors for the specified window (defaults to active window). '
'Colors will be output to stdout in the same syntax as used for kitty.conf'
)
options_spec = '''\
--configured -c
type=bool-set
Instead of outputting the colors for the specified window, output the currently
configured colors.
''' + '\n\n' + MATCH_WINDOW_OPTION
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
return {'configured': opts.configured, 'match': opts.match}
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
ans = {k: getattr(boss.opts, k) for k in boss.opts if isinstance(getattr(boss.opts, k), Color)}
if not payload_get('configured'):
windows = (window or boss.active_window,)
if payload_get('match'):
windows = tuple(boss.match_windows(payload_get('match')))
if not windows:
raise MatchError(payload_get('match'))
ans.update({k: color_from_int(v) for k, v in windows[0].current_colors.items()})
all_keys = natsort_ints(ans)
maxlen = max(map(len, all_keys))
return '\n'.join(('{:%ds} {}' % maxlen).format(key, color_as_sharp(ans[key])) for key in all_keys)
# }}}
get_colors = GetColors()

67
kitty/rc/get_text.py Normal file
View File

@ -0,0 +1,67 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING
from .base import (
MATCH_WINDOW_OPTION, ArgsType, Boss, MatchError, PayloadGetType,
PayloadType, RCOptions, RemoteCommand, ResponseType, Window
)
if TYPE_CHECKING:
from kitty.cli_stub import GetTextRCOptions as CLIOptions
class GetText(RemoteCommand):
'''
match: The tab to focus
extent: One of :code:`screen`, :code:`all`, or :code:`selection`
ansi: Boolean, if True send ANSI formatting codes
self: Boolean, if True use window command was run in
'''
short_desc = 'Get text from the specified window'
options_spec = MATCH_WINDOW_OPTION + '''\n
--extent
default=screen
choices=screen, all, selection
What text to get. The default of screen means all text currently on the screen. all means
all the screen+scrollback and selection means currently selected text.
--ansi
type=bool-set
By default, only plain text is returned. If you specify this flag, the text will
include the formatting escape codes for colors/bold/italic/etc. Note that when
getting the current selection, the result is always plain text.
--self
type=bool-set
If specified get text from the window this command is run in, rather than the active window.
'''
argspec = ''
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
return {'match': opts.match, 'extent': opts.extent, 'ansi': opts.ansi, 'self': opts.self}
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
match = payload_get('match')
if match:
windows = tuple(boss.match_windows(match))
if not windows:
raise MatchError(match)
else:
windows = [window if window and payload_get('self') else boss.active_window]
window = windows[0]
if payload_get('extent') == 'selection':
ans = window.text_for_selection()
else:
ans = window.as_text(as_ansi=bool(payload_get('ansi')), add_history=payload_get('extent') == 'all')
return ans
get_text = GetText()

56
kitty/rc/goto_layout.py Normal file
View File

@ -0,0 +1,56 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING
from .base import (
MATCH_TAB_OPTION, ArgsType, Boss, MatchError, PayloadGetType, PayloadType,
RCOptions, RemoteCommand, ResponseType, UnknownLayout, Window
)
if TYPE_CHECKING:
from kitty.cli_stub import GotoLayoutRCOptions as CLIOptions
class GotoLayout(RemoteCommand):
'''
layout+: The new layout name
match: Which tab to change the layout of
'''
short_desc = 'Set the window layout'
desc = (
'Set the window layout in the specified tab (or the active tab if not specified).'
' You can use special match value :italic:`all` to set the layout in all tabs.'
)
options_spec = MATCH_TAB_OPTION
argspec = 'LAYOUT_NAME'
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
try:
return {'layout': args[0], 'match': opts.match}
except IndexError:
raise self.fatal('No layout specified')
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
match = payload_get('match')
if match:
if match == 'all':
tabs = tuple(boss.all_tabs)
else:
tabs = tuple(boss.match_tabs(match))
if not tabs:
raise MatchError(match, 'tabs')
else:
tabs = [boss.tab_for_window(window) if window else boss.active_tab]
for tab in tabs:
if tab:
try:
tab.goto_layout(payload_get('layout'), raise_exception=True)
except ValueError:
raise UnknownLayout('The layout {} is unknown or disabled'.format(payload_get('layout')))
goto_layout = GotoLayout()

52
kitty/rc/kitten.py Normal file
View File

@ -0,0 +1,52 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING
from .base import (
MATCH_WINDOW_OPTION, ArgsType, Boss, MatchError, PayloadGetType,
PayloadType, RCOptions, RemoteCommand, ResponseType, Window
)
if TYPE_CHECKING:
from kitty.cli_stub import KittenRCOptions as CLIOptions
class Kitten(RemoteCommand):
'''
kitten+: The name of the kitten to run
args: Arguments to pass to the kitten as a list
match: The window to run the kitten over
'''
short_desc = 'Run a kitten'
desc = (
'Run a kitten over the specified window (active window by default).'
' The :italic:`kitten_name` can be either the name of a builtin kitten'
' or the path to a python file containing a custom kitten. If a relative path'
' is used it is searched for in the kitty config directory.'
)
options_spec = MATCH_WINDOW_OPTION
argspec = 'kitten_name'
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
if len(args) < 1:
self.fatal('Must specify kitten name')
return {'match': opts.match, 'args': list(args)[1:], 'kitten': args[0]}
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
windows = [window or boss.active_window]
match = payload_get('match')
if match:
windows = tuple(boss.match_windows(match))
if not windows:
raise MatchError(match)
for window in windows:
if window:
boss._run_kitten(payload_get('kitten'), args=tuple(payload_get('args') or ()), window=window)
break
kitten = Kitten()

View File

@ -0,0 +1,48 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING
from .base import (
MATCH_TAB_OPTION, ArgsType, Boss, MatchError, PayloadGetType,
PayloadType, RCOptions, RemoteCommand, ResponseType, Window
)
if TYPE_CHECKING:
from kitty.cli_stub import LastUsedLayoutRCOptions as CLIOptions
class LastUsedLayout(RemoteCommand):
'''
match: Which tab to change the layout of
'''
short_desc = 'Switch to the last used layout'
desc = (
'Switch to the last used window layout in the specified tab (or the active tab if not specified).'
' You can use special match value :italic:`all` to set the layout in all tabs.'
)
options_spec = MATCH_TAB_OPTION
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
return {'match': opts.match}
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
match = payload_get('match')
if match:
if match == 'all':
tabs = tuple(boss.all_tabs)
else:
tabs = tuple(boss.match_tabs(match))
if not tabs:
raise MatchError(match, 'tabs')
else:
tabs = [boss.tab_for_window(window) if window else boss.active_tab]
for tab in tabs:
if tab:
tab.last_used_layout()
last_used_layout = LastUsedLayout()

95
kitty/rc/launch.py Normal file
View File

@ -0,0 +1,95 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING
from kitty.launch import (
LaunchCLIOptions, launch as do_launch, options_spec as launch_options_spec,
parse_launch_args
)
from .base import (
MATCH_TAB_OPTION, ArgsType, Boss, MatchError, PayloadGetType, PayloadType,
RCOptions, RemoteCommand, ResponseType, Window
)
if TYPE_CHECKING:
from kitty.cli_stub import LaunchRCOptions as CLIOptions
class Launch(RemoteCommand):
'''
args+: The command line to run in the new window, as a list, use an empty list to run the default shell
match: The tab to open the new window in
window_title: Title for the new window
cwd: Working directory for the new window
env: List of environment variables of the form NAME=VALUE
tab_title: Title for the new tab
type: The type of window to open
keep_focus: Boolean indicating whether the current window should retain focus or not
copy_colors: Boolean indicating whether to copy the colors from the current window
copy_cmdline: Boolean indicating whether to copy the cmdline from the current window
copy_env: Boolean indicating whether to copy the environ from the current window
location: Where in the tab to open the new window
allow_remote_control: Boolean indicating whether to allow remote control from the new window
stdin_source: Where to get stdin for thew process from
stdin_add_formatting: Boolean indicating whether to add formatting codes to stdin
stdin_add_line_wrap_markers: Boolean indicating whether to add line wrap markers to stdin
no_response: Boolean indicating whether to send back the window id
marker: Specification for marker for new window, for example: "text 1 ERROR"
'''
short_desc = 'Run an arbitrary process in a new window/tab'
desc = (
' Prints out the id of the newly opened window. Any command line arguments'
' are assumed to be the command line used to run in the new window, if none'
' are provided, the default shell is run. For example:'
' :italic:`kitty @ launch --title Email mutt`.'
)
options_spec = MATCH_TAB_OPTION + '\n\n' + '''\
--no-response
type=bool-set
Do not print out the id of the newly created window.
--self
type=bool-set
If specified the tab containing the window this command is run in is used
instead of the active tab
''' + '\n\n' + launch_options_spec().replace(':option:`launch', ':option:`kitty @ launch')
argspec = '[CMD ...]'
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
if opts.no_response:
global_opts.no_command_response = True
ans = {'args': args or []}
for attr, val in opts.__dict__.items():
ans[attr] = val
return ans
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
default_opts = parse_launch_args()[0]
opts = LaunchCLIOptions()
for key, default_value in default_opts.__dict__.items():
val = payload_get(key)
if val is None:
val = default_value
setattr(opts, key, val)
match = payload_get('match')
if match:
tabs = tuple(boss.match_tabs(match))
if not tabs:
raise MatchError(match, 'tabs')
else:
tabs = [boss.active_tab]
if payload_get('self') and window and window.tabref():
tabs = [window.tabref()]
tab = tabs[0]
w = do_launch(boss, opts, payload_get('args') or None, target_tab=tab)
return None if payload_get('no_response') else str(getattr(w, 'id', 0))
launch = Launch()

40
kitty/rc/ls.py Normal file
View File

@ -0,0 +1,40 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
import json
from typing import Any
from kitty.constants import appname
from .base import (
ArgsType, Boss, PayloadGetType, PayloadType, RCOptions, RemoteCommand,
ResponseType, Window
)
class LS(RemoteCommand):
'''
No payload
'''
short_desc = 'List all tabs/windows'
desc = (
'List all windows. The list is returned as JSON tree. The top-level is a list of'
f' operating system {appname} windows. Each OS window has an :italic:`id` and a list'
' of :italic:`tabs`. Each tab has its own :italic:`id`, a :italic:`title` and a list of :italic:`windows`.'
' Each window has an :italic:`id`, :italic:`title`, :italic:`current working directory`, :italic:`process id (PID)`, '
' :italic:`command-line` and :italic:`environment` of the process running in the window.\n\n'
'You can use these criteria to select windows/tabs for the other commands.'
)
argspec = ''
def message_to_kitty(self, global_opts: RCOptions, opts: Any, args: ArgsType) -> PayloadType:
pass
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
data = list(boss.list_os_windows())
return json.dumps(data, indent=2, sort_keys=True)
ls = LS()

124
kitty/rc/new_window.py Normal file
View File

@ -0,0 +1,124 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING
from kitty.fast_data_types import focus_os_window
from kitty.tabs import SpecialWindow
from .base import (
MATCH_TAB_OPTION, ArgsType, Boss, MatchError, PayloadGetType, PayloadType,
RCOptions, RemoteCommand, ResponseType, Window
)
if TYPE_CHECKING:
from kitty.cli_stub import NewWindowRCOptions as CLIOptions
class NewWindow(RemoteCommand):
'''
args+: The command line to run in the new window, as a list, use an empty list to run the default shell
match: The tab to open the new window in
title: Title for the new window
cwd: Working directory for the new window
tab_title: Title for the new tab
window_type: One of :code:`kitty` or :code:`os`
keep_focus: Boolean indicating whether the current window should retain focus or not
'''
short_desc = 'Open new window'
desc = (
'Open a new window in the specified tab. If you use the :option:`kitty @ new-window --match` option'
' the first matching tab is used. Otherwise the currently active tab is used.'
' Prints out the id of the newly opened window (unless :option:`--no-response` is used). Any command line arguments'
' are assumed to be the command line used to run in the new window, if none'
' are provided, the default shell is run. For example:\n'
':italic:`kitty @ new-window --title Email mutt`'
)
options_spec = MATCH_TAB_OPTION + '''\n
--title
The title for the new window. By default it will use the title set by the
program running in it.
--cwd
The initial working directory for the new window. Defaults to whatever
the working directory for the kitty process you are talking to is.
--keep-focus
type=bool-set
Keep the current window focused instead of switching to the newly opened window
--window-type
default=kitty
choices=kitty,os
What kind of window to open. A kitty window or a top-level OS window.
--new-tab
type=bool-set
Open a new tab
--tab-title
When using --new-tab set the title of the tab.
--no-response
type=bool-set
default=false
Don't wait for a response giving the id of the newly opened window. Note that
using this option means that you will not be notified of failures and that
the id of the new window will not be printed out.
'''
argspec = '[CMD ...]'
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
if opts.no_response:
global_opts.no_command_response = True
return {'match': opts.match, 'title': opts.title, 'cwd': opts.cwd,
'new_tab': opts.new_tab, 'tab_title': opts.tab_title,
'window_type': opts.window_type, 'no_response': opts.no_response,
'keep_focus': opts.keep_focus, 'args': args or []}
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
w = SpecialWindow(cmd=payload_get('args') or None, override_title=payload_get('title'), cwd=payload_get('cwd'))
old_window = boss.active_window
if payload_get('new_tab'):
boss._new_tab(w)
tab = boss.active_tab
if payload_get('tab_title'):
tab.set_title(payload_get('tab_title'))
wid = boss.active_window.id
if payload_get('keep_focus') and old_window:
boss.set_active_window(old_window)
return None if payload_get('no_response') else str(wid)
if payload_get('window_type') == 'os':
boss._new_os_window(w)
wid = boss.active_window.id
if payload_get('keep_focus') and old_window:
os_window_id = boss.set_active_window(old_window)
if os_window_id:
focus_os_window(os_window_id)
return None if payload_get('no_response') else str(wid)
match = payload_get('match')
if match:
tabs = tuple(boss.match_tabs(match))
if not tabs:
raise MatchError(match, 'tabs')
else:
tabs = [boss.active_tab]
tab = tabs[0]
w = tab.new_special_window(w)
if payload_get('keep_focus') and old_window:
boss.set_active_window(old_window)
return None if payload_get('no_response') else str(w.id)
new_window = NewWindow()

48
kitty/rc/remove_marker.py Normal file
View File

@ -0,0 +1,48 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING
from .base import (
MATCH_WINDOW_OPTION, ArgsType, Boss, MatchError, PayloadGetType,
PayloadType, RCOptions, RemoteCommand, ResponseType, Window
)
if TYPE_CHECKING:
from kitty.cli_stub import RemoveMarkerRCOptions as CLIOptions
class RemoveMarker(RemoteCommand):
'''
match: Which window to remove the marker from
self: Boolean indicating whether to detach the window the command is run in
'''
short_desc = 'Remove the currently set marker, if any.'
options_spec = MATCH_WINDOW_OPTION + '''\n
--self
type=bool-set
If specified apply marker to the window this command is run in, rather than the active window.
'''
argspec = ''
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
return {'match': opts.match, 'self': opts.self}
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
match = payload_get('match')
if match:
windows = tuple(boss.match_windows(match))
if not windows:
raise MatchError(match)
else:
windows = [window if window and payload_get('self') else boss.active_window]
for window in windows:
window.remove_marker()
remove_marker = RemoveMarker()

69
kitty/rc/resize_window.py Normal file
View File

@ -0,0 +1,69 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING
from .base import (
MATCH_WINDOW_OPTION, ArgsType, Boss, MatchError, PayloadGetType,
PayloadType, RCOptions, RemoteCommand, ResponseType, Window
)
if TYPE_CHECKING:
from kitty.cli_stub import ResizeWindowRCOptions as CLIOptions
class ResizeWindow(RemoteCommand):
'''
match: Which window to resize
self: Boolean indicating whether to close the window the command is run in
increment: Integer specifying the resize increment
axis: One of :code:`horizontal, vertical` or :code:`reset`
'''
short_desc = 'Resize the specified window'
desc = 'Resize the specified window in the current layout. Note that not all layouts can resize all windows in all directions.'
options_spec = MATCH_WINDOW_OPTION + '''\n
--increment -i
type=int
default=2
The number of cells to change the size by, can be negative to decrease the size.
--axis -a
type=choices
choices=horizontal,vertical,reset
default=horizontal
The axis along which to resize. If :italic:`horizontal`, it will make the window wider or narrower by the specified increment.
If :italic:`vertical`, it will make the window taller or shorter by the specified increment. The special value :italic:`reset` will
reset the layout to its default configuration.
--self
type=bool-set
If specified resize the window this command is run in, rather than the active window.
'''
argspec = ''
string_return_is_error = True
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
return {'match': opts.match, 'increment': opts.increment, 'axis': opts.axis, 'self': opts.self}
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
match = payload_get('match')
if match:
windows = tuple(boss.match_windows(match))
if not windows:
raise MatchError(match)
else:
windows = [window if window and payload_get('self') else boss.active_window]
resized = False
if windows and windows[0]:
resized = boss.resize_layout_window(
windows[0], increment=payload_get('increment'), is_horizontal=payload_get('axis') == 'horizontal',
reset=payload_get('axis') == 'reset'
)
return resized
resize_window = ResizeWindow()

70
kitty/rc/scroll_window.py Normal file
View File

@ -0,0 +1,70 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING
from .base import (
MATCH_WINDOW_OPTION, ArgsType, Boss, MatchError, PayloadGetType,
PayloadType, RCOptions, RemoteCommand, ResponseType, Window
)
if TYPE_CHECKING:
from kitty.cli_stub import ScrollWindowRCOptions as CLIOptions
class ScrollWindow(RemoteCommand):
'''
amount+: The amount to scroll, a two item list with the first item being \
either a number or the keywords, start and end. \
And the second item being either 'p' for pages or 'l' for lines.
match: The window to scroll
'''
short_desc = 'Scroll the specified window'
desc = (
'Scroll the specified window, if no window is specified, scroll the window this command is run inside.'
' SCROLL_AMOUNT can be either the keywords :code:`start` or :code:`end` or an'
' argument of the form <number>[unit][+-]. For example, 30 will scroll down 30 lines and 2p- will'
' scroll up 2 pages.'
)
argspec = 'SCROLL_AMOUNT'
options_spec = MATCH_WINDOW_OPTION
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
amt = args[0]
ans = {'match': opts.match}
if amt in ('start', 'end'):
ans['amount'] = amt, None
else:
pages = 'p' in amt
amt = amt.replace('p', '')
mult = -1 if amt.endswith('-') else 1
amt = int(amt.replace('-', ''))
ans['amount'] = [amt * mult, 'p' if pages else 'l']
return ans
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
windows = [window or boss.active_window]
match = payload_get('match')
amt = payload_get('amount')
if match:
windows = tuple(boss.match_windows(match))
if not windows:
raise MatchError(match)
for window in windows:
if window:
if amt[0] in ('start', 'end'):
getattr(window, {'start': 'scroll_home'}.get(amt[0], 'scroll_end'))()
else:
amt, unit = amt
unit = 'page' if unit == 'p' else 'line'
direction = 'up' if amt < 0 else 'down'
func = getattr(window, 'scroll_{}_{}'.format(unit, direction))
for i in range(abs(amt)):
func()
scroll_window = ScrollWindow()

132
kitty/rc/send_text.py Normal file
View File

@ -0,0 +1,132 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
import os
import sys
from typing import TYPE_CHECKING
from kitty.config import parse_send_text_bytes
from .base import (
MATCH_TAB_OPTION, MATCH_WINDOW_OPTION, ArgsType, Boss, MatchError,
PayloadGetType, PayloadType, RCOptions, RemoteCommand, ResponseType,
Window
)
if TYPE_CHECKING:
from kitty.cli_stub import SendTextRCOptions as CLIOptions
class SendText(RemoteCommand):
'''
text+: The text being sent
is_binary+: If False text is interpreted as a python string literal instead of plain text
match: A string indicating the window to send text to
match_tab: A string indicating the tab to send text to
'''
short_desc = 'Send arbitrary text to specified windows'
desc = (
'Send arbitrary text to specified windows. The text follows Python'
' escaping rules. So you can use escapes like :italic:`\\x1b` to send control codes'
' and :italic:`\\u21fa` to send unicode characters. If you use the :option:`kitty @ send-text --match` option'
' the text will be sent to all matched windows. By default, text is sent to'
' only the currently active window.'
)
options_spec = MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t') + '''\n
--stdin
type=bool-set
Read the text to be sent from :italic:`stdin`. Note that in this case the text is sent as is,
not interpreted for escapes. If stdin is a terminal, you can press Ctrl-D to end reading.
--from-file
Path to a file whose contents you wish to send. Note that in this case the file contents
are sent as is, not interpreted for escapes.
'''
no_response = True
argspec = '[TEXT TO SEND]'
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
limit = 1024
ret = {'match': opts.match, 'is_binary': False, 'match_tab': opts.match_tab}
def pipe():
ret['is_binary'] = True
if sys.stdin.isatty():
import select
fd = sys.stdin.fileno()
keep_going = True
while keep_going:
rd = select.select([fd], [], [])[0]
if not rd:
break
data = os.read(fd, limit)
if not data:
break # eof
data = data.decode('utf-8')
if '\x04' in data:
data = data[:data.index('\x04')]
keep_going = False
ret['text'] = data
yield ret
else:
while True:
data = sys.stdin.read(limit)
if not data:
break
ret['text'] = data[:limit]
yield ret
def chunks(text):
ret['is_binary'] = False
while text:
ret['text'] = text[:limit]
yield ret
text = text[limit:]
def file_pipe(path):
ret['is_binary'] = True
with open(path, encoding='utf-8') as f:
while True:
data = f.read(limit)
if not data:
break
ret['text'] = data
yield ret
sources = []
if opts.stdin:
sources.append(pipe())
if opts.from_file:
sources.append(file_pipe(opts.from_file))
text = ' '.join(args)
sources.append(chunks(text))
def chain():
for src in sources:
yield from src
return chain()
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
windows = [boss.active_window]
match = payload_get('match')
if match:
windows = tuple(boss.match_windows(match))
mt = payload_get('match_tab')
if mt:
windows = []
tabs = tuple(boss.match_tabs(mt))
if not tabs:
raise MatchError(payload_get('match_tab'), 'tabs')
for tab in tabs:
windows += tuple(tab)
data = payload_get('text').encode('utf-8') if payload_get('is_binary') else parse_send_text_bytes(payload_get('text'))
for window in windows:
if window is not None:
window.write_to_child(data)
send_text = SendText()

View File

@ -0,0 +1,116 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
import imghdr
import tempfile
from base64 import standard_b64decode, standard_b64encode
from typing import TYPE_CHECKING, BinaryIO, Optional
from uuid import uuid4
from .base import (
MATCH_WINDOW_OPTION, ArgsType, Boss, PayloadGetType, PayloadType,
RCOptions, RemoteCommand, ResponseType, Window, no_response,
windows_for_payload
)
if TYPE_CHECKING:
from kitty.cli_stub import SetBackgroundImageRCOptions as CLIOptions
class SetBackgroundImage(RemoteCommand):
'''
data+: Chunk of at most 512 bytes of PNG data, base64 encoded. Must send an empty chunk to indicate end of image. \
Or the special value - to indicate image must be removed.
img_id+: Unique uuid (as string) used for chunking
match: Window to change opacity in
layout: The image layout
all: Boolean indicating operate on all windows
configured: Boolean indicating if the configured value should be changed
'''
short_desc = 'Set the background_image'
desc = (
'Set the background image for the specified OS windows. You must specify the path to a PNG image that'
' will be used as the background. If you specify the special value "none" then any existing image will'
' be removed.'
)
options_spec = '''\
--all -a
type=bool-set
By default, background image is only changed for the currently active OS window. This option will
cause the image to be changed in all windows.
--configured -c
type=bool-set
Change the configured background image which is used for new OS windows.
--layout
type=choices
choices=tiled,scaled,mirror-tiled,configured
How the image should be displayed. The value of configured will use the configured value.
''' + '\n\n' + MATCH_WINDOW_OPTION
argspec = 'PATH_TO_PNG_IMAGE'
args_count = 1
args_completion = {'files': ('PNG Images', ('*.png',))}
current_img_id: Optional[str] = None
current_file_obj: Optional[BinaryIO] = None
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
if not args:
self.fatal('Must specify path to PNG image')
path = args[0]
ret = {'match': opts.match, 'configured': opts.configured, 'layout': opts.layout, 'all': opts.all, 'img_id': str(uuid4())}
if path.lower() == 'none':
ret['data'] = '-'
return ret
if imghdr.what(path) != 'png':
self.fatal('{} is not a PNG image'.format(path))
def file_pipe(path):
with open(path, 'rb') as f:
while True:
data = f.read(512)
if not data:
break
ret['data'] = standard_b64encode(data).decode('ascii')
yield ret
ret['data'] = ''
yield ret
return file_pipe(path)
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
data = payload_get('data')
if data != '-':
img_id = payload_get('img_id')
if img_id != set_background_image.current_img_id:
set_background_image.current_img_id = img_id
set_background_image.current_file_obj = tempfile.NamedTemporaryFile()
if data:
set_background_image.current_file_obj.write(standard_b64decode(data))
return no_response
windows = windows_for_payload(boss, window, payload_get)
os_windows = tuple({w.os_window_id for w in windows})
layout = payload_get('layout')
if data == '-':
path = None
else:
f = set_background_image.current_file_obj
path = f.name
set_background_image.current_file_obj = None
f.flush()
try:
boss.set_background_image(path, os_windows, payload_get('configured'), layout)
except ValueError as err:
err.hide_traceback = True
raise
set_background_image = SetBackgroundImage()

View File

@ -0,0 +1,58 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING
from .base import (
MATCH_TAB_OPTION, MATCH_WINDOW_OPTION, ArgsType, Boss, OpacityError,
PayloadGetType, PayloadType, RCOptions, RemoteCommand, ResponseType,
Window, windows_for_payload
)
if TYPE_CHECKING:
from kitty.cli_stub import SetBackgroundOpacityRCOptions as CLIOptions
class SetBackgroundOpacity(RemoteCommand):
'''
opacity+: A number between 0.1 and 1
match_window: Window to change opacity in
match_tab: Tab to change opacity in
all: Boolean indicating operate on all windows
'''
short_desc = 'Set the background_opacity'
desc = (
'Set the background opacity for the specified windows. This will only work if you have turned on'
' :opt:`dynamic_background_opacity` in :file:`kitty.conf`. The background opacity affects all kitty windows in a'
' single os_window. For example: kitty @ set-background-opacity 0.5'
)
options_spec = '''\
--all -a
type=bool-set
By default, colors are only changed for the currently active window. This option will
cause colors to be changed in all windows.
''' + '\n\n' + MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t')
argspec = 'OPACITY'
args_count = 1
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
opacity = max(0.1, min(float(args[0]), 1.0))
return {
'opacity': opacity, 'match_window': opts.match,
'all': opts.all, 'match_tab': opts.match_tab
}
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
if not boss.opts.dynamic_background_opacity:
raise OpacityError('You must turn on the dynamic_background_opacity option in kitty.conf to be able to set background opacity')
windows = windows_for_payload(boss, window, payload_get)
for os_window_id in {w.os_window_id for w in windows}:
boss._set_os_window_background_opacity(os_window_id, payload_get('opacity'))
set_background_opacity = SetBackgroundOpacity()

97
kitty/rc/set_colors.py Normal file
View File

@ -0,0 +1,97 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
import os
from typing import TYPE_CHECKING
from kitty.config import parse_config
from kitty.fast_data_types import patch_color_profiles
from kitty.rgb import Color, color_as_int
from .base import (
MATCH_TAB_OPTION, MATCH_WINDOW_OPTION, ArgsType, Boss, PayloadGetType,
PayloadType, RCOptions, RemoteCommand, ResponseType, Window,
windows_for_payload
)
if TYPE_CHECKING:
from kitty.cli_stub import SetColorsRCOptions as CLIOptions
class SetColors(RemoteCommand):
'''
colors+: An object mapping names to colors as 24-bit RGB integers
cursor_text_color: A 24-bit color for text under the cursor
match_window: Window to change colors in
match_tab: Tab to change colors in
all: Boolean indicating change colors everywhere or not
configured: Boolean indicating whether to change the configured colors. Must be True if reset is True
reset: Boolean indicating colors should be reset to startup values
'''
short_desc = 'Set terminal colors'
desc = (
'Set the terminal colors for the specified windows/tabs (defaults to active window). You can either specify the path to a conf file'
' (in the same format as kitty.conf) to read the colors from or you can specify individual colors,'
' for example: kitty @ set-colors foreground=red background=white'
)
options_spec = '''\
--all -a
type=bool-set
By default, colors are only changed for the currently active window. This option will
cause colors to be changed in all windows.
--configured -c
type=bool-set
Also change the configured colors (i.e. the colors kitty will use for new
windows or after a reset).
--reset
type=bool-set
Restore all colors to the values they had at kitty startup. Note that if you specify
this option, any color arguments are ignored and --configured and --all are implied.
''' + '\n\n' + MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t')
argspec = 'COLOR_OR_FILE ...'
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
colors, cursor_text_color = {}, False
if not opts.reset:
for spec in args:
if '=' in spec:
colors.update(parse_config((spec.replace('=', ' '),)))
else:
with open(os.path.expanduser(spec), encoding='utf-8', errors='replace') as f:
colors.update(parse_config(f))
cursor_text_color = colors.pop('cursor_text_color', False)
colors = {k: color_as_int(v) for k, v in colors.items() if isinstance(v, Color)}
return {
'match_window': opts.match, 'match_tab': opts.match_tab,
'all': opts.all or opts.reset, 'configured': opts.configured or opts.reset,
'colors': colors, 'reset': opts.reset, 'cursor_text_color': cursor_text_color
}
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
windows = windows_for_payload(boss, window, payload_get)
colors = payload_get('colors')
cursor_text_color = payload_get('cursor_text_color') or False
if payload_get('reset'):
colors = {k: color_as_int(v) for k, v in boss.startup_colors.items()}
cursor_text_color = boss.startup_cursor_text_color
profiles = tuple(w.screen.color_profile for w in windows)
if isinstance(cursor_text_color, (tuple, list, Color)):
cursor_text_color = color_as_int(Color(*cursor_text_color))
patch_color_profiles(colors, cursor_text_color, profiles, payload_get('configured'))
boss.patch_colors(colors, cursor_text_color, payload_get('configured'))
default_bg_changed = 'background' in colors
for w in windows:
if default_bg_changed:
boss.default_bg_changed_for(w.id)
w.refresh()
set_colors = SetColors()

54
kitty/rc/set_font_size.py Normal file
View File

@ -0,0 +1,54 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING
from .base import (
ArgsType, Boss, PayloadGetType, PayloadType, RCOptions, RemoteCommand,
ResponseType, Window
)
if TYPE_CHECKING:
from kitty.cli_stub import SetFontSizeRCOptions as CLIOptions
class SetFontSize(RemoteCommand):
'''
size+: The new font size in pts (a positive number)
all: Boolean whether to change font size in the current window or all windows
increment_op: The string ``+`` or ``-`` to interpret size as an increment
'''
short_desc = 'Set the font size in the active top-level OS window'
desc = (
'Sets the font size to the specified size, in pts. Note'
' that in kitty all sub-windows in the same OS window'
' must have the same font size. A value of zero'
' resets the font size to default. Prefixing the value'
' with a + or - increments the font size by the specified'
' amount.'
)
argspec = 'FONT_SIZE'
args_count = 1
options_spec = '''\
--all -a
type=bool-set
By default, the font size is only changed in the active OS window,
this option will cause it to be changed in all OS windows.
'''
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
if not args:
self.fatal('No font size specified')
fs = args[0]
inc = fs[0] if fs and fs[0] in '+-' else None
return {'size': abs(float(fs)), 'all': opts.all, 'increment_op': inc}
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
boss.change_font_size(
payload_get('all'),
payload_get('increment_op'), payload_get('size'))
set_font_size = SetFontSize()

50
kitty/rc/set_tab_title.py Normal file
View File

@ -0,0 +1,50 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING
from .base import (
MATCH_TAB_OPTION, ArgsType, Boss, MatchError, PayloadGetType,
PayloadType, RCOptions, RemoteCommand, ResponseType, Window
)
if TYPE_CHECKING:
from kitty.cli_stub import SetTabTitleRCOptions as CLIOptions
class SetTabTitle(RemoteCommand):
'''
title+: The new title
match: Which tab to change the title of
'''
short_desc = 'Set the tab title'
desc = (
'Set the title for the specified tab(s). If you use the :option:`kitty @ set-tab-title --match` option'
' the title will be set for all matched tabs. By default, only the tab'
' in which the command is run is affected. If you do not specify a title, the'
' title of the currently active window in the tab is used.'
)
options_spec = MATCH_TAB_OPTION
argspec = 'TITLE ...'
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
return {'title': ' '.join(args), 'match': opts.match}
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
match = payload_get('match')
if match:
tabs = tuple(boss.match_tabs(match))
if not tabs:
raise MatchError(match, 'tabs')
else:
tabs = [boss.tab_for_window(window) if window else boss.active_tab]
for tab in tabs:
if tab:
tab.set_title(payload_get('title'))
set_tab_title = SetTabTitle()

View File

@ -0,0 +1,59 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING
from .base import (
MATCH_WINDOW_OPTION, ArgsType, Boss, MatchError, PayloadGetType,
PayloadType, RCOptions, RemoteCommand, ResponseType, Window
)
if TYPE_CHECKING:
from kitty.cli_stub import SetWindowTitleRCOptions as CLIOptions
class SetWindowTitle(RemoteCommand):
'''
title+: The new title
match: Which windows to change the title in
temporary: Boolean indicating if the change is temporary or permanent
'''
short_desc = 'Set the window title'
desc = (
'Set the title for the specified window(s). If you use the :option:`kitty @ set-window-title --match` option'
' the title will be set for all matched windows. By default, only the window'
' in which the command is run is affected. If you do not specify a title, the'
' last title set by the child process running in the window will be used.'
)
options_spec = '''\
--temporary
type=bool-set
By default, if you use :italic:`set-window-title` the title will be permanently changed
and programs running in the window will not be able to change it again. If you
want to allow other programs to change it afterwards, use this option.
''' + '\n\n' + MATCH_WINDOW_OPTION
argspec = 'TITLE ...'
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
return {'title': ' '.join(args), 'match': opts.match, 'temporary': opts.temporary}
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
windows = [window or boss.active_window]
match = payload_get('match')
if match:
windows = tuple(boss.match_windows(match))
if not windows:
raise MatchError(match)
for window in windows:
if window:
if payload_get('temporary'):
window.override_title = None
window.title_changed(payload_get('title'))
else:
window.set_title(payload_get('title'))
set_window_title = SetWindowTitle()

View File

@ -9,13 +9,16 @@
import types
from contextlib import suppress
from functools import partial
from typing import Any, List, Optional
from .cli import emph, parse_args
from .cmds import (
cmap, no_response as no_response_sentinel, parse_subcommand_cli
)
from .cli_stub import RCOptions
from .constants import appname, version
from .fast_data_types import read_command_response
from .rc.base import (
all_command_names, command_for_name, no_response as no_response_sentinel,
parse_subcommand_cli
)
from .utils import TTYIO, parse_address_spec
@ -27,11 +30,14 @@ def handle_cmd(boss, window, cmd):
if no_response:
return
return {'ok': False, 'error': 'The kitty client you are using to send remote commands is newer than this kitty instance. This is not supported.'}
c = cmap[cmd['cmd']]
func = partial(c.impl(), boss, window)
payload = cmd.get('payload')
c = command_for_name(cmd['cmd'])
payload = cmd.get('payload') or {}
def payload_get(key: str, opt_name: Optional[str] = None) -> Any:
return c.payload_get(payload, key, opt_name)
try:
ans = func() if payload is None else func(payload)
ans = c.response_from_kitty(boss, window, payload_get)
except Exception:
if no_response: # don't report errors if --no-response was used
return
@ -130,7 +136,6 @@ def send_generator():
return response
all_commands = tuple(sorted(cmap))
cli_msg = (
'Control {appname} by sending it commands. Set the'
' :opt:`allow_remote_control` option to yes in :file:`kitty.conf` for this'
@ -138,12 +143,9 @@ def send_generator():
).format(appname=appname)
class RCOptions:
pass
def parse_rc_args(args):
cmds = (' :green:`{}`\n {}'.format(cmap[c].name, cmap[c].short_desc) for c in all_commands)
def parse_rc_args(args: List[str]):
cmap = {name: command_for_name(name) for name in sorted(all_command_names())}
cmds = (' :green:`{}`\n {}'.format(cmd.name, cmd.short_desc) for c, cmd in cmap.items())
msg = cli_msg + (
'\n\n:title:`Commands`:\n{cmds}\n\n'
'You can get help for each individual command by using:\n'
@ -161,12 +163,12 @@ def main(args):
return
cmd = items[0]
try:
func = cmap[cmd]
c = command_for_name(cmd)
except KeyError:
raise SystemExit('{} is not a known command. Known commands are: {}'.format(
emph(cmd), ', '.join(all_commands)))
opts, items = parse_subcommand_cli(func, items)
payload = func(global_opts, opts, items)
emph(cmd), ', '.join(x.replace('_', '-') for x in all_command_names())))
opts, items = parse_subcommand_cli(c, items)
payload = c.message_to_kitty(global_opts, opts, items)
send = {
'cmd': cmd,
'version': version,
@ -176,7 +178,7 @@ def main(args):
if global_opts.no_command_response is not None:
no_response = global_opts.no_command_response
else:
no_response = func.no_response
no_response = c.no_response
send['no_response'] = no_response
if not global_opts.to and 'KITTY_LISTEN_ON' in os.environ:
global_opts.to = os.environ['KITTY_LISTEN_ON']
@ -189,6 +191,6 @@ def main(args):
raise SystemExit(response['error'])
data = response.get('data')
if data is not None:
if func.string_return_is_error and isinstance(data, str):
if c.string_return_is_error and isinstance(data, str):
raise SystemExit(data)
print(data)

View File

@ -7,17 +7,25 @@
import shlex
import sys
import traceback
from functools import lru_cache
from contextlib import suppress
from functools import lru_cache
from typing import Dict, Tuple
from .cli import (
emph, green, italic, parse_option_spec, print_help_for_seq, title
OptionDict, emph, green, italic, parse_option_spec, print_help_for_seq,
title
)
from .cmds import cmap, display_subcommand_help, parse_subcommand_cli
from .constants import cache_dir, is_macos, version
from .rc.base import (
all_command_names, command_for_name, display_subcommand_help,
parse_subcommand_cli
)
all_commands = tuple(sorted(cmap))
match_commands = tuple(sorted(all_commands + ('exit', 'help', 'quit')))
@lru_cache(maxsize=2)
def match_commands() -> Tuple[str, ...]:
all_commands = tuple(sorted(all_command_names()))
return tuple(sorted(all_commands + ('exit', 'help', 'quit')))
def init_readline(readline):
@ -33,16 +41,16 @@ def init_readline(readline):
def cmd_names_matching(prefix):
for cmd in match_commands:
for cmd in match_commands():
if not prefix or cmd.startswith(prefix):
yield cmd + ' '
@lru_cache()
def options_for_cmd(cmd):
def options_for_cmd(cmd: str) -> Tuple[Tuple[str, ...], Dict[str, OptionDict]]:
alias_map = {}
try:
func = cmap[cmd]
func = command_for_name(cmd)
except KeyError:
return (), alias_map
if not func.options_spec:
@ -106,8 +114,8 @@ def print_help(which=None):
print('Control kitty by sending it commands.')
print()
print(title('Commands') + ':')
for cmd in all_commands:
c = cmap[cmd]
for cmd in all_command_names():
c = command_for_name(cmd)
print(' ', green(c.name))
print(' ', c.short_desc)
print(' ', green('exit'))
@ -115,7 +123,7 @@ def print_help(which=None):
print('\nUse help {} for help on individual commands'.format(italic('command')))
else:
try:
func = cmap[which]
func = command_for_name(which)
except KeyError:
if which == 'exit':
print('Exit this shell')
@ -170,7 +178,7 @@ def real_main(global_opts):
cmd = cmdline[0].lower()
try:
func = cmap[cmd]
func = command_for_name(cmd)
except KeyError:
if cmd in ('exit', 'quit'):
break