kitty/kitty/cli.py

1080 lines
35 KiB
Python

#!/usr/bin/env python3
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
import os
import re
import shlex
import sys
from collections import deque
from dataclasses import dataclass
from enum import Enum, auto
from typing import Any, Callable, Dict, FrozenSet, Iterator, List, Match, Optional, Sequence, Tuple, Type, TypeVar, Union, cast
from .cli_stub import CLIOptions
from .conf.utils import resolve_config
from .constants import appname, clear_handled_signals, config_dir, default_pager_for_help, defconf, is_macos, str_version, website_url
from .fast_data_types import wcswidth
from .options.types import Options as KittyOpts
from .types import run_once
from .typing import BadLineType, TypedDict
class CompletionType(Enum):
file = auto()
directory = auto()
keyword = auto()
special = auto()
none = auto()
class CompletionRelativeTo(Enum):
cwd = auto()
config_dir = auto()
@dataclass
class CompletionSpec:
type: CompletionType = CompletionType.none
kwds: Sequence[str] = ()
extensions: Sequence[str] = ()
mime_patterns: Sequence[str] = ()
group: str = ''
relative_to: CompletionRelativeTo = CompletionRelativeTo.cwd
@staticmethod
def from_string(raw: str) -> 'CompletionSpec':
self = CompletionSpec()
for x in shlex.split(raw):
ck, vv = x.split(':', 1)
if ck == 'type':
self.type = getattr(CompletionType, vv)
elif ck == 'kwds':
self.kwds = tuple(vv.split(','))
elif ck == 'ext':
self.extensions = tuple(vv.split(','))
elif ck == 'group':
self.group = vv
elif ck == 'mime':
self.mime_patterns = tuple(vv.split(','))
elif ck == 'relative':
if vv == 'conf':
self.relative_to = CompletionRelativeTo.config_dir
else:
raise ValueError(f'Unknown completion relative to value: {vv}')
else:
raise KeyError(f'Unknown completion property: {ck}')
return self
def as_go_code(self, go_name: str, sep: str = ': ') -> Iterator[str]:
completers = []
if self.kwds:
kwds = (f'"{serialize_as_go_string(x)}"' for x in self.kwds)
g = (self.group if self.type is CompletionType.keyword else '') or "Keywords"
completers.append(f'cli.NamesCompleter("{serialize_as_go_string(g)}", ' + ', '.join(kwds) + ')')
relative_to = 'CONFIG' if self.relative_to is CompletionRelativeTo.config_dir else 'CWD'
if self.type is CompletionType.file:
g = serialize_as_go_string(self.group or 'Files')
added = False
if self.extensions:
added = True
pats = (f'"*.{ext}"' for ext in self.extensions)
completers.append(f'cli.FnmatchCompleter("{g}", cli.{relative_to}, ' + ', '.join(pats) + ')')
if self.mime_patterns:
added = True
completers.append(f'cli.MimepatCompleter("{g}", cli.{relative_to}, ' + ', '.join(f'"{p}"' for p in self.mime_patterns) + ')')
if not added:
completers.append(f'cli.FnmatchCompleter("{g}", cli.{relative_to}, "*")')
if self.type is CompletionType.directory:
g = serialize_as_go_string(self.group or 'Directories')
completers.append(f'cli.DirectoryCompleter("{g}", cli.{relative_to})')
if self.type is CompletionType.special:
completers.append(self.group)
if len(completers) > 1:
yield f'{go_name}{sep}cli.ChainCompleters(' + ', '.join(completers) + ')'
elif completers:
yield f'{go_name}{sep}{completers[0]}'
class OptionDict(TypedDict):
dest: str
name: str
aliases: FrozenSet[str]
help: str
choices: FrozenSet[str]
type: str
default: Optional[str]
condition: bool
completion: CompletionSpec
def serialize_as_go_string(x: str) -> str:
return x.replace('\\', '\\\\').replace('\n', '\\n').replace('"', '\\"')
go_type_map = {
'bool-set': 'bool', 'bool-reset': 'bool', 'int': 'int', 'float': 'float64',
'': 'string', 'list': '[]string', 'choices': 'string', 'str': 'string'}
class GoOption:
def __init__(self, x: OptionDict) -> None:
flags = sorted(x['aliases'], key=len)
short = ''
self.aliases = []
if len(flags) > 1 and not flags[0].startswith("--"):
short = flags[0][1:]
self.short, self.long = short, x['name'].replace('_', '-')
for f in flags:
q = f[2:] if f.startswith('--') else f[1:]
self.aliases.append(q)
self.type = x['type']
if x['choices']:
self.type = 'choices'
self.default = x['default']
self.obj_dict = x
self.go_type = go_type_map[self.type]
if x['dest']:
self.go_var_name = ''.join(x.capitalize() for x in x['dest'].replace('-', '_').split('_'))
else:
self.go_var_name = ''.join(x.capitalize() for x in self.long.replace('-', '_').split('_'))
self.help_text = serialize_as_go_string(self.obj_dict['help'].strip())
def struct_declaration(self) -> str:
return f'{self.go_var_name} {self.go_type}'
def as_option(self, cmd_name: str = 'cmd', depth: int = 0, group: str = '') -> str:
add = f'AddToGroup("{serialize_as_go_string(group)}", ' if group else 'Add('
aliases = ' '.join(sorted(self.obj_dict['aliases']))
ans = f'''{cmd_name}.{add}cli.OptionSpec{{
Name: "{serialize_as_go_string(aliases)}",
Type: "{self.type}",
Dest: "{serialize_as_go_string(self.go_var_name)}",
Help: "{self.help_text}",
'''
if self.type in ('choice', 'choices'):
c = ', '.join(self.sorted_choices)
cx = ', '.join(f'"{serialize_as_go_string(x)}"' for x in self.sorted_choices)
ans += f'\nChoices: "{serialize_as_go_string(c)}",\n'
ans += f'\nCompleter: cli.NamesCompleter("Choices for {self.long}", {cx}),'
elif self.obj_dict['completion'].type is not CompletionType.none:
ans += ''.join(self.obj_dict['completion'].as_go_code('Completer', ': ')) + ','
if depth > 0:
ans += f'\nDepth: {depth},\n'
if self.default:
ans += f'\nDefault: "{serialize_as_go_string(self.default)}",\n'
return ans + '})'
@property
def sorted_choices(self) -> List[str]:
choices = sorted(self.obj_dict['choices'])
choices.remove(self.default or '')
choices.insert(0, self.default or '')
return choices
def go_options_for_seq(seq: 'OptionSpecSeq') -> Iterator[GoOption]:
for x in seq:
if not isinstance(x, str):
yield GoOption(x)
CONFIG_HELP = '''\
Specify a path to the configuration file(s) to use. All configuration files are
merged onto the builtin :file:`{conf_name}.conf`, overriding the builtin values.
This option can be specified multiple times to read multiple configuration files
in sequence, which are merged. Use the special value :code:`NONE` to not load
any config file.
If this option is not specified, config files are searched for in the order:
:file:`$XDG_CONFIG_HOME/{appname}/{conf_name}.conf`,
:file:`~/.config/{appname}/{conf_name}.conf`,{macos_confpath}
:file:`$XDG_CONFIG_DIRS/{appname}/{conf_name}.conf`. The first one that exists
is used as the config file.
If the environment variable :envvar:`KITTY_CONFIG_DIRECTORY` is specified, that
directory is always used and the above searching does not happen.
If :file:`/etc/xdg/{appname}/{conf_name}.conf` exists, it is merged before (i.e.
with lower priority) than any user config files. It can be used to specify
system-wide defaults for all users. You can use either :code:`-` or
:file:`/dev/stdin` to read the config from STDIN.
'''.replace(
'{macos_confpath}',
(' :file:`~/Library/Preferences/{appname}/{conf_name}.conf`,' if is_macos else ''), 1
)
def surround(x: str, start: int, end: int) -> str:
if sys.stdout.isatty():
x = f'\033[{start}m{x}\033[{end}m'
return x
role_map: Dict[str, Callable[[str], str]] = {}
def role(func: Callable[[str], str]) -> Callable[[str], str]:
role_map[func.__name__] = func
return func
@role
def emph(x: str) -> str:
return surround(x, 91, 39)
@role
def cyan(x: str) -> str:
return surround(x, 96, 39)
@role
def green(x: str) -> str:
return surround(x, 32, 39)
@role
def blue(x: str) -> str:
return surround(x, 34, 39)
@role
def yellow(x: str) -> str:
return surround(x, 93, 39)
@role
def italic(x: str) -> str:
return surround(x, 3, 23)
@role
def bold(x: str) -> str:
return surround(x, 1, 22)
@role
def title(x: str) -> str:
return blue(bold(x))
@role
def opt(text: str) -> str:
return bold(text)
@role
def option(x: str) -> str:
idx = x.rfind('--')
if idx < 0:
idx = x.find('-')
if idx > -1:
x = x[idx:]
return bold(x.rstrip('>'))
@role
def code(x: str) -> str:
return cyan(x)
def text_and_target(x: str) -> Tuple[str, str]:
parts = x.split('<', 1)
return parts[0].strip(), parts[-1].rstrip('>')
@role
def term(x: str) -> str:
return ref_hyperlink(x, 'term-')
@role
def kbd(x: str) -> str:
return x
@role
def env(x: str) -> str:
return ref_hyperlink(x, 'envvar-')
role_map['envvar'] = role_map['env']
@run_once
def hostname() -> str:
from .utils import get_hostname
return get_hostname(fallback='localhost')
def hyperlink_for_url(url: str, text: str) -> str:
if sys.stdout.isatty():
return f'\x1b]8;;{url}\x1b\\\x1b[4:3;58:5:4m{text}\x1b[4:0;59m\x1b]8;;\x1b\\'
return text
def hyperlink_for_path(path: str, text: str) -> str:
path = os.path.abspath(path).replace(os.sep, "/")
if os.path.isdir(path):
path += path.rstrip("/") + "/"
return hyperlink_for_url(f'file://{hostname()}{path}', text)
@role
def file(x: str) -> str:
if x == 'kitty.conf':
x = hyperlink_for_path(os.path.join(config_dir, x), x)
return italic(x)
@role
def doc(x: str) -> str:
t, q = text_and_target(x)
if t == q:
from .conf.types import ref_map
m = ref_map()['doc']
q = q.strip('/')
if q in m:
x = f'{m[q]} <{t}>'
return ref_hyperlink(x, 'doc-')
def ref_hyperlink(x: str, prefix: str = '') -> str:
t, q = text_and_target(x)
url = f'kitty+doc://{hostname()}/#ref={prefix}{q}'
t = re.sub(r':([a-z]+):`([^`]+)`', r'\2', t)
return hyperlink_for_url(url, t)
@role
def ref(x: str) -> str:
return ref_hyperlink(x)
@role
def ac(x: str) -> str:
return ref_hyperlink(x, 'action-')
@role
def iss(x: str) -> str:
return ref_hyperlink(x, 'issues-')
@role
def pull(x: str) -> str:
return ref_hyperlink(x, 'pull-')
@role
def disc(x: str) -> str:
return ref_hyperlink(x, 'discussions-')
OptionSpecSeq = List[Union[str, OptionDict]]
def parse_option_spec(spec: Optional[str] = None) -> Tuple[OptionSpecSeq, OptionSpecSeq]:
if spec is None:
spec = options_spec()
NORMAL, METADATA, HELP = 'NORMAL', 'METADATA', 'HELP'
state = NORMAL
lines = spec.splitlines()
prev_line = ''
prev_indent = 0
seq: OptionSpecSeq = []
disabled: OptionSpecSeq = []
mpat = re.compile('([a-z]+)=(.+)')
current_cmd: OptionDict = {
'dest': '', 'aliases': frozenset(), 'help': '', 'choices': frozenset(),
'type': '', 'condition': False, 'default': None, 'completion': CompletionSpec(), 'name': ''
}
empty_cmd = current_cmd
def indent_of_line(x: str) -> int:
return len(x) - len(x.lstrip())
for line in lines:
line = line.rstrip()
if state is NORMAL:
if not line:
continue
if line.startswith('# '):
seq.append(line[2:])
continue
if line.startswith('--'):
parts = line.split(' ')
defdest = parts[0][2:].replace('-', '_')
current_cmd = {
'dest': defdest, 'aliases': frozenset(parts), 'help': '',
'choices': frozenset(), 'type': '', 'name': defdest,
'default': None, 'condition': True, 'completion': CompletionSpec(),
}
state = METADATA
continue
raise ValueError(f'Invalid option spec, unexpected line: {line}')
elif state is METADATA:
m = mpat.match(line)
if m is None:
state = HELP
current_cmd['help'] += line
else:
k, v = m.group(1), m.group(2)
if k == 'choices':
vals = tuple(x.strip() for x in v.split(','))
current_cmd['choices'] = frozenset(vals)
if current_cmd['default'] is None:
current_cmd['default'] = vals[0]
else:
if k == 'default':
current_cmd['default'] = v
elif k == 'type':
if v == 'choice':
v = 'choices'
current_cmd['type'] = v
elif k == 'dest':
current_cmd['dest'] = v
elif k == 'condition':
current_cmd['condition'] = bool(eval(v))
elif k == 'completion':
current_cmd['completion'] = CompletionSpec.from_string(v)
elif state is HELP:
if line:
current_indent = indent_of_line(line)
if current_indent > 1:
if prev_indent == 0:
current_cmd['help'] += '\n'
else:
line = line.strip()
prev_indent = current_indent
spc = '' if current_cmd['help'].endswith('\n') else ' '
current_cmd['help'] += spc + line
else:
prev_indent = 0
if prev_line:
current_cmd['help'] += '\n' if current_cmd['help'].endswith('::') else '\n\n'
else:
state = NORMAL
(seq if current_cmd.get('condition', True) else disabled).append(current_cmd)
current_cmd = empty_cmd
prev_line = line
if current_cmd is not empty_cmd:
(seq if current_cmd.get('condition', True) else disabled).append(current_cmd)
return seq, disabled
def prettify(text: str) -> str:
def identity(x: str) -> str:
return x
def sub(m: 'Match[str]') -> str:
role, text = m.group(1, 2)
return role_map.get(role, identity)(text)
text = re.sub(r':([a-z]+):`([^`]+)`', sub, text)
return text
def prettify_rst(text: str) -> str:
return re.sub(r':([a-z]+):`([^`]+)`(=[^\s.]+)', r':\1:`\2`:code:`\3`', text)
def version(add_rev: bool = False) -> str:
rev = ''
from . import fast_data_types
if add_rev and hasattr(fast_data_types, 'KITTY_VCS_REV'):
rev = f' ({fast_data_types.KITTY_VCS_REV[:10]})'
return '{} {}{} created by {}'.format(italic(appname), green(str_version), rev, title('Kovid Goyal'))
def wrap(text: str, limit: int = 80) -> Iterator[str]:
if not text.strip():
yield ''
return
in_escape = 0
current_line: List[str] = []
escapes: List[str] = []
current_word: List[str] = []
current_line_length = 0
def print_word(ch: str = '') -> Iterator[str]:
nonlocal current_word, current_line, escapes, current_line_length
cw = ''.join(current_word)
w = wcswidth(cw)
if current_line_length + w > limit:
yield ''.join(current_line)
current_line = []
current_line_length = 0
cw = cw.strip()
current_word = [cw]
if escapes:
current_line.append(''.join(escapes))
escapes = []
if current_word:
current_line.append(cw)
current_line_length += w
current_word = []
if ch:
current_word.append(ch)
for i, ch in enumerate(text):
if in_escape > 0:
if in_escape == 1 and ch in '[]':
in_escape = 2 if ch == '[' else 3
if (in_escape == 2 and ch == 'm') or (in_escape == 3 and ch == '\\' and text[i-1] == '\x1b'):
in_escape = 0
escapes.append(ch)
continue
if ch == '\x1b':
in_escape = 1
if current_word:
yield from print_word()
escapes.append(ch)
continue
if current_word and ch.isspace() and ch != '\xa0':
yield from print_word(ch)
else:
current_word.append(ch)
yield from print_word()
if current_line:
yield ''.join(current_line)
def get_defaults_from_seq(seq: OptionSpecSeq) -> Dict[str, Any]:
ans: Dict[str, Any] = {}
for opt in seq:
if not isinstance(opt, str):
ans[opt['dest']] = defval_for_opt(opt)
return ans
default_msg = ('''\
Run the :italic:`{appname}` terminal emulator. You can also specify the
:italic:`program` to run inside :italic:`{appname}` as normal arguments
following the :italic:`options`.
For example: {appname} --hold sh -c "echo hello, world"
For comprehensive documentation for kitty, please see: {url}''').format(
appname=appname, url=website_url())
class PrintHelpForSeq:
allow_pager = True
def __call__(self, seq: OptionSpecSeq, usage: Optional[str], message: Optional[str], appname: str) -> None:
from kitty.utils import screen_size_function
screen_size = screen_size_function()
try:
linesz = min(screen_size().cols, 76)
except OSError:
linesz = 76
blocks: List[str] = []
a = blocks.append
def wa(text: str, indent: int = 0, leading_indent: Optional[int] = None) -> None:
if leading_indent is None:
leading_indent = indent
j = '\n' + (' ' * indent)
lines: List[str] = []
for ln in text.splitlines():
lines.extend(wrap(ln, limit=linesz - indent))
a((' ' * leading_indent) + j.join(lines))
usage = '[program-to-run ...]' if usage is None else usage
optstring = '[options] ' if seq else ''
a('{}: {} {}{}'.format(title('Usage'), bold(yellow(appname)), optstring, usage))
a('')
message = message or default_msg
# replace rst literal code block syntax
message = message.replace('::\n\n', ':\n\n')
wa(prettify(message))
a('')
if seq:
a('{}:'.format(title('Options')))
for opt in seq:
if isinstance(opt, str):
a(f'{title(opt)}:')
continue
help_text = opt['help']
if help_text == '!':
continue # hidden option
a(' ' + ', '.join(map(green, sorted(opt['aliases'], reverse=True))))
defval = opt.get('default')
if not opt.get('type', '').startswith('bool-'):
if defval:
dt = '=[{}]'.format(italic(defval))
blocks[-1] += dt
if opt.get('help'):
t = help_text.replace('%default', str(defval)).strip()
# replace rst literal code block syntax
t = t.replace('::\n\n', ':\n\n')
t = t.replace('#placeholder_for_formatting#', '')
wa(prettify(t), indent=4)
if opt.get('choices'):
wa('Choices: {}'.format(', '.join(opt['choices'])), indent=4)
a('')
text = '\n'.join(blocks) + '\n\n' + version()
if print_help_for_seq.allow_pager and sys.stdout.isatty():
import subprocess
try:
p = subprocess.Popen(default_pager_for_help, stdin=subprocess.PIPE, preexec_fn=clear_handled_signals)
except FileNotFoundError:
print(text)
else:
try:
p.communicate(text.encode('utf-8'))
except KeyboardInterrupt:
raise SystemExit(1)
raise SystemExit(p.wait())
else:
print(text)
print_help_for_seq = PrintHelpForSeq()
def seq_as_rst(
seq: OptionSpecSeq,
usage: Optional[str],
message: Optional[str],
appname: Optional[str],
heading_char: str = '-'
) -> str:
import textwrap
blocks: List[str] = []
a = blocks.append
usage = '[program-to-run ...]' if usage is None else usage
optstring = '[options] ' if seq else ''
a('.. highlight:: sh')
a('.. code-block:: sh')
a('')
a(f' {appname} {optstring}{usage}')
a('')
message = message or default_msg
a(prettify_rst(message))
a('')
if seq:
a('Options')
a(heading_char * 30)
for opt in seq:
if isinstance(opt, str):
a(opt)
a('~' * (len(opt) + 10))
continue
help_text = opt['help']
if help_text == '!':
continue # hidden option
defn = '.. option:: '
if not opt.get('type', '').startswith('bool-'):
val_name = ' <{}>'.format(opt['dest'].upper())
else:
val_name = ''
a(defn + ', '.join(o + val_name for o in sorted(opt['aliases'])))
if opt.get('help'):
defval = opt.get('default')
t = help_text.replace('%default', str(defval)).strip()
t = t.replace('#placeholder_for_formatting#', '')
a('')
a(textwrap.indent(prettify_rst(t), ' ' * 4))
if defval is not None:
a(textwrap.indent(f'Default: :code:`{defval}`', ' ' * 4))
if opt.get('choices'):
a(textwrap.indent('Choices: {}'.format(', '.join(f':code:`{c}`' for c in sorted(opt['choices']))), ' ' * 4))
a('')
text = '\n'.join(blocks)
return text
def as_type_stub(seq: OptionSpecSeq, disabled: OptionSpecSeq, class_name: str, extra_fields: Sequence[str] = ()) -> str:
from itertools import chain
ans: List[str] = [f'class {class_name}:']
for opt in chain(seq, disabled):
if isinstance(opt, str):
continue
name = opt['dest']
otype = opt['type'] or 'str'
if otype in ('str', 'int', 'float'):
t = otype
if t == 'str' and defval_for_opt(opt) is None:
t = 'typing.Optional[str]'
elif otype == 'list':
t = 'typing.Sequence[str]'
elif otype in ('choice', 'choices'):
if opt['choices']:
t = 'typing.Literal[{}]'.format(','.join(f'{x!r}' for x in opt['choices']))
else:
t = 'str'
elif otype.startswith('bool-'):
t = 'bool'
else:
raise ValueError(f'Unknown CLI option type: {otype}')
ans.append(f' {name}: {t}')
for x in extra_fields:
ans.append(f' {x}')
return '\n'.join(ans) + '\n\n\n'
def defval_for_opt(opt: OptionDict) -> Any:
dv: Any = opt.get('default')
typ = opt.get('type', '')
if typ.startswith('bool-'):
if dv is None:
dv = False if typ == 'bool-set' else True
else:
dv = dv.lower() in ('true', 'yes', 'y')
elif typ == 'list':
dv = []
elif typ in ('int', 'float'):
dv = (int if typ == 'int' else float)(dv or 0)
return dv
class Options:
def __init__(self, seq: OptionSpecSeq, usage: Optional[str], message: Optional[str], appname: Optional[str]):
self.alias_map = {}
self.seq = seq
self.names_map: Dict[str, OptionDict] = {}
self.values_map: Dict[str, Any] = {}
self.usage, self.message, self.appname = usage, message, appname
for opt in seq:
if isinstance(opt, str):
continue
for alias in opt['aliases']:
self.alias_map[alias] = opt
name = opt['dest']
self.names_map[name] = opt
self.values_map[name] = defval_for_opt(opt)
def opt_for_alias(self, alias: str) -> OptionDict:
opt = self.alias_map.get(alias)
if opt is None:
raise SystemExit(f'Unknown option: {emph(alias)}')
return opt
def needs_arg(self, alias: str) -> bool:
if alias in ('-h', '--help'):
print_help_for_seq(self.seq, self.usage, self.message, self.appname or appname)
raise SystemExit(0)
opt = self.opt_for_alias(alias)
if opt['dest'] == 'version':
print(version())
raise SystemExit(0)
typ = opt.get('type', '')
return not typ.startswith('bool-')
def process_arg(self, alias: str, val: Any = None) -> None:
opt = self.opt_for_alias(alias)
typ = opt.get('type', '')
name = opt['dest']
nmap = {'float': float, 'int': int}
if typ == 'bool-set':
self.values_map[name] = True
elif typ == 'bool-reset':
self.values_map[name] = False
elif typ == 'list':
self.values_map.setdefault(name, [])
self.values_map[name].append(val)
elif typ == 'choices':
choices = opt['choices']
if val not in choices:
raise SystemExit('{} is not a valid value for the {} option. Valid values are: {}'.format(
val, emph(alias), ', '.join(choices)))
self.values_map[name] = val
elif typ in nmap:
f = nmap[typ]
try:
self.values_map[name] = f(val)
except Exception:
raise SystemExit('{} is not a valid value for the {} option, a number is required.'.format(
val, emph(alias)))
else:
self.values_map[name] = val
def parse_cmdline(oc: Options, disabled: OptionSpecSeq, ans: Any, args: Optional[List[str]] = None) -> List[str]:
NORMAL, EXPECTING_ARG = 'NORMAL', 'EXPECTING_ARG'
state = NORMAL
dargs = deque(sys.argv[1:] if args is None else args)
leftover_args: List[str] = []
current_option = None
while dargs:
arg = dargs.popleft()
if state is NORMAL:
if arg.startswith('-'):
if arg == '--':
leftover_args = list(dargs)
break
parts = arg.split('=', 1)
needs_arg = oc.needs_arg(parts[0])
if not needs_arg:
if len(parts) != 1:
raise SystemExit(f'The {emph(parts[0])} option does not accept arguments')
oc.process_arg(parts[0])
continue
if len(parts) == 1:
current_option = parts[0]
state = EXPECTING_ARG
continue
oc.process_arg(parts[0], parts[1])
else:
leftover_args = [arg] + list(dargs)
break
elif current_option is not None:
oc.process_arg(current_option, arg)
current_option, state = None, NORMAL
if state is EXPECTING_ARG:
raise SystemExit(f'An argument is required for the option: {emph(arg)}')
for key, val in oc.values_map.items():
setattr(ans, key, val)
for opt in disabled:
if not isinstance(opt, str):
setattr(ans, opt['dest'], defval_for_opt(opt))
return leftover_args
def options_spec() -> str:
if not hasattr(options_spec, 'ans'):
OPTIONS = '''
--class
dest=cls
default={appname}
condition=not is_macos
Set the class part of the :italic:`WM_CLASS` window property. On Wayland, it
sets the app id.
--name
condition=not is_macos
Set the name part of the :italic:`WM_CLASS` property. Defaults to using the
value from :option:`{appname} --class`.
--title -T
Set the OS window title. This will override any title set by the program running
inside kitty, permanently fixing the OS window's title. So only use this if you
are running a program that does not set titles.
--config -c
type=list
completion=type:file ext:conf group:"Config files" kwds:none,NONE
{config_help}
--override -o
type=list
completion=type:special group:complete_kitty_override
Override individual configuration options, can be specified multiple times.
Syntax: :italic:`name=value`. For example: :option:`{appname} -o` font_size=20
--directory --working-directory -d
default=.
completion=type:directory
Change to the specified directory when launching.
--detach
type=bool-set
condition=not is_macos
Detach from the controlling terminal, if any. Not available on macOS.
--session
completion=type:file ext:session relative:conf group:"Session files"
Path to a file containing the startup :italic:`session` (tabs, windows, layout,
programs). Use - to read from STDIN. See the :file:`README` file for details and
an example. Environment variables in the file name are expanded,
relative paths are resolved relative to the kitty configuration directory.
--hold
type=bool-set
Remain open after child process exits. Note that this only affects the first
window. You can quit by either using the close window shortcut or pressing any
key.
--single-instance -1
type=bool-set
If specified only a single instance of :italic:`{appname}` will run. New
invocations will instead create a new top-level window in the existing
:italic:`{appname}` instance. This allows :italic:`{appname}` to share a single
sprite cache on the GPU and also reduces startup time. You can also have
separate groups of :italic:`{appname}` instances by using the :option:`{appname}
--instance-group` option.
--instance-group
Used in combination with the :option:`{appname} --single-instance` option. All
:italic:`{appname}` invocations with the same :option:`{appname}
--instance-group` will result in new windows being created in the first
:italic:`{appname}` instance within that group.
--wait-for-single-instance-window-close
type=bool-set
Normally, when using :option:`{appname} --single-instance`, :italic:`{appname}`
will open a new window in an existing instance and quit immediately. With this
option, it will not quit till the newly opened window is closed. Note that if no
previous instance is found, then :italic:`{appname}` will wait anyway,
regardless of this option.
--listen-on
completion=type:special group:complete_kitty_listen_on
Listen on the specified socket address for control messages. For example,
:option:`{appname} --listen-on`=unix:/tmp/mykitty or :option:`{appname}
--listen-on`=tcp:localhost:12345. On Linux systems, you can also use abstract
UNIX sockets, not associated with a file, like this: :option:`{appname}
--listen-on`=unix:@mykitty. Environment variables are expanded and relative
paths are resolved with respect to the temporary directory. To control kitty,
you can send commands to it with :italic:`{appname} @` using the
:option:`{appname} @ --to` option to specify this address. Note that if you run
:italic:`{appname} @` within a kitty window, there is no need to specify the
:option:`{appname} @ --to` option as it will automatically read from the
environment. Note that this will be ignored unless :opt:`allow_remote_control`
is set to either: :code:`yes`, :code:`socket` or :code:`socket-only`. For UNIX
sockets, this can also be specified in :file:`{conf_name}.conf`.
--start-as
type=choices
default=normal
choices=normal,fullscreen,maximized,minimized
Control how the initial kitty window is created.
# Debugging options
--version -v
type=bool-set
The current {appname} version.
--dump-commands
type=bool-set
Output commands received from child process to STDOUT.
--replay-commands
Replay previously dumped commands. Specify the path to a dump file previously
created by :option:`{appname} --dump-commands`. You
can open a new kitty window to replay the commands with::
{appname} sh -c "{appname} --replay-commands /path/to/dump/file; read"
--dump-bytes
Path to file in which to store the raw bytes received from the child process.
--debug-rendering --debug-gl
type=bool-set
Debug rendering commands. This will cause all OpenGL calls to check for errors
instead of ignoring them. Also prints out miscellaneous debug information.
Useful when debugging rendering problems.
--debug-input --debug-keyboard
dest=debug_keyboard
type=bool-set
Print out key and mouse events as they are received.
--debug-font-fallback
type=bool-set
Print out information about the selection of fallback fonts for characters not
present in the main font.
--watcher
completion=type:file ext:py relative:conf group:"Watcher files"
This option is deprecated in favor of the :opt:`watcher` option in
:file:`{conf_name}.conf` and should not be used.
--execute -e
type=bool-set
!
'''
setattr(options_spec, 'ans', OPTIONS.format(
appname=appname, conf_name=appname,
config_help=CONFIG_HELP.format(appname=appname, conf_name=appname),
))
ans: str = getattr(options_spec, 'ans')
return ans
def options_for_completion() -> OptionSpecSeq:
raw = '--help -h\ntype=bool-set\nShow help for {appname} command line options\n\n{raw}'.format(
appname=appname, raw=options_spec())
return parse_option_spec(raw)[0]
def option_spec_as_rst(
ospec: Callable[[], str] = options_spec,
usage: Optional[str] = None, message: Optional[str] = None, appname: Optional[str] = None,
heading_char: str = '-'
) -> str:
options = parse_option_spec(ospec())
seq, disabled = options
oc = Options(seq, usage, message, appname)
return seq_as_rst(oc.seq, oc.usage, oc.message, oc.appname, heading_char=heading_char)
T = TypeVar('T')
def parse_args(
args: Optional[List[str]] = None,
ospec: Callable[[], str] = options_spec,
usage: Optional[str] = None,
message: Optional[str] = None,
appname: Optional[str] = None,
result_class: Optional[Type[T]] = None,
) -> Tuple[T, List[str]]:
options = parse_option_spec(ospec())
seq, disabled = options
oc = Options(seq, usage, message, appname)
if result_class is not None:
ans = result_class()
else:
ans = cast(T, CLIOptions())
return ans, parse_cmdline(oc, disabled, ans, args=args)
SYSTEM_CONF = f'/etc/xdg/{appname}/{appname}.conf'
def default_config_paths(conf_paths: Sequence[str]) -> Tuple[str, ...]:
return tuple(resolve_config(SYSTEM_CONF, defconf, conf_paths))
def create_opts(args: CLIOptions, accumulate_bad_lines: Optional[List[BadLineType]] = None) -> KittyOpts:
from .config import load_config
config = default_config_paths(args.config)
# Does not cover the case where `name =` when `=` is the value.
pat = re.compile(r'^([a-zA-Z0-9_]+)[ \t]*=')
overrides = (pat.sub(r'\1 ', a.lstrip()) for a in args.override or ())
opts = load_config(*config, overrides=overrides, accumulate_bad_lines=accumulate_bad_lines)
return opts
def create_default_opts() -> KittyOpts:
from .config import load_config
config = default_config_paths(())
opts = load_config(*config)
return opts