1459 lines
49 KiB
Python
1459 lines
49 KiB
Python
#!/usr/bin/env python3
|
|
# vim:fileencoding=utf-8
|
|
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from contextlib import suppress
|
|
from typing import Any, BinaryIO, Callable, Dict, List, Optional
|
|
|
|
from .cli import (
|
|
Namespace, get_defaults_from_seq, parse_args, parse_option_spec
|
|
)
|
|
from .config import parse_config, parse_send_text_bytes
|
|
from .constants import appname
|
|
from .fast_data_types import focus_os_window
|
|
from .launch import (
|
|
launch as do_launch, options_spec as launch_options_spec,
|
|
parse_launch_args
|
|
)
|
|
from .tabs import SpecialWindow
|
|
from .utils import natsort_ints
|
|
|
|
no_response = object()
|
|
|
|
|
|
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
|
|
|
|
|
|
CommandFunction = Callable[[Namespace, Namespace, List[str]], Optional[Dict[str, Any]]]
|
|
cmap: Dict[str, CommandFunction] = {}
|
|
|
|
|
|
def cmd(
|
|
short_desc,
|
|
desc=None,
|
|
options_spec=None,
|
|
no_response=False,
|
|
argspec='...',
|
|
string_return_is_error=False,
|
|
args_count=None,
|
|
args_completion=None
|
|
):
|
|
|
|
if options_spec:
|
|
defaults = None
|
|
|
|
def get_defaut_value(name, missing=None):
|
|
nonlocal defaults
|
|
if defaults is None:
|
|
defaults = get_defaults_from_seq(parse_option_spec(options_spec)[0])
|
|
return defaults.get(name, missing)
|
|
else:
|
|
def get_defaut_value(name, missing=None):
|
|
return missing
|
|
|
|
def payload_get(payload, key, opt_name=None):
|
|
ans = payload.get(key, payload_get)
|
|
if ans is not payload_get:
|
|
return ans
|
|
return get_defaut_value(opt_name or key)
|
|
|
|
def w(func):
|
|
func.short_desc = short_desc
|
|
func.argspec = argspec
|
|
func.desc = desc or short_desc
|
|
func.name = func.__name__[4:].replace('_', '-')
|
|
func.options_spec = options_spec
|
|
func.is_cmd = True
|
|
func.impl = lambda: globals()[func.__name__[4:]]
|
|
func.no_response = no_response
|
|
func.string_return_is_error = string_return_is_error
|
|
func.args_count = 0 if not argspec else args_count
|
|
func.get_default = get_defaut_value
|
|
func.payload_get = payload_get
|
|
func.args_completion = args_completion
|
|
cmap[func.name] = func
|
|
return func
|
|
return w
|
|
|
|
|
|
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, window, payload):
|
|
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['match_window']))
|
|
if not windows:
|
|
raise MatchError(payload['match_window'])
|
|
if payload.get('match_tab'):
|
|
tabs = tuple(boss.match_tabs(payload['match_tab']))
|
|
if not tabs:
|
|
raise MatchError(payload['match_tab'], 'tabs')
|
|
for tab in tabs:
|
|
windows += tuple(tab)
|
|
return windows
|
|
|
|
|
|
# ls {{{
|
|
@cmd(
|
|
'List all tabs/windows',
|
|
'List all windows. The list is returned as JSON tree. The top-level is a list of'
|
|
' 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.'.format(appname=appname),
|
|
argspec=''
|
|
)
|
|
def cmd_ls(global_opts, opts, args):
|
|
'''
|
|
No payload
|
|
'''
|
|
pass
|
|
|
|
|
|
def ls(boss, window):
|
|
data = list(boss.list_os_windows())
|
|
data = json.dumps(data, indent=2, sort_keys=True)
|
|
return data
|
|
# }}}
|
|
|
|
|
|
# set_font_size {{{
|
|
@cmd(
|
|
'Set the font size in the active top-level OS window',
|
|
'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 cmd_set_font_size(global_opts, opts, args):
|
|
'''
|
|
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
|
|
'''
|
|
if not args:
|
|
raise SystemExit('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 set_font_size(boss, window, payload):
|
|
boss.change_font_size(
|
|
cmd_set_font_size.payload_get(payload, 'all'),
|
|
payload.get('increment_op', None), payload['size'])
|
|
# }}}
|
|
|
|
|
|
# send_text {{{
|
|
@cmd(
|
|
'Send arbitrary text to specified windows',
|
|
'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 cmd_send_text(global_opts, opts, args):
|
|
'''
|
|
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
|
|
'''
|
|
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 send_text(boss, window, payload):
|
|
windows = [boss.active_window]
|
|
pg = cmd_send_text.payload_get
|
|
match = pg(payload, 'match')
|
|
if match:
|
|
windows = tuple(boss.match_windows(match))
|
|
mt = pg(payload, 'match_tab')
|
|
if mt:
|
|
windows = []
|
|
tabs = tuple(boss.match_tabs(mt))
|
|
if not tabs:
|
|
raise MatchError(payload['match_tab'], 'tabs')
|
|
for tab in tabs:
|
|
windows += tuple(tab)
|
|
data = payload['text'].encode('utf-8') if payload['is_binary'] else parse_send_text_bytes(payload['text'])
|
|
for window in windows:
|
|
if window is not None:
|
|
window.write_to_child(data)
|
|
# }}}
|
|
|
|
|
|
# set_window_title {{{
|
|
@cmd(
|
|
'Set the window title',
|
|
'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 cmd_set_window_title(global_opts, opts, args):
|
|
'''
|
|
title+: The new title
|
|
match: Which windows to change the title in
|
|
temporary: Boolean indicating if the change is temporary or permanent
|
|
'''
|
|
return {'title': ' '.join(args), 'match': opts.match, 'temporary': opts.temporary}
|
|
|
|
|
|
def set_window_title(boss, window, payload):
|
|
windows = [window or boss.active_window]
|
|
pg = cmd_set_window_title.payload_get
|
|
match = pg(payload, 'match')
|
|
if match:
|
|
windows = tuple(boss.match_windows(match))
|
|
if not windows:
|
|
raise MatchError(match)
|
|
for window in windows:
|
|
if window:
|
|
if pg(payload, 'temporary'):
|
|
window.override_title = None
|
|
window.title_changed(payload['title'])
|
|
else:
|
|
window.set_title(payload['title'])
|
|
|
|
# }}}
|
|
|
|
|
|
# set_tab_title {{{
|
|
@cmd(
|
|
'Set the tab title',
|
|
'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 cmd_set_tab_title(global_opts, opts, args):
|
|
'''
|
|
title+: The new title
|
|
match: Which tab to change the title of
|
|
'''
|
|
return {'title': ' '.join(args), 'match': opts.match}
|
|
|
|
|
|
def set_tab_title(boss, window, payload):
|
|
pg = cmd_set_tab_title.payload_get
|
|
match = pg(payload, '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['title'])
|
|
# }}}
|
|
|
|
|
|
# create_marker {{{
|
|
@cmd(
|
|
'Create a marker that highlights specified text',
|
|
'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 cmd_create_marker(global_opts, opts, args):
|
|
'''
|
|
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']
|
|
'''
|
|
from .config import parse_marker_spec
|
|
if len(args) < 2:
|
|
raise ValueError('Invalid marker specification: {}'.format(' '.join(args)))
|
|
parse_marker_spec(args[0], args[1:])
|
|
return {'match': opts.match, 'self': opts.self, 'marker_spec': args}
|
|
|
|
|
|
def create_marker(boss, window, payload):
|
|
pg = cmd_create_marker.payload_get
|
|
match = pg(payload, 'match')
|
|
if match:
|
|
windows = tuple(boss.match_windows(match))
|
|
if not windows:
|
|
raise MatchError(match)
|
|
else:
|
|
windows = [window if window and pg(payload, 'self') else boss.active_window]
|
|
args = pg(payload, 'marker_spec')
|
|
|
|
for window in windows:
|
|
window.set_marker(args)
|
|
|
|
# }}}
|
|
|
|
|
|
# remove_marker {{{
|
|
@cmd(
|
|
'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 cmd_remove_marker(global_opts, opts, args):
|
|
'''
|
|
match: Which window to detach
|
|
self: Boolean indicating whether to detach the window the command is run in
|
|
'''
|
|
return {'match': opts.match, 'self': opts.self}
|
|
|
|
|
|
def remove_marker(boss, window, payload):
|
|
pg = cmd_create_marker.payload_get
|
|
match = pg(payload, 'match')
|
|
if match:
|
|
windows = tuple(boss.match_windows(match))
|
|
if not windows:
|
|
raise MatchError(match)
|
|
else:
|
|
windows = [window if window and pg(payload, 'self') else boss.active_window]
|
|
|
|
for window in windows:
|
|
window.remove_marker()
|
|
|
|
# }}}
|
|
|
|
|
|
# detach_window {{{
|
|
@cmd(
|
|
'Detach a window and place it in a different/new tab',
|
|
'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 cmd_detach_window(global_opts, opts, args):
|
|
'''
|
|
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
|
|
'''
|
|
return {'match': opts.match, 'target': opts.target_tab, 'self': opts.self}
|
|
|
|
|
|
def detach_window(boss, window, payload):
|
|
pg = cmd_detach_window.payload_get
|
|
match = pg(payload, 'match')
|
|
if match:
|
|
windows = tuple(boss.match_windows(match))
|
|
if not windows:
|
|
raise MatchError(match)
|
|
else:
|
|
windows = [window if window and pg(payload, 'self') else boss.active_window]
|
|
match = pg(payload, '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_tab {{{
|
|
@cmd(
|
|
'Detach a tab and place it in a different/new OS Window',
|
|
'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 cmd_detach_tab(global_opts, opts, args):
|
|
'''
|
|
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
|
|
'''
|
|
return {'match': opts.match, 'target': opts.target_tab, 'self': opts.self}
|
|
|
|
|
|
def detach_tab(boss, window, payload):
|
|
pg = cmd_detach_tab.payload_get
|
|
match = pg(payload, 'match')
|
|
if match:
|
|
tabs = tuple(boss.match_tabs(match))
|
|
if not tabs:
|
|
raise MatchError(match)
|
|
else:
|
|
tabs = [window.tabref() if pg(payload, 'self') and window and window.tabref() else boss.active_tab]
|
|
match = pg(payload, '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)
|
|
|
|
# }}}
|
|
|
|
|
|
# goto_layout {{{
|
|
@cmd(
|
|
'Set the window layout',
|
|
'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 cmd_goto_layout(global_opts, opts, args):
|
|
'''
|
|
layout+: The new layout name
|
|
match: Which tab to change the layout of
|
|
'''
|
|
try:
|
|
return {'layout': args[0], 'match': opts.match}
|
|
except IndexError:
|
|
raise SystemExit('No layout specified')
|
|
|
|
|
|
def goto_layout(boss, window, payload):
|
|
pg = cmd_goto_layout.payload_get
|
|
match = pg(payload, '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['layout'], raise_exception=True)
|
|
except ValueError:
|
|
raise UnknownLayout('The layout {} is unknown or disabled'.format(payload['layout']))
|
|
# }}}
|
|
|
|
|
|
# last_used_layout {{{
|
|
@cmd(
|
|
'Switch to the last used layout',
|
|
'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 cmd_last_used_layout(global_opts, opts, args):
|
|
'''
|
|
match: Which tab to change the layout of
|
|
'''
|
|
return {'match': opts.match}
|
|
|
|
|
|
def last_used_layout(boss, window, payload):
|
|
pg = cmd_last_used_layout.payload_get
|
|
match = pg(payload, '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()
|
|
# }}}
|
|
|
|
|
|
# close_window {{{
|
|
@cmd(
|
|
'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 cmd_close_window(global_opts, opts, args):
|
|
'''
|
|
match: Which window to close
|
|
self: Boolean indicating whether to close the window the command is run in
|
|
'''
|
|
return {'match': opts.match, 'self': opts.self}
|
|
|
|
|
|
def close_window(boss, window, payload):
|
|
pg = cmd_close_window.payload_get
|
|
match = pg(payload, 'match')
|
|
if match:
|
|
windows = tuple(boss.match_windows(match))
|
|
if not windows:
|
|
raise MatchError(match)
|
|
else:
|
|
windows = [window if window and pg(payload, 'self') else boss.active_window]
|
|
for window in windows:
|
|
if window:
|
|
boss.close_window(window)
|
|
# }}}
|
|
|
|
|
|
# resize_window {{{
|
|
@cmd(
|
|
'Resize the specified window',
|
|
'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 cmd_resize_window(global_opts, opts, args):
|
|
'''
|
|
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`
|
|
'''
|
|
return {'match': opts.match, 'increment': opts.increment, 'axis': opts.axis, 'self': opts.self}
|
|
|
|
|
|
def resize_window(boss, window, payload):
|
|
pg = cmd_resize_window.payload_get
|
|
match = pg(payload, 'match')
|
|
if match:
|
|
windows = tuple(boss.match_windows(match))
|
|
if not windows:
|
|
raise MatchError(match)
|
|
else:
|
|
windows = [window if window and pg(payload, 'self') else boss.active_window]
|
|
resized = False
|
|
if windows and windows[0]:
|
|
resized = boss.resize_layout_window(
|
|
windows[0], increment=pg(payload, 'increment'), is_horizontal=pg(payload, 'axis') == 'horizontal',
|
|
reset=pg(payload, 'axis') == 'reset'
|
|
)
|
|
return resized
|
|
# }}}
|
|
|
|
|
|
# close_tab {{{
|
|
@cmd(
|
|
'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 cmd_close_tab(global_opts, opts, args):
|
|
'''
|
|
match: Which tab to close
|
|
self: Boolean indicating whether to close the window the command is run in
|
|
'''
|
|
return {'match': opts.match, 'self': opts.self}
|
|
|
|
|
|
def close_tab(boss, window, payload):
|
|
pg = cmd_close_tab.payload_get
|
|
match = pg(payload, '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 pg(payload, 'self') else boss.active_tab]
|
|
for tab in tabs:
|
|
if window:
|
|
if tab:
|
|
boss.close_tab(tab)
|
|
# }}}
|
|
|
|
|
|
# new_window {{{
|
|
@cmd(
|
|
'Open new window',
|
|
'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 cmd_new_window(global_opts, opts, args):
|
|
'''
|
|
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
|
|
'''
|
|
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 new_window(boss, window, payload):
|
|
pg = cmd_new_window.payload_get
|
|
w = SpecialWindow(cmd=payload['args'] or None, override_title=pg(payload, 'title'), cwd=pg(payload, 'cwd'))
|
|
old_window = boss.active_window
|
|
if pg(payload, 'new_tab'):
|
|
boss._new_tab(w)
|
|
tab = boss.active_tab
|
|
if pg(payload, 'tab_title'):
|
|
tab.set_title(pg(payload, 'tab_title'))
|
|
wid = boss.active_window.id
|
|
if pg(payload, 'keep_focus') and old_window:
|
|
boss.set_active_window(old_window)
|
|
return None if pg(payload, 'no_response') else str(wid)
|
|
|
|
if pg(payload, 'window_type') == 'os':
|
|
boss._new_os_window(w)
|
|
wid = boss.active_window.id
|
|
if pg(payload, '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 pg(payload, 'no_response') else str(wid)
|
|
|
|
match = pg(payload, '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 pg(payload, 'keep_focus') and old_window:
|
|
boss.set_active_window(old_window)
|
|
return None if pg(payload, 'no_response') else str(w.id)
|
|
# }}}
|
|
|
|
|
|
# launch {{{
|
|
@cmd(
|
|
'Run an arbitrary process in a new window/tab',
|
|
' 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 cmd_launch(global_opts, opts, args):
|
|
'''
|
|
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"
|
|
'''
|
|
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 launch(boss, window, payload):
|
|
pg = cmd_launch.payload_get
|
|
default_opts = parse_launch_args()[0]
|
|
opts = {}
|
|
for key, default_value in default_opts.__dict__.items():
|
|
opts[key] = payload.get(key, default_value)
|
|
opts = Namespace(opts)
|
|
match = pg(payload, 'match')
|
|
if match:
|
|
tabs = tuple(boss.match_tabs(match))
|
|
if not tabs:
|
|
raise MatchError(match, 'tabs')
|
|
else:
|
|
tabs = [boss.active_tab]
|
|
if pg(payload, 'self') and window and window.tabref():
|
|
tabs = [window.tabref()]
|
|
tab = tabs[0]
|
|
w = do_launch(boss, opts, pg(payload, 'args') or None, target_tab=tab)
|
|
return None if pg(payload, 'no_response') else str(getattr(w, 'id', 0))
|
|
# }}}
|
|
|
|
|
|
# focus_window {{{
|
|
@cmd(
|
|
'Focus the specified window',
|
|
'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 cmd_focus_window(global_opts, opts, args):
|
|
'''
|
|
match: The window to focus
|
|
'''
|
|
if opts.no_response:
|
|
global_opts.no_command_response = True
|
|
return {'match': opts.match, 'no_response': opts.no_response}
|
|
|
|
|
|
def focus_window(boss, window, payload):
|
|
pg = cmd_focus_window.payload_get
|
|
windows = [window or boss.active_window]
|
|
match = pg(payload, '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
|
|
# }}}
|
|
|
|
|
|
# scroll_window {{{
|
|
@cmd(
|
|
'Scroll the specified window',
|
|
'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 cmd_scroll_window(global_opts, opts, args):
|
|
'''
|
|
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
|
|
'''
|
|
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 scroll_window(boss, window, payload):
|
|
pg = cmd_scroll_window.payload_get
|
|
windows = [window or boss.active_window]
|
|
match = pg(payload, 'match')
|
|
amt = pg(payload, '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()
|
|
# }}}
|
|
|
|
|
|
# focus_tab {{{
|
|
@cmd(
|
|
'Focus the specified tab',
|
|
'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 cmd_focus_tab(global_opts, opts, args):
|
|
'''
|
|
match: The tab to focus
|
|
'''
|
|
if opts.no_response:
|
|
global_opts.no_command_response = True
|
|
return {'match': opts.match}
|
|
|
|
|
|
def focus_tab(boss, window, payload):
|
|
pg = cmd_focus_tab.payload_get
|
|
match = pg(payload, 'match')
|
|
tabs = tuple(boss.match_tabs(match))
|
|
if not tabs:
|
|
raise MatchError(match, 'tabs')
|
|
tab = tabs[0]
|
|
boss.set_active_tab(tab)
|
|
# }}}
|
|
|
|
|
|
# get_text {{{
|
|
@cmd(
|
|
'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 cmd_get_text(global_opts, opts, args):
|
|
'''
|
|
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
|
|
'''
|
|
return {'match': opts.match, 'extent': opts.extent, 'ansi': opts.ansi, 'self': opts.self}
|
|
|
|
|
|
def get_text(boss, window, payload):
|
|
pg = cmd_get_text.payload_get
|
|
match = pg(payload, 'match')
|
|
if match:
|
|
windows = tuple(boss.match_windows(match))
|
|
if not windows:
|
|
raise MatchError(match)
|
|
else:
|
|
windows = [window if window and pg(payload, 'self') else boss.active_window]
|
|
window = windows[0]
|
|
if pg(payload, 'extent') == 'selection':
|
|
ans = window.text_for_selection()
|
|
else:
|
|
ans = window.as_text(as_ansi=bool(pg(payload, 'ansi')), add_history=pg(payload, 'extent') == 'all')
|
|
return ans
|
|
# }}}
|
|
|
|
|
|
# set_colors {{{
|
|
@cmd(
|
|
'Set terminal colors',
|
|
'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 cmd_set_colors(global_opts, opts, args):
|
|
'''
|
|
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
|
|
'''
|
|
from .rgb import color_as_int, Color
|
|
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 set_colors(boss, window, payload):
|
|
pg = cmd_set_colors.payload_get
|
|
from .rgb import color_as_int, Color
|
|
windows = windows_for_payload(boss, window, payload)
|
|
if pg(payload, 'reset'):
|
|
payload['colors'] = {k: color_as_int(v) for k, v in boss.startup_colors.items()}
|
|
payload['cursor_text_color'] = boss.startup_cursor_text_color
|
|
profiles = tuple(w.screen.color_profile for w in windows)
|
|
from .fast_data_types import patch_color_profiles
|
|
cursor_text_color = payload.get('cursor_text_color', False)
|
|
if isinstance(cursor_text_color, (tuple, list, Color)):
|
|
cursor_text_color = color_as_int(Color(*cursor_text_color))
|
|
patch_color_profiles(payload['colors'], cursor_text_color, profiles, pg(payload, 'configured'))
|
|
boss.patch_colors(payload['colors'], cursor_text_color, pg(payload, 'configured'))
|
|
default_bg_changed = 'background' in payload['colors']
|
|
for w in windows:
|
|
if default_bg_changed:
|
|
boss.default_bg_changed_for(w.id)
|
|
w.refresh()
|
|
# }}}
|
|
|
|
|
|
# get_colors {{{
|
|
@cmd(
|
|
'Get terminal colors',
|
|
'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 cmd_get_colors(global_opts, opts, args):
|
|
'''
|
|
match: The window to get the colors for
|
|
configured: Boolean indicating whether to get configured or current colors
|
|
'''
|
|
return {'configured': opts.configured, 'match': opts.match}
|
|
|
|
|
|
def get_colors(boss, window, payload):
|
|
from .rgb import Color, color_as_sharp, color_from_int
|
|
pg = cmd_get_colors.payload_get
|
|
ans = {k: getattr(boss.opts, k) for k in boss.opts if isinstance(getattr(boss.opts, k), Color)}
|
|
if not pg(payload, 'configured'):
|
|
windows = (window or boss.active_window,)
|
|
if pg(payload, 'match'):
|
|
windows = tuple(boss.match_windows(pg(payload, 'match')))
|
|
if not windows:
|
|
raise MatchError(pg(payload, '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)
|
|
# }}}
|
|
|
|
|
|
# set_background_opacity {{{
|
|
@cmd(
|
|
'Set the background_opacity',
|
|
'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 cmd_set_background_opacity(global_opts, opts, args):
|
|
'''
|
|
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
|
|
'''
|
|
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 set_background_opacity(boss, window, payload):
|
|
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)
|
|
for os_window_id in {w.os_window_id for w in windows}:
|
|
boss._set_os_window_background_opacity(os_window_id, payload['opacity'])
|
|
# }}}
|
|
|
|
|
|
# set_background_image {{{
|
|
@cmd(
|
|
'Set the background_image',
|
|
'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',))}
|
|
)
|
|
def cmd_set_background_image(global_opts, opts, args):
|
|
'''
|
|
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
|
|
'''
|
|
import imghdr
|
|
from uuid import uuid4
|
|
from base64 import standard_b64encode
|
|
if not args:
|
|
raise SystemExit('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':
|
|
raise SystemExit('{} 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)
|
|
|
|
|
|
class SetBackgroundImage:
|
|
|
|
current_img_id: Optional[str] = None
|
|
current_file_obj: Optional[BinaryIO] = None
|
|
|
|
def __call__(self, boss, window, payload):
|
|
from base64 import standard_b64decode
|
|
import tempfile
|
|
pg = cmd_set_background_image.payload_get
|
|
data = pg(payload, 'data')
|
|
if data != '-':
|
|
img_id = pg(payload, '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)
|
|
os_windows = tuple({w.os_window_id for w in windows})
|
|
layout = pg(payload, '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, pg(payload, 'configured'), layout)
|
|
except ValueError as err:
|
|
err.hide_traceback = True
|
|
raise
|
|
|
|
|
|
set_background_image = SetBackgroundImage()
|
|
# }}}
|
|
|
|
|
|
# disable_ligatures {{{
|
|
@cmd(
|
|
'Control ligature rendering',
|
|
'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 cmd_disable_ligatures(global_opts, opts, args):
|
|
'''
|
|
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
|
|
'''
|
|
if not args:
|
|
raise SystemExit(
|
|
'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'):
|
|
raise SystemExit('{} 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 disable_ligatures(boss, window, payload):
|
|
windows = windows_for_payload(boss, window, payload)
|
|
boss.disable_ligatures_in(windows, payload['strategy'])
|
|
# }}}
|
|
|
|
|
|
# kitten {{{
|
|
@cmd(
|
|
'Run a kitten',
|
|
'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 cmd_kitten(global_opts, opts, args):
|
|
'''
|
|
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
|
|
'''
|
|
if len(args) < 1:
|
|
raise SystemExit('Must specify kitten name')
|
|
return {'match': opts.match, 'args': list(args)[1:], 'kitten': args[0]}
|
|
|
|
|
|
def kitten(boss, window, payload):
|
|
windows = [window or boss.active_window]
|
|
pg = cmd_kitten.payload_get
|
|
match = pg(payload, '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['kitten'], args=tuple(payload.get('args', ())), window=window)
|
|
break
|
|
# }}}
|
|
|
|
|
|
def cli_params_for(func):
|
|
return (func.options_spec or '\n').format, func.argspec, func.desc, '{} @ {}'.format(appname, func.name)
|
|
|
|
|
|
def parse_subcommand_cli(func, args):
|
|
opts, items = parse_args(args[1:], *cli_params_for(func))
|
|
if func.args_count is not None and func.args_count != len(items):
|
|
if func.args_count == 0:
|
|
raise SystemExit('Unknown extra argument(s) supplied to {}'.format(func.name))
|
|
raise SystemExit('Must specify exactly {} argument(s) for {}'.format(func.args_count, func.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)
|