Add type checking for the various CLI options objects

This commit is contained in:
Kovid Goyal
2020-03-05 15:47:12 +05:30
parent 0f4e7921ee
commit f05890719d
27 changed files with 487 additions and 157 deletions

View File

@@ -7,6 +7,7 @@ from contextlib import suppress
from kitty.cli import parse_args from kitty.cli import parse_args
from kitty.constants import cache_dir from kitty.constants import cache_dir
from kitty.cli_stub import AskCLIOptions
from ..tui.operations import alternate_screen, styled from ..tui.operations import alternate_screen, styled
from ..tui.handler import result_handler from ..tui.handler import result_handler
@@ -89,7 +90,7 @@ def main(args):
from kitty.shell import init_readline from kitty.shell import init_readline
msg = 'Ask the user for input' msg = 'Ask the user for input'
try: try:
args, items = parse_args(args[1:], option_text, '', msg, 'kitty ask') args, items = parse_args(args[1:], option_text, '', msg, 'kitty ask', result_class=AskCLIOptions)
except SystemExit as e: except SystemExit as e:
if e.code != 0: if e.code != 0:
print(e.args[0]) print(e.args[0])

View File

@@ -6,6 +6,7 @@ import os
import sys import sys
from kitty.cli import parse_args from kitty.cli import parse_args
from kitty.cli_stub import ClipboardCLIOptions
from ..tui.handler import Handler from ..tui.handler import Handler
from ..tui.loop import Loop from ..tui.loop import Loop
@@ -81,7 +82,7 @@ usage = ''
def main(args): def main(args):
args, items = parse_args(args[1:], OPTIONS, usage, help_text, 'kitty +kitten clipboard') args, items = parse_args(args[1:], OPTIONS, usage, help_text, 'kitty +kitten clipboard', result_class=ClipboardCLIOptions)
if items: if items:
raise SystemExit('Unrecognized extra command line arguments') raise SystemExit('Unrecognized extra command line arguments')
data = None data = None

View File

@@ -12,7 +12,7 @@ from kitty.conf.definition import config_lines
from kitty.constants import config_dir from kitty.constants import config_dir
from kitty.rgb import color_as_sgr from kitty.rgb import color_as_sgr
from .config_data import type_map, all_options from .config_data import type_convert, all_options
defaults = None defaults = None
@@ -89,7 +89,7 @@ def parse_config(lines, check_keys=True):
parse_config_base( parse_config_base(
lines, lines,
defaults, defaults,
type_map, type_convert,
special_handling, special_handling,
ans, ans,
check_keys=check_keys check_keys=check_keys

View File

@@ -3,10 +3,10 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
from functools import partial
# Utils {{{ # Utils {{{
from functools import partial
from gettext import gettext as _ from gettext import gettext as _
from typing import Dict, Union from typing import Any, Dict, Union
from kitty.conf.definition import Option, Shortcut, option_func from kitty.conf.definition import Option, Shortcut, option_func
from kitty.conf.utils import positive_int, python_string, to_color from kitty.conf.utils import positive_int, python_string, to_color
@@ -120,4 +120,9 @@ k('prev_match', '<', 'scroll_to prev-match', _('Scroll to previous search match'
k('search_forward_simple', 'f', 'start_search substring forward', _('Search forward (no regex)')) k('search_forward_simple', 'f', 'start_search substring forward', _('Search forward (no regex)'))
k('search_backward_simple', 'b', 'start_search substring backward', _('Search backward (no regex)')) k('search_backward_simple', 'b', 'start_search substring backward', _('Search backward (no regex)'))
type_map = {o.name: o.option_type for o in all_options.values() if isinstance(o, Option)}
def type_convert(name: str, val: Any) -> Any:
o = all_options.get(name)
if isinstance(o, Option):
val = o.option_type(val)
return val

View File

@@ -15,6 +15,7 @@ from functools import partial
from gettext import gettext as _ from gettext import gettext as _
from kitty.cli import CONFIG_HELP, parse_args from kitty.cli import CONFIG_HELP, parse_args
from kitty.cli_stub import DiffCLIOptions
from kitty.constants import appname from kitty.constants import appname
from kitty.fast_data_types import wcswidth from kitty.fast_data_types import wcswidth
from kitty.key_encoding import RELEASE, enter_key, key_defs as K from kitty.key_encoding import RELEASE, enter_key, key_defs as K
@@ -542,7 +543,7 @@ def get_remote_file(path):
def main(args): def main(args):
warnings.showwarning = showwarning warnings.showwarning = showwarning
args, items = parse_args(args[1:], OPTIONS, usage, help_text, 'kitty +kitten diff') args, items = parse_args(args[1:], OPTIONS, usage, help_text, 'kitty +kitten diff', result_class=DiffCLIOptions)
if len(items) != 2: if len(items) != 2:
raise SystemExit('You must specify exactly two files/directories to compare') raise SystemExit('You must specify exactly two files/directories to compare')
left, right = items left, right = items

View File

@@ -11,6 +11,7 @@ from gettext import gettext as _
from itertools import repeat from itertools import repeat
from kitty.cli import parse_args from kitty.cli import parse_args
from kitty.cli_stub import HintsCLIOptions
from kitty.fast_data_types import set_clipboard_string from kitty.fast_data_types import set_clipboard_string
from kitty.key_encoding import key_defs as K, backspace_key, enter_key from kitty.key_encoding import key_defs as K, backspace_key, enter_key
from kitty.utils import screen_size_function from kitty.utils import screen_size_function
@@ -508,7 +509,7 @@ usage = ''
def parse_hints_args(args): def parse_hints_args(args):
return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten hints') return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten hints', result_class=HintsCLIOptions)
def main(args): def main(args):

View File

@@ -14,6 +14,7 @@ from math import ceil
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from kitty.cli import parse_args from kitty.cli import parse_args
from kitty.cli_stub import IcatCLIOptions
from kitty.constants import appname from kitty.constants import appname
from kitty.utils import TTYIO, fit_image, screen_size_function from kitty.utils import TTYIO, fit_image, screen_size_function
@@ -343,7 +344,7 @@ def process_single_item(item, args, url_pat=None, maybe_dir=True):
def main(args=sys.argv): def main(args=sys.argv):
args, items = parse_args(args[1:], options_spec, usage, help_text, '{} +kitten icat'.format(appname)) args, items = parse_args(args[1:], options_spec, usage, help_text, '{} +kitten icat'.format(appname), result_class=IcatCLIOptions)
if args.print_window_size: if args.print_window_size:
screen_size_function.ans = None screen_size_function.ans = None

View File

@@ -8,6 +8,7 @@ import subprocess
import sys import sys
from kitty.cli import parse_args from kitty.cli import parse_args
from kitty.cli_stub import PanelCLIOptions
from kitty.constants import is_macos from kitty.constants import is_macos
OPTIONS = r''' OPTIONS = r'''
@@ -48,7 +49,7 @@ usage = 'program-to-run'
def parse_panel_args(args): def parse_panel_args(args):
return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten panel') return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten panel', result_class=PanelCLIOptions)
def call_xprop(*cmd, silent=False): def call_xprop(*cmd, silent=False):

View File

@@ -6,6 +6,7 @@
import sys import sys
from kitty.cli import parse_args from kitty.cli import parse_args
from kitty.cli_stub import ResizeCLIOptions
from kitty.cmds import cmap, parse_subcommand_cli from kitty.cmds import cmap, parse_subcommand_cli
from kitty.constants import version from kitty.constants import version
from kitty.key_encoding import CTRL, RELEASE, key_defs as K from kitty.key_encoding import CTRL, RELEASE, key_defs as K
@@ -119,7 +120,7 @@ The base vertical increment.
def main(args): def main(args):
msg = 'Resize the current window' msg = 'Resize the current window'
try: try:
args, items = parse_args(args[1:], OPTIONS, '', msg, 'resize_window') args, items = parse_args(args[1:], OPTIONS, '', msg, 'resize_window', result_class=ResizeCLIOptions)
except SystemExit as e: except SystemExit as e:
if e.code != 0: if e.code != 0:
print(e.args[0], file=sys.stderr) print(e.args[0], file=sys.stderr)

View File

@@ -7,6 +7,7 @@ import sys
from contextlib import suppress from contextlib import suppress
from kitty.cli import parse_args from kitty.cli import parse_args
from kitty.cli_stub import ErrorCLIOptions
from ..tui.operations import styled from ..tui.operations import styled
@@ -19,7 +20,7 @@ The title for the error message.
def real_main(args): def real_main(args):
msg = 'Show an error message' msg = 'Show an error message'
args, items = parse_args(args[1:], OPTIONS, '', msg, 'hints') args, items = parse_args(args[1:], OPTIONS, '', msg, 'hints', result_class=ErrorCLIOptions)
error_message = sys.stdin.buffer.read().decode('utf-8') error_message = sys.stdin.buffer.read().decode('utf-8')
sys.stdin = open(os.ctermid()) sys.stdin = open(os.ctermid())
print(styled(args.title, fg_intense=True, fg='red', bold=True)) print(styled(args.title, fg_intense=True, fg='red', bold=True))

View File

@@ -11,6 +11,7 @@ from functools import lru_cache
from gettext import gettext as _ from gettext import gettext as _
from kitty.cli import parse_args from kitty.cli import parse_args
from kitty.cli_stub import UnicodeCLIOptions
from kitty.config import cached_values_for from kitty.config import cached_values_for
from kitty.constants import config_dir from kitty.constants import config_dir
from kitty.fast_data_types import is_emoji_presentation_base, wcswidth from kitty.fast_data_types import is_emoji_presentation_base, wcswidth
@@ -530,7 +531,7 @@ default form specified in the unicode standard for the symbol is used.
def parse_unicode_input_args(args): def parse_unicode_input_args(args):
return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten unicode_input') return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten unicode_input', result_class=UnicodeCLIOptions)
def main(args): def main(args):

View File

@@ -304,8 +304,9 @@ class Boss:
else: else:
msg = json.loads(msg) msg = json.loads(msg)
if isinstance(msg, dict) and msg.get('cmd') == 'new_instance': if isinstance(msg, dict) and msg.get('cmd') == 'new_instance':
from .cli_stub import CLIOptions
startup_id = msg.get('startup_id') startup_id = msg.get('startup_id')
args, rest = parse_args(msg['args'][1:]) args, rest = parse_args(msg['args'][1:], result_class=CLIOptions)
args.args = rest args.args = rest
opts = create_opts(args) opts = create_opts(args)
if not os.path.isabs(args.directory): if not os.path.isabs(args.directory):

View File

@@ -6,9 +6,30 @@ import os
import re import re
import sys import sys
from collections import deque from collections import deque
from typing import (
Any, Callable, Dict, FrozenSet, Iterator, List, Optional, Tuple, Type,
TypeVar, Union, cast
)
from .conf.utils import resolve_config from .conf.utils import resolve_config
from .constants import appname, defconf, is_macos, is_wayland, str_version from .constants import appname, defconf, is_macos, is_wayland, str_version
from .cli_stub import CLIOptions
from .options_stub import Options as OptionsStub
try:
from typing import TypedDict
class OptionDict(TypedDict):
dest: str
aliases: FrozenSet[str]
help: str
choices: FrozenSet[str]
type: str
default: Optional[str]
condition: bool
except ImportError:
OptionDict = Dict[str, Any] # type: ignore
CONFIG_HELP = '''\ CONFIG_HELP = '''\
Specify a path to the configuration file(s) to use. All configuration files are Specify a path to the configuration file(s) to use. All configuration files are
@@ -34,49 +55,49 @@ defaults for all users.
) )
def surround(x, start, end): def surround(x: str, start: int, end: int) -> str:
if sys.stdout.isatty(): if sys.stdout.isatty():
x = '\033[{}m{}\033[{}m'.format(start, x, end) x = '\033[{}m{}\033[{}m'.format(start, x, end)
return x return x
def emph(x): def emph(x: str) -> str:
return surround(x, 91, 39) return surround(x, 91, 39)
def cyan(x): def cyan(x: str) -> str:
return surround(x, 96, 39) return surround(x, 96, 39)
def green(x): def green(x: str) -> str:
return surround(x, 32, 39) return surround(x, 32, 39)
def blue(x): def blue(x: str) -> str:
return surround(x, 34, 39) return surround(x, 34, 39)
def yellow(x): def yellow(x: str) -> str:
return surround(x, 93, 39) return surround(x, 93, 39)
def italic(x): def italic(x: str) -> str:
return surround(x, 3, 23) return surround(x, 3, 23)
def bold(x): def bold(x: str) -> str:
return surround(x, 1, 22) return surround(x, 1, 22)
def title(x): def title(x: str) -> str:
return blue(bold(x)) return blue(bold(x))
def opt(text): def opt(text: str) -> str:
return text return text
def option(x): def option(x: str) -> str:
idx = x.find('-') idx = x.find('-')
if idx > -1: if idx > -1:
x = x[idx:] x = x[idx:]
@@ -84,33 +105,39 @@ def option(x):
return ' '.join(parts) return ' '.join(parts)
def code(x): def code(x: str) -> str:
return x return x
def kbd(x): def kbd(x: str) -> str:
return x return x
def env(x): def env(x: str) -> str:
return italic(x) return italic(x)
def file(x): def file(x: str) -> str:
return italic(x) return italic(x)
def parse_option_spec(spec=None): OptionSpecSeq = List[Union[str, OptionDict]]
def parse_option_spec(spec: Optional[str] = None) -> Tuple[OptionSpecSeq, OptionSpecSeq]:
if spec is None: if spec is None:
spec = options_spec() spec = options_spec()
NORMAL, METADATA, HELP = 'NORMAL', 'METADATA', 'HELP' NORMAL, METADATA, HELP = 'NORMAL', 'METADATA', 'HELP'
state = NORMAL state = NORMAL
lines = spec.splitlines() lines = spec.splitlines()
prev_line = '' prev_line = ''
seq = [] seq: OptionSpecSeq = []
disabled = [] disabled: OptionSpecSeq = []
mpat = re.compile('([a-z]+)=(.+)') mpat = re.compile('([a-z]+)=(.+)')
current_cmd = None current_cmd: OptionDict = {
'dest': '', 'aliases': frozenset(), 'help': '', 'choices': frozenset(), 'type': '', 'condition': False, 'default': None
}
empty_cmd = current_cmd
for line in lines: for line in lines:
line = line.rstrip() line = line.rstrip()
@@ -122,7 +149,10 @@ def parse_option_spec(spec=None):
continue continue
if line.startswith('--'): if line.startswith('--'):
parts = line.split(' ') parts = line.split(' ')
current_cmd = {'dest': parts[0][2:].replace('-', '_'), 'aliases': frozenset(parts), 'help': ''} current_cmd = {
'dest': parts[0][2:].replace('-', '_'), 'aliases': frozenset(parts), 'help': '', 'choices': frozenset(), 'type': '',
'default': None, 'condition': True
}
state = METADATA state = METADATA
continue continue
raise ValueError('Invalid option spec, unexpected line: {}'.format(line)) raise ValueError('Invalid option spec, unexpected line: {}'.format(line))
@@ -133,11 +163,17 @@ def parse_option_spec(spec=None):
current_cmd['help'] += line current_cmd['help'] += line
else: else:
k, v = m.group(1), m.group(2) k, v = m.group(1), m.group(2)
if k == 'condition':
v = eval(v)
current_cmd[k] = v
if k == 'choices': if k == 'choices':
current_cmd['choices'] = {x.strip() for x in current_cmd['choices'].split(',')} current_cmd['choices'] = frozenset(x.strip() for x in v.split(','))
else:
if k == 'default':
current_cmd['default'] = v
elif k == 'type':
current_cmd['type'] = v
elif k == 'dest':
current_cmd['dest'] = v
elif k == 'condition':
current_cmd['condition'] = bool(eval(v))
elif state is HELP: elif state is HELP:
if line: if line:
spc = '' if current_cmd['help'].endswith('\n') else ' ' spc = '' if current_cmd['help'].endswith('\n') else ' '
@@ -148,15 +184,15 @@ def parse_option_spec(spec=None):
else: else:
state = NORMAL state = NORMAL
(seq if current_cmd.get('condition', True) else disabled).append(current_cmd) (seq if current_cmd.get('condition', True) else disabled).append(current_cmd)
current_cmd = None current_cmd = empty_cmd
prev_line = line prev_line = line
if current_cmd is not None: if current_cmd is not empty_cmd:
(seq if current_cmd.get('condition', True) else disabled).append(current_cmd) (seq if current_cmd.get('condition', True) else disabled).append(current_cmd)
return seq, disabled return seq, disabled
def prettify(text): def prettify(text: str) -> str:
role_map = globals() role_map = globals()
def sub(m): def sub(m):
@@ -167,11 +203,11 @@ def prettify(text):
return text return text
def prettify_rst(text): def prettify_rst(text: str) -> str:
return re.sub(r':([a-z]+):`([^`]+)`(=[^\s.]+)', r':\1:`\2`:code:`\3`', text) return re.sub(r':([a-z]+):`([^`]+)`(=[^\s.]+)', r':\1:`\2`:code:`\3`', text)
def version(add_rev=False): def version(add_rev: bool = False) -> str:
rev = '' rev = ''
from . import fast_data_types from . import fast_data_types
if add_rev and hasattr(fast_data_types, 'KITTY_VCS_REV'): if add_rev and hasattr(fast_data_types, 'KITTY_VCS_REV'):
@@ -179,7 +215,7 @@ def version(add_rev=False):
return '{} {}{} created by {}'.format(italic(appname), green(str_version), rev, title('Kovid Goyal')) return '{} {}{} created by {}'.format(italic(appname), green(str_version), rev, title('Kovid Goyal'))
def wrap(text, limit=80): def wrap(text: str, limit: int = 80) -> Iterator[str]:
NORMAL, IN_FORMAT = 'NORMAL', 'IN_FORMAT' NORMAL, IN_FORMAT = 'NORMAL', 'IN_FORMAT'
state = NORMAL state = NORMAL
last_space_at = None last_space_at = None
@@ -203,7 +239,7 @@ def wrap(text, limit=80):
last_space_at = None last_space_at = None
chars_in_line = i - breaks[-1] chars_in_line = i - breaks[-1]
lines = [] lines: List[str] = []
for b in reversed(breaks): for b in reversed(breaks):
lines.append(text[b:].lstrip()) lines.append(text[b:].lstrip())
text = text[:b] text = text[:b]
@@ -212,8 +248,8 @@ def wrap(text, limit=80):
return reversed(lines) return reversed(lines)
def get_defaults_from_seq(seq): def get_defaults_from_seq(seq: OptionSpecSeq) -> Dict[str, Any]:
ans = {} ans: Dict[str, Any] = {}
for opt in seq: for opt in seq:
if not isinstance(opt, str): if not isinstance(opt, str):
ans[opt['dest']] = defval_for_opt(opt) ans[opt['dest']] = defval_for_opt(opt)
@@ -232,21 +268,21 @@ class PrintHelpForSeq:
allow_pager = True allow_pager = True
def __call__(self, seq, usage, message, appname): def __call__(self, seq: OptionSpecSeq, usage: Optional[str], message: Optional[str], appname: str) -> None:
from kitty.utils import screen_size_function from kitty.utils import screen_size_function
screen_size = screen_size_function() screen_size = screen_size_function()
try: try:
linesz = min(screen_size().cols, 76) linesz = min(screen_size().cols, 76)
except OSError: except OSError:
linesz = 76 linesz = 76
blocks = [] blocks: List[str] = []
a = blocks.append a = blocks.append
def wa(text, indent=0, leading_indent=None): def wa(text, indent=0, leading_indent=None):
if leading_indent is None: if leading_indent is None:
leading_indent = indent leading_indent = indent
j = '\n' + (' ' * indent) j = '\n' + (' ' * indent)
lines = [] lines: List[str] = []
for l in text.splitlines(): for l in text.splitlines():
if l: if l:
lines.extend(wrap(l, limit=linesz - indent)) lines.extend(wrap(l, limit=linesz - indent))
@@ -279,7 +315,7 @@ class PrintHelpForSeq:
wa(prettify(t.strip()), indent=4) wa(prettify(t.strip()), indent=4)
if defval is not None: if defval is not None:
wa('Default: {}'.format(defval), indent=4) wa('Default: {}'.format(defval), indent=4)
if 'choices' in opt: if opt.get('choices'):
wa('Choices: {}'.format(', '.join(opt['choices'])), indent=4) wa('Choices: {}'.format(', '.join(opt['choices'])), indent=4)
a('') a('')
@@ -299,9 +335,9 @@ class PrintHelpForSeq:
print_help_for_seq = PrintHelpForSeq() print_help_for_seq = PrintHelpForSeq()
def seq_as_rst(seq, usage, message, appname, heading_char='-'): def seq_as_rst(seq: OptionSpecSeq, usage: Optional[str], message: Optional[str], appname: Optional[str], heading_char: str = '-') -> str:
import textwrap import textwrap
blocks = [] blocks: List[str] = []
a = blocks.append a = blocks.append
usage = '[program-to-run ...]' if usage is None else usage usage = '[program-to-run ...]' if usage is None else usage
@@ -338,7 +374,7 @@ def seq_as_rst(seq, usage, message, appname, heading_char='-'):
a(textwrap.indent(prettify_rst(t), ' ' * 4)) a(textwrap.indent(prettify_rst(t), ' ' * 4))
if defval is not None: if defval is not None:
a(textwrap.indent('Default: :code:`{}`'.format(defval), ' ' * 4)) a(textwrap.indent('Default: :code:`{}`'.format(defval), ' ' * 4))
if 'choices' in opt: if opt.get('choices'):
a(textwrap.indent('Choices: :code:`{}`'.format(', '.join(sorted(opt['choices']))), ' ' * 4)) a(textwrap.indent('Choices: :code:`{}`'.format(', '.join(sorted(opt['choices']))), ' ' * 4))
a('') a('')
@@ -346,8 +382,30 @@ def seq_as_rst(seq, usage, message, appname, heading_char='-'):
return text return text
def defval_for_opt(opt): def as_type_stub(seq: OptionSpecSeq, disabled: OptionSpecSeq, class_name: str) -> str:
dv = opt.get('default') from itertools import chain
ans: List[str] = ['class {}:'.format(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
elif otype == 'list':
t = 'typing.Sequence[str]'
elif otype in ('choice', 'choices'):
t = 'str'
elif otype.startswith('bool-'):
t = 'bool'
else:
raise ValueError('Unknown CLI option type: {}'.format(otype))
ans.append(' {}: {}'.format(name, t))
return '\n'.join(ans) + '\n\n\n'
def defval_for_opt(opt: OptionDict) -> Any:
dv: Any = opt.get('default')
typ = opt.get('type', '') typ = opt.get('type', '')
if typ.startswith('bool-'): if typ.startswith('bool-'):
if dv is None: if dv is None:
@@ -363,11 +421,11 @@ def defval_for_opt(opt):
class Options: class Options:
def __init__(self, seq, usage, message, appname): def __init__(self, seq: OptionSpecSeq, usage: Optional[str], message: Optional[str], appname: Optional[str]):
self.alias_map = {} self.alias_map = {}
self.seq = seq self.seq = seq
self.names_map = {} self.names_map: Dict[str, OptionDict] = {}
self.values_map = {} self.values_map: Dict[str, Any] = {}
self.usage, self.message, self.appname = usage, message, appname self.usage, self.message, self.appname = usage, message, appname
for opt in seq: for opt in seq:
if isinstance(opt, str): if isinstance(opt, str):
@@ -378,13 +436,13 @@ class Options:
self.names_map[name] = opt self.names_map[name] = opt
self.values_map[name] = defval_for_opt(opt) self.values_map[name] = defval_for_opt(opt)
def opt_for_alias(self, alias): def opt_for_alias(self, alias: str) -> OptionDict:
opt = self.alias_map.get(alias) opt = self.alias_map.get(alias)
if opt is None: if opt is None:
raise SystemExit('Unknown option: {}'.format(emph(alias))) raise SystemExit('Unknown option: {}'.format(emph(alias)))
return opt return opt
def needs_arg(self, alias): def needs_arg(self, alias: str) -> bool:
if alias in ('-h', '--help'): if alias in ('-h', '--help'):
print_help_for_seq(self.seq, self.usage, self.message, self.appname or appname) print_help_for_seq(self.seq, self.usage, self.message, self.appname or appname)
raise SystemExit(0) raise SystemExit(0)
@@ -395,7 +453,7 @@ class Options:
typ = opt.get('type', '') typ = opt.get('type', '')
return not typ.startswith('bool-') return not typ.startswith('bool-')
def process_arg(self, alias, val=None): def process_arg(self, alias: str, val: Any = None) -> None:
opt = self.opt_for_alias(alias) opt = self.opt_for_alias(alias)
typ = opt.get('type', '') typ = opt.get('type', '')
name = opt['dest'] name = opt['dest']
@@ -424,23 +482,15 @@ class Options:
self.values_map[name] = val self.values_map[name] = val
class Namespace: def parse_cmdline(oc: Options, disabled: OptionSpecSeq, ans: Any, args: Optional[List[str]] = None) -> List[str]:
def __init__(self, kwargs):
for name, val in kwargs.items():
setattr(self, name, val)
def parse_cmdline(oc, disabled, args=None):
NORMAL, EXPECTING_ARG = 'NORMAL', 'EXPECTING_ARG' NORMAL, EXPECTING_ARG = 'NORMAL', 'EXPECTING_ARG'
state = NORMAL state = NORMAL
if args is None: dargs = deque(sys.argv[1:] if args is None else args)
args = sys.argv[1:] leftover_args: List[str] = []
args = deque(args)
current_option = None current_option = None
while args: while dargs:
arg = args.popleft() arg = dargs.popleft()
if state is NORMAL: if state is NORMAL:
if arg.startswith('-'): if arg.startswith('-'):
if arg == '--': if arg == '--':
@@ -458,21 +508,23 @@ def parse_cmdline(oc, disabled, args=None):
continue continue
oc.process_arg(parts[0], parts[1]) oc.process_arg(parts[0], parts[1])
else: else:
args = [arg] + list(args) leftover_args = [arg] + list(dargs)
break break
else: elif current_option is not None:
oc.process_arg(current_option, arg) oc.process_arg(current_option, arg)
current_option, state = None, NORMAL current_option, state = None, NORMAL
if state is EXPECTING_ARG: if state is EXPECTING_ARG:
raise SystemExit('An argument is required for the option: {}'.format(emph(arg))) raise SystemExit('An argument is required for the option: {}'.format(emph(arg)))
ans = Namespace(oc.values_map) for key, val in oc.values_map.items():
setattr(ans, key, val)
for opt in disabled: for opt in disabled:
setattr(ans, opt['dest'], defval_for_opt(opt)) if not isinstance(opt, str):
return ans, list(args) setattr(ans, opt['dest'], defval_for_opt(opt))
return leftover_args
def options_spec(): def options_spec() -> str:
if not hasattr(options_spec, 'ans'): if not hasattr(options_spec, 'ans'):
OPTIONS = ''' OPTIONS = '''
--class --class
@@ -618,31 +670,49 @@ Print out information about the system and kitty configuration.
type=bool-set type=bool-set
! !
''' '''
options_spec.ans = OPTIONS.format( setattr(options_spec, 'ans', OPTIONS.format(
appname=appname, config_help=CONFIG_HELP.format(appname=appname, conf_name=appname) appname=appname, config_help=CONFIG_HELP.format(appname=appname, conf_name=appname)
) ))
return options_spec.ans return getattr(options_spec, 'ans')
def options_for_completion(): def options_for_completion() -> OptionSpecSeq:
raw = '--help -h\ntype=bool-set\nShow help for {appname} command line options\n\n{raw}'.format( raw = '--help -h\ntype=bool-set\nShow help for {appname} command line options\n\n{raw}'.format(
appname=appname, raw=options_spec()) appname=appname, raw=options_spec())
return parse_option_spec(raw)[0] return parse_option_spec(raw)[0]
def option_spec_as_rst(ospec=options_spec, usage=None, message=None, appname=None, heading_char='-'): 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:
options = parse_option_spec(ospec()) options = parse_option_spec(ospec())
seq, disabled = options seq, disabled = options
oc = Options(seq, usage, message, appname) oc = Options(seq, usage, message, appname)
return seq_as_rst(oc.seq, oc.usage, oc.message, oc.appname, heading_char=heading_char) return seq_as_rst(oc.seq, oc.usage, oc.message, oc.appname, heading_char=heading_char)
def parse_args(args=None, ospec=options_spec, usage=None, message=None, appname=None): 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: Type[T] = None,
) -> Tuple[T, List[str]]:
options = parse_option_spec(ospec()) options = parse_option_spec(ospec())
seq, disabled = options seq, disabled = options
oc = Options(seq, usage, message, appname) oc = Options(seq, usage, message, appname)
return parse_cmdline(oc, disabled, args=args) 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 = '/etc/xdg/kitty/kitty.conf' SYSTEM_CONF = '/etc/xdg/kitty/kitty.conf'
@@ -655,8 +725,8 @@ def print_shortcut(key_sequence, action):
mmap = {m[len('GLFW_MOD_'):].lower(): x for m, x in v.items() if m.startswith('GLFW_MOD_')} mmap = {m[len('GLFW_MOD_'):].lower(): x for m, x in v.items() if m.startswith('GLFW_MOD_')}
kmap = {k[len('GLFW_KEY_'):].lower(): x for k, x in v.items() if k.startswith('GLFW_KEY_')} kmap = {k[len('GLFW_KEY_'):].lower(): x for k, x in v.items() if k.startswith('GLFW_KEY_')}
krmap = {v: k for k, v in kmap.items()} krmap = {v: k for k, v in kmap.items()}
print_shortcut.maps = mmap, krmap setattr(print_shortcut, 'maps', (mmap, krmap))
mmap, krmap = print_shortcut.maps mmap, krmap = getattr(print_shortcut, 'maps')
keys = [] keys = []
for key in key_sequence: for key in key_sequence:
names = [] names = []
@@ -667,7 +737,8 @@ def print_shortcut(key_sequence, action):
if key: if key:
if is_native: if is_native:
from .fast_data_types import GLFW_KEY_UNKNOWN, glfw_get_key_name from .fast_data_types import GLFW_KEY_UNKNOWN, glfw_get_key_name
names.append(glfw_get_key_name(GLFW_KEY_UNKNOWN, key)) kn = glfw_get_key_name(GLFW_KEY_UNKNOWN, key) or 'Unknown key'
names.append(kn)
else: else:
names.append(krmap[key]) names.append(krmap[key])
keys.append('+'.join(names)) keys.append('+'.join(names))
@@ -700,12 +771,12 @@ def flatten_sequence_map(m):
return ans return ans
def compare_opts(opts): def compare_opts(opts: OptionsStub) -> None:
from .config import defaults, load_config from .config import defaults, load_config
print('\nConfig options different from defaults:') print('\nConfig options different from defaults:')
default_opts = load_config() default_opts = load_config()
changed_opts = [ changed_opts = [
f for f in sorted(defaults._fields) f for f in sorted(defaults._fields) # type: ignore
if f not in ('key_definitions', 'keymap', 'sequence_map') and getattr(opts, f) != getattr(defaults, f) if f not in ('key_definitions', 'keymap', 'sequence_map') and getattr(opts, f) != getattr(defaults, f)
] ]
field_len = max(map(len, changed_opts)) if changed_opts else 20 field_len = max(map(len, changed_opts)) if changed_opts else 20
@@ -713,9 +784,9 @@ def compare_opts(opts):
for f in changed_opts: for f in changed_opts:
print(title(fmt.format(f)), getattr(opts, f)) print(title(fmt.format(f)), getattr(opts, f))
final, initial = opts.keymap, default_opts.keymap final_, initial_ = opts.keymap, default_opts.keymap
final = {(k,): v for k, v in final.items()} final = {(k,): v for k, v in final_.items()}
initial = {(k,): v for k, v in initial.items()} initial = {(k,): v for k, v in initial_.items()}
final_s, initial_s = map(flatten_sequence_map, (opts.sequence_map, default_opts.sequence_map)) final_s, initial_s = map(flatten_sequence_map, (opts.sequence_map, default_opts.sequence_map))
final.update(final_s) final.update(final_s)
initial.update(initial_s) initial.update(initial_s)

64
kitty/cli_stub.py Normal file
View File

@@ -0,0 +1,64 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
class CLIOptions:
pass
LaunchCLIOptions = AskCLIOptions = ClipboardCLIOptions = DiffCLIOptions = CLIOptions
HintsCLIOptions = IcatCLIOptions = PanelCLIOptions = ResizeCLIOptions = CLIOptions
ErrorCLIOptions = UnicodeCLIOptions = CLIOptions
def generate_stub() -> None:
from .cli import parse_option_spec, as_type_stub
from .conf.definition import save_type_stub
text = 'import typing\n\n\n'
def do(otext=None, cls: str = 'CLIOptions'):
nonlocal text
text += as_type_stub(*parse_option_spec(otext), class_name=cls)
do()
from .launch import options_spec
do(options_spec(), 'LaunchCLIOptions')
from kittens.ask.main import option_text
do(option_text(), 'AskCLIOptions')
from kittens.clipboard.main import OPTIONS
do(OPTIONS(), 'ClipboardCLIOptions')
from kittens.diff.main import OPTIONS
do(OPTIONS(), 'DiffCLIOptions')
from kittens.hints.main import OPTIONS
do(OPTIONS(), 'HintsCLIOptions')
from kittens.icat.main import OPTIONS
do(OPTIONS, 'IcatCLIOptions')
from kittens.panel.main import OPTIONS
do(OPTIONS(), 'PanelCLIOptions')
from kittens.resize_window.main import OPTIONS
do(OPTIONS(), 'ResizeCLIOptions')
from kittens.show_error.main import OPTIONS
do(OPTIONS(), 'ErrorCLIOptions')
from kittens.unicode_input.main import OPTIONS
do(OPTIONS(), 'UnicodeCLIOptions')
save_type_stub(text, __file__)
if __name__ == '__main__':
import subprocess
subprocess.Popen([
'kitty', '+runpy',
'from kitty.cli_stub import generate_stub; generate_stub()'
])

118
kitty/cli_stub.pyi Normal file
View File

@@ -0,0 +1,118 @@
# Update this file by running: python kitty/cli_stub.py
import typing
class CLIOptions:
cls: str
name: str
title: str
config: typing.Sequence[str]
override: typing.Sequence[str]
directory: str
detach: bool
session: str
hold: bool
single_instance: bool
instance_group: str
wait_for_single_instance_window_close: bool
listen_on: str
start_as: str
version: bool
dump_commands: bool
replay_commands: str
dump_bytes: str
debug_gl: bool
debug_keyboard: bool
debug_font_fallback: bool
debug_config: bool
execute: bool
class LaunchCLIOptions:
window_title: str
tab_title: str
type: str
keep_focus: bool
cwd: str
env: typing.Sequence[str]
copy_colors: bool
copy_cmdline: bool
copy_env: bool
location: str
allow_remote_control: bool
stdin_source: str
stdin_add_formatting: bool
stdin_add_line_wrap_markers: bool
marker: str
class AskCLIOptions:
type: str
message: str
name: str
class ClipboardCLIOptions:
get_clipboard: bool
use_primary: bool
wait_for_completion: bool
class DiffCLIOptions:
context: int
config: typing.Sequence[str]
override: typing.Sequence[str]
class HintsCLIOptions:
program: typing.Sequence[str]
type: str
regex: str
linenum_action: str
url_prefixes: str
word_characters: str
minimum_match_length: int
multiple: bool
multiple_joiner: str
add_trailing_space: str
hints_offset: int
alphabet: str
ascending: bool
customize_processing: str
class IcatCLIOptions:
align: str
place: str
scale_up: bool
clear: bool
transfer_mode: str
detect_support: bool
detection_timeout: float
print_window_size: bool
stdin: str
silent: bool
z_index: str
class PanelCLIOptions:
lines: int
columns: int
edge: str
config: typing.Sequence[str]
override: typing.Sequence[str]
class ResizeCLIOptions:
horizontal_increment: int
vertical_increment: int
class ErrorCLIOptions:
title: str
class UnicodeCLIOptions:
emoji_variation: str

View File

@@ -9,7 +9,7 @@ from contextlib import suppress
from typing import Any, BinaryIO, Callable, Dict, List, Optional from typing import Any, BinaryIO, Callable, Dict, List, Optional
from .cli import ( from .cli import (
Namespace, get_defaults_from_seq, parse_args, parse_option_spec get_defaults_from_seq, parse_args, parse_option_spec
) )
from .config import parse_config, parse_send_text_bytes from .config import parse_config, parse_send_text_bytes
from .constants import appname from .constants import appname
@@ -42,7 +42,7 @@ class UnknownLayout(ValueError):
hide_traceback = True hide_traceback = True
CommandFunction = Callable[[Namespace, Namespace, List[str]], Optional[Dict[str, Any]]] CommandFunction = Callable[[Any, Any, List[str]], Optional[Dict[str, Any]]]
cmap: Dict[str, CommandFunction] = {} cmap: Dict[str, CommandFunction] = {}
@@ -906,13 +906,16 @@ def cmd_launch(global_opts, opts, args):
return ans return ans
class GlobalOpts:
pass
def launch(boss, window, payload): def launch(boss, window, payload):
pg = cmd_launch.payload_get pg = cmd_launch.payload_get
default_opts = parse_launch_args()[0] default_opts = parse_launch_args()[0]
opts = {} opts = GlobalOpts()
for key, default_value in default_opts.__dict__.items(): for key, default_value in default_opts.__dict__.items():
opts[key] = payload.get(key, default_value) setattr(opts, key, payload.get(key, default_value))
opts = Namespace(opts)
match = pg(payload, 'match') match = pg(payload, 'match')
if match: if match:
tabs = tuple(boss.match_tabs(match)) tabs = tuple(boss.match_tabs(match))
@@ -1444,8 +1447,12 @@ def cli_params_for(func):
return (func.options_spec or '\n').format, func.argspec, func.desc, '{} @ {}'.format(appname, func.name) return (func.options_spec or '\n').format, func.argspec, func.desc, '{} @ {}'.format(appname, func.name)
class SubCommandOptions:
pass
def parse_subcommand_cli(func, args): def parse_subcommand_cli(func, args):
opts, items = parse_args(args[1:], *cli_params_for(func)) opts, items = parse_args(args[1:], *cli_params_for(func), result_class=SubCommandOptions)
if func.args_count is not None and func.args_count != len(items): if func.args_count is not None and func.args_count != len(items):
if func.args_count == 0: if func.args_count == 0:
raise SystemExit('Unknown extra argument(s) supplied to {}'.format(func.name)) raise SystemExit('Unknown extra argument(s) supplied to {}'.format(func.name))
@@ -1455,4 +1462,4 @@ def parse_subcommand_cli(func, args):
def display_subcommand_help(func): def display_subcommand_help(func):
with suppress(SystemExit): with suppress(SystemExit):
parse_args(['--help'], (func.options_spec or '\n').format, func.argspec, func.desc, func.name) parse_args(['--help'], (func.options_spec or '\n').format, func.argspec, func.desc, func.name, result_class=SubCommandOptions)

View File

@@ -4,7 +4,9 @@
import re import re
from functools import partial from functools import partial
from typing import Any, Dict, List, Set, Tuple, Union, get_type_hints, Optional from typing import (
Any, Dict, Iterator, List, Optional, Set, Tuple, Union, get_type_hints
)
from .utils import to_bool from .utils import to_bool
@@ -26,7 +28,7 @@ class Option:
__slots__ = 'name', 'group', 'long_text', 'option_type', 'defval_as_string', 'add_to_default', 'add_to_docs', 'line' __slots__ = 'name', 'group', 'long_text', 'option_type', 'defval_as_string', 'add_to_default', 'add_to_docs', 'line'
def __init__(self, name, group, defval, option_type, long_text, add_to_default, add_to_docs): def __init__(self, name: str, group: str, defval: str, option_type: Any, long_text: str, add_to_default: bool, add_to_docs: bool):
self.name, self.group = name, group self.name, self.group = name, group
self.long_text, self.option_type = long_text.strip(), option_type self.long_text, self.option_type = long_text.strip(), option_type
self.defval_as_string = defval self.defval_as_string = defval
@@ -158,8 +160,8 @@ def remove_markup(text):
return re.sub(r':([a-zA-Z0-9]+):`(.+?)`', sub, text, flags=re.DOTALL) return re.sub(r':([a-zA-Z0-9]+):`(.+?)`', sub, text, flags=re.DOTALL)
def iter_blocks(lines): def iter_blocks(lines: Iterator[str]):
current_block = [] current_block: List[str] = []
prev_indent = 0 prev_indent = 0
for line in lines: for line in lines:
indent_size = len(line) - len(line.lstrip()) indent_size = len(line) - len(line.lstrip())
@@ -180,9 +182,10 @@ def wrapped_block(lines):
wrapper = getattr(wrapped_block, 'wrapper', None) wrapper = getattr(wrapped_block, 'wrapper', None)
if wrapper is None: if wrapper is None:
import textwrap import textwrap
wrapper = wrapped_block.wrapper = textwrap.TextWrapper( wrapper = textwrap.TextWrapper(
initial_indent='#: ', subsequent_indent='#: ', width=70, break_long_words=False initial_indent='#: ', subsequent_indent='#: ', width=70, break_long_words=False
) )
setattr(wrapped_block, 'wrapper', wrapper)
for block, indent_size in iter_blocks(lines): for block, indent_size in iter_blocks(lines):
if indent_size > 0: if indent_size > 0:
for line in block: for line in block:
@@ -278,9 +281,9 @@ def as_conf_file(all_options):
else: else:
count += 1 count += 1
else: else:
if start is not None: if start is not None and count is not None:
map_groups.append((start, count)) map_groups.append((start, count))
start = None start = count = None
for start, count in map_groups: for start, count in map_groups:
r = range(start, start + count) r = range(start, start + count)
sz = max(len(ans[i].split(' ', 3)[1]) for i in r) sz = max(len(ans[i].split(' ', 3)[1]) for i in r)
@@ -304,8 +307,13 @@ def config_lines(all_options):
yield sc.line yield sc.line
def as_type_stub(all_options: Dict[str, Union[Option, List[Shortcut]]], special_types: Optional[Dict[str, str]] = None) -> str: def as_type_stub(
ans = ['import typing\n', '', 'class Options:'] all_options: Dict[str, Union[Option, List[Shortcut]]],
special_types: Optional[Dict[str, str]] = None,
preamble_lines: Union[Tuple[str, ...], List[str], Iterator[str]] = (),
extra_fields: Union[Tuple[Tuple[str, str], ...], List[Tuple[str, str]], Iterator[Tuple[str, str]]] = ()
) -> str:
ans = ['import typing\n'] + list(preamble_lines) + ['', 'class Options:']
imports: Set[Tuple[str, str]] = set() imports: Set[Tuple[str, str]] = set()
overrides = special_types or {} overrides = special_types or {}
for name, val in all_options.items(): for name, val in all_options.items():
@@ -316,4 +324,16 @@ def as_type_stub(all_options: Dict[str, Union[Option, List[Shortcut]]], special_
for mod, name in imports: for mod, name in imports:
ans.insert(0, 'from {} import {}'.format(mod, name)) ans.insert(0, 'from {} import {}'.format(mod, name))
ans.insert(0, 'import {}'.format(mod)) ans.insert(0, 'import {}'.format(mod))
for field_name, type_def in extra_fields:
ans.append(' {}: {}'.format(field_name, type_def))
return '\n'.join(ans) return '\n'.join(ans)
def save_type_stub(text: str, fpath: str) -> None:
import os
with open(fpath + 'i', 'w') as f:
print(
'# Update this file by running: python {}'.format(os.path.relpath(os.path.abspath(fpath))),
file=f
)
f.write(text)

View File

@@ -73,7 +73,9 @@ def choices(*choices) -> Callable[[str], str]:
def parse_line( def parse_line(
line: str, type_map: Dict[str, Any], special_handling: Callable, line: str,
type_convert: Callable[[str, Any], Any],
special_handling: Callable,
ans: Dict[str, Any], all_keys: Optional[FrozenSet[str]], ans: Dict[str, Any], all_keys: Optional[FrozenSet[str]],
base_path_for_includes: str base_path_for_includes: str
) -> None: ) -> None:
@@ -93,7 +95,7 @@ def parse_line(
val = os.path.join(base_path_for_includes, val) val = os.path.join(base_path_for_includes, val)
try: try:
with open(val, encoding='utf-8', errors='replace') as include: with open(val, encoding='utf-8', errors='replace') as include:
_parse(include, type_map, special_handling, ans, all_keys) _parse(include, type_convert, special_handling, ans, all_keys)
except FileNotFoundError: except FileNotFoundError:
log_error( log_error(
'Could not find included config file: {}, ignoring'. 'Could not find included config file: {}, ignoring'.
@@ -108,15 +110,12 @@ def parse_line(
if all_keys is not None and key not in all_keys: if all_keys is not None and key not in all_keys:
log_error('Ignoring unknown config key: {}'.format(key)) log_error('Ignoring unknown config key: {}'.format(key))
return return
tm = type_map.get(key) ans[key] = type_convert(key, val)
if tm is not None:
val = tm(val)
ans[key] = val
def _parse( def _parse(
lines: Iterator[str], lines: Iterator[str],
type_map: Dict[str, Any], type_convert: Callable[[str, Any], Any],
special_handling: Callable, special_handling: Callable,
ans: Dict[str, Any], ans: Dict[str, Any],
all_keys: Optional[FrozenSet[str]], all_keys: Optional[FrozenSet[str]],
@@ -131,7 +130,7 @@ def _parse(
for i, line in enumerate(lines): for i, line in enumerate(lines):
try: try:
parse_line( parse_line(
line, type_map, special_handling, ans, all_keys, line, type_convert, special_handling, ans, all_keys,
base_path_for_includes base_path_for_includes
) )
except Exception as e: except Exception as e:
@@ -143,7 +142,7 @@ def _parse(
def parse_config_base( def parse_config_base(
lines: Iterator[str], lines: Iterator[str],
defaults: Any, defaults: Any,
type_map: Dict[str, Any], type_convert: Callable[[str, Any], Any],
special_handling: Callable, special_handling: Callable,
ans: Dict[str, Any], ans: Dict[str, Any],
check_keys=True, check_keys=True,
@@ -151,7 +150,7 @@ def parse_config_base(
): ):
all_keys: Optional[FrozenSet[str]] = defaults._asdict() if check_keys else None all_keys: Optional[FrozenSet[str]] = defaults._asdict() if check_keys else None
_parse( _parse(
lines, type_map, special_handling, ans, all_keys, accumulate_bad_lines lines, type_convert, special_handling, ans, all_keys, accumulate_bad_lines
) )

View File

@@ -7,9 +7,9 @@ import os
import re import re
import sys import sys
from collections import namedtuple from collections import namedtuple
from contextlib import contextmanager from contextlib import contextmanager, suppress
from contextlib import suppress
from functools import partial from functools import partial
from typing import Type
from . import fast_data_types as defines from . import fast_data_types as defines
from .conf.definition import as_conf_file, config_lines from .conf.definition import as_conf_file, config_lines
@@ -17,10 +17,11 @@ from .conf.utils import (
init_config, key_func, load_config as _load_config, merge_dicts, init_config, key_func, load_config as _load_config, merge_dicts,
parse_config_base, python_string, to_bool, to_cmdline parse_config_base, python_string, to_bool, to_cmdline
) )
from .config_data import all_options, parse_mods, type_map from .config_data import all_options, parse_mods, type_convert
from .constants import cache_dir, defconf, is_macos from .constants import cache_dir, defconf, is_macos
from .utils import log_error
from .key_names import get_key_name_lookup, key_name_aliases from .key_names import get_key_name_lookup, key_name_aliases
from .options_stub import Options as OptionsStub
from .utils import log_error
def parse_shortcut(sc): def parse_shortcut(sc):
@@ -575,9 +576,6 @@ def special_handling(key, val, ans):
return True return True
defaults = None
def option_names_for_completion(): def option_names_for_completion():
yield from defaults yield from defaults
yield from special_handlers yield from special_handlers
@@ -585,10 +583,15 @@ def option_names_for_completion():
def parse_config(lines, check_keys=True, accumulate_bad_lines=None): def parse_config(lines, check_keys=True, accumulate_bad_lines=None):
ans = {'symbol_map': {}, 'keymap': {}, 'sequence_map': {}, 'key_definitions': [], 'env': {}, 'kitten_aliases': {}, 'font_features': {}} ans = {'symbol_map': {}, 'keymap': {}, 'sequence_map': {}, 'key_definitions': [], 'env': {}, 'kitten_aliases': {}, 'font_features': {}}
if check_keys:
defs = defaults
else:
defs = None
parse_config_base( parse_config_base(
lines, lines,
defaults, defs,
type_map, type_convert,
special_handling, special_handling,
ans, ans,
check_keys=check_keys, check_keys=check_keys,
@@ -602,7 +605,9 @@ def parse_defaults(lines, check_keys=False):
return ans return ans
Options, defaults = init_config(config_lines(all_options), parse_defaults) xc = init_config(config_lines(all_options), parse_defaults)
Options: Type[OptionsStub] = xc[0]
defaults: OptionsStub = xc[1]
actions = frozenset(all_key_actions) | frozenset( actions = frozenset(all_key_actions) | frozenset(
'run_simple_kitten combine send_text goto_tab goto_layout set_font_size new_tab_with_cwd new_window_with_cwd new_os_window_with_cwd'. 'run_simple_kitten combine send_text goto_tab goto_layout set_font_size new_tab_with_cwd new_window_with_cwd new_os_window_with_cwd'.
split() split()
@@ -763,7 +768,7 @@ def finalize_keys(opts):
opts.sequence_map = sequence_map opts.sequence_map = sequence_map
def load_config(*paths, overrides=None, accumulate_bad_lines=None): def load_config(*paths, overrides=None, accumulate_bad_lines=None) -> OptionsStub:
parser = parse_config parser = parse_config
if accumulate_bad_lines is not None: if accumulate_bad_lines is not None:
parser = partial(parse_config, accumulate_bad_lines=accumulate_bad_lines) parser = partial(parse_config, accumulate_bad_lines=accumulate_bad_lines)

View File

@@ -6,7 +6,7 @@
import os import os
from gettext import gettext as _ from gettext import gettext as _
from typing import ( from typing import (
Dict, FrozenSet, Iterable, List, Optional, Set, Tuple, TypeVar, Union Any, Dict, FrozenSet, Iterable, List, Optional, Set, Tuple, TypeVar, Union
) )
from . import fast_data_types as defines from . import fast_data_types as defines
@@ -1432,4 +1432,9 @@ the line (same as pressing the Home key)::
# }}} # }}}
# }}} # }}}
type_map = {o.name: o.option_type for o in all_options.values() if isinstance(o, Option)}
def type_convert(name: str, val: Any) -> Any:
o = all_options.get(name)
if isinstance(o, Option):
val = o.option_type(val)
return val

View File

@@ -1,9 +1,10 @@
from typing import Any, Callable, Dict, List, NewType, Optional, Tuple, Union from typing import Any, Callable, Dict, List, NewType, Optional, Tuple, Union
from kitty.boss import Boss from kitty.boss import Boss
from kitty.cli import Namespace from kitty.options_stub import Options
# Constants {{{ # Constants {{{
KITTY_VCS_REV: str
ERROR_PREFIX: str ERROR_PREFIX: str
GLSL_VERSION: int GLSL_VERSION: int
GLFW_IBEAM_CURSOR: int GLFW_IBEAM_CURSOR: int
@@ -339,6 +340,10 @@ def redirect_std_streams(devnull: str) -> None:
pass pass
def glfw_get_key_name(key: int, native_key: int) -> Optional[str]:
pass
StartupCtx = NewType('StartupCtx', int) StartupCtx = NewType('StartupCtx', int)
Display = NewType('Display', int) Display = NewType('Display', int)
@@ -468,7 +473,7 @@ def update_window_visibility(os_window_id: int, tab_id: int, window_id: int, win
def set_options( def set_options(
opts: Namespace, opts: Options,
is_wayland: bool = False, is_wayland: bool = False,
debug_gl: bool = False, debug_gl: bool = False,
debug_font_fallback: bool = False debug_font_fallback: bool = False

View File

@@ -4,10 +4,11 @@
from functools import lru_cache from functools import lru_cache
from typing import Dict from typing import Dict, List, Optional, Sequence, Tuple
from .child import Child from .child import Child
from .cli import parse_args from .cli import parse_args
from .cli_stub import LaunchCLIOptions
from .fast_data_types import set_clipboard_string from .fast_data_types import set_clipboard_string
from .utils import set_primary_selection from .utils import set_primary_selection
@@ -130,16 +131,16 @@ the same as for the :code:`toggle_marker` map action (see :doc:`/marks`).
''' '''
def parse_launch_args(args=None): def parse_launch_args(args: Optional[Sequence[str]] = None) -> Tuple[LaunchCLIOptions, List[str]]:
args = list(args or ()) args = list(args or ())
try: try:
opts, args = parse_args(args=args, ospec=options_spec) opts, args = parse_args(result_class=LaunchCLIOptions, args=args, ospec=options_spec)
except SystemExit as e: except SystemExit as e:
raise ValueError from e raise ValueError from e
return opts, args return opts, args
def get_env(opts, active_child: Child) -> Dict[str, str]: def get_env(opts: LaunchCLIOptions, active_child: Child) -> Dict[str, str]:
env: Dict[str, str] = {} env: Dict[str, str] = {}
if opts.copy_env and active_child: if opts.copy_env and active_child:
env.update(active_child.foreground_environ) env.update(active_child.foreground_environ)
@@ -150,7 +151,7 @@ def get_env(opts, active_child: Child) -> Dict[str, str]:
return env return env
def tab_for_window(boss, opts, target_tab=None): def tab_for_window(boss, opts: LaunchCLIOptions, target_tab=None):
if opts.type == 'tab': if opts.type == 'tab':
tm = boss.active_tab_manager tm = boss.active_tab_manager
tab = tm.new_tab(empty_tab=True, location=opts.location) tab = tm.new_tab(empty_tab=True, location=opts.location)
@@ -168,7 +169,7 @@ def tab_for_window(boss, opts, target_tab=None):
return tab return tab
def launch(boss, opts, args, target_tab=None): def launch(boss, opts: LaunchCLIOptions, args: List[str], target_tab=None):
active = boss.active_window_for_cwd active = boss.active_window_for_cwd
active_child = getattr(active, 'child', None) active_child = getattr(active, 'child', None)
env = get_env(opts, active_child) env = get_env(opts, active_child)

View File

@@ -12,6 +12,7 @@ from .borders import load_borders_program
from .boss import Boss from .boss import Boss
from .child import set_default_env from .child import set_default_env
from .cli import create_opts, parse_args from .cli import create_opts, parse_args
from .cli_stub import CLIOptions
from .config import cached_values_for, initial_window_size_func from .config import cached_values_for, initial_window_size_func
from .constants import ( from .constants import (
appname, beam_cursor_data_file, config_dir, glfw_path, is_macos, appname, beam_cursor_data_file, config_dir, glfw_path, is_macos,
@@ -280,7 +281,7 @@ def _main():
cwd_ok = False cwd_ok = False
if not cwd_ok: if not cwd_ok:
os.chdir(os.path.expanduser('~')) os.chdir(os.path.expanduser('~'))
args, rest = parse_args(args=args) args, rest = parse_args(result_class=CLIOptions, args=args)
args.args = rest args.args = rest
if args.debug_config: if args.debug_config:
create_opts(args, debug_config=True) create_opts(args, debug_config=True)

View File

@@ -2,8 +2,6 @@
# vim:fileencoding=utf-8 # vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net> # License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
import os
class Options: class Options:
pass pass
@@ -11,19 +9,23 @@ class Options:
def generate_stub(): def generate_stub():
from .config_data import all_options from .config_data import all_options
from .conf.definition import as_type_stub from .conf.definition import as_type_stub, save_type_stub
text = as_type_stub( text = as_type_stub(
all_options, all_options,
special_types={ special_types={
'symbol_map': 'typing.Dict[typing.Tuple[int, int], str]' 'symbol_map': 'typing.Dict[typing.Tuple[int, int], str]'
} },
) preamble_lines=(
with open(__file__ + 'i', 'w') as f: 'from kitty.config import KeyAction',
print( 'KeySpec = typing.Tuple[int, bool, int]',
'# Update this file by running: python {}'.format(os.path.relpath(os.path.abspath(__file__))), 'KeyMap = typing.Dict[KeySpec, KeyAction]',
file=f ),
extra_fields=(
('keymap', 'KeyMap'),
('sequence_map', 'typing.Dict[KeySpec, KeyMap]'),
) )
f.write(text) )
save_type_stub(text, __file__)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -3,6 +3,9 @@ import kitty.rgb
from kitty.rgb import Color from kitty.rgb import Color
import typing import typing
from kitty.config import KeyAction
KeySpec = typing.Tuple[int, bool, int]
KeyMap = typing.Dict[KeySpec, KeyAction]
class Options: class Options:
font_family: str font_family: str
@@ -381,3 +384,5 @@ class Options:
kitty_mod: int kitty_mod: int
clear_all_shortcuts: bool clear_all_shortcuts: bool
kitten_alias: str kitten_alias: str
keymap: KeyMap
sequence_map: typing.Dict[KeySpec, KeyMap]

View File

@@ -138,13 +138,17 @@ cli_msg = (
).format(appname=appname) ).format(appname=appname)
class RCOptions:
pass
def parse_rc_args(args): def parse_rc_args(args):
cmds = (' :green:`{}`\n {}'.format(cmap[c].name, cmap[c].short_desc) for c in all_commands) cmds = (' :green:`{}`\n {}'.format(cmap[c].name, cmap[c].short_desc) for c in all_commands)
msg = cli_msg + ( msg = cli_msg + (
'\n\n:title:`Commands`:\n{cmds}\n\n' '\n\n:title:`Commands`:\n{cmds}\n\n'
'You can get help for each individual command by using:\n' 'You can get help for each individual command by using:\n'
'{appname} @ :italic:`command` -h').format(appname=appname, cmds='\n'.join(cmds)) '{appname} @ :italic:`command` -h').format(appname=appname, cmds='\n'.join(cmds))
return parse_args(args[1:], global_options_spec, 'command ...', msg, '{} @'.format(appname)) return parse_args(args[1:], global_options_spec, 'command ...', msg, '{} @'.format(appname), result_class=RCOptions)
def main(args): def main(args):

View File

@@ -15,3 +15,11 @@ multi_line_output = 5
[mypy] [mypy]
files = kitty,kittens,glfw/glfw.py,*.py files = kitty,kittens,glfw/glfw.py,*.py
[mypy-kitty.conf.*]
check_untyped_defs = True
follow_imports = silent
[mypy-kitty.cli]
check_untyped_defs = True
follow_imports = silent