Cache parsing of command line specifications
Change option specification from dict to tuple for better performance and immutability and better type checking.
This commit is contained in:
@@ -247,7 +247,7 @@ def clone_safe_launch_opts() -> Sequence[GoOption]:
|
|||||||
ans = []
|
ans = []
|
||||||
allowed = clone_safe_opts()
|
allowed = clone_safe_opts()
|
||||||
for o in go_options_for_seq(parse_option_spec(options_spec())[0]):
|
for o in go_options_for_seq(parse_option_spec(options_spec())[0]):
|
||||||
if o.obj_dict['name'] in allowed:
|
if o.obj_defn.name in allowed:
|
||||||
ans.append(o)
|
ans.append(o)
|
||||||
return tuple(ans)
|
return tuple(ans)
|
||||||
|
|
||||||
|
|||||||
@@ -29,16 +29,16 @@ def migrate_help(x: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def help_of(x: str) -> str:
|
def help_of(x: str) -> str:
|
||||||
return migrate_help(panel_opts[x]['help'])
|
return migrate_help(panel_opts[x].help)
|
||||||
|
|
||||||
|
|
||||||
agr('qat', 'Window appearance')
|
agr('qat', 'Window appearance')
|
||||||
|
|
||||||
opt('lines', '25', long_text=panel_opts['lines']['help'])
|
opt('lines', '25', long_text=panel_opts['lines'].help)
|
||||||
|
|
||||||
opt('columns', '80', long_text=panel_opts['columns']['help'])
|
opt('columns', '80', long_text=panel_opts['columns'].help)
|
||||||
|
|
||||||
opt('edge', 'top', choices=panel_opts['edge']['choices'], long_text=help_of('edge'))
|
opt('edge', 'top', choices=panel_opts['edge'].choices, long_text=help_of('edge'))
|
||||||
|
|
||||||
opt('background_opacity', '0.85', option_type='unit_float', long_text='''
|
opt('background_opacity', '0.85', option_type='unit_float', long_text='''
|
||||||
The background opacity of the window. This works the same as the kitty
|
The background opacity of the window. This works the same as the kitty
|
||||||
@@ -80,7 +80,7 @@ opt('output_name', '', long_text=help_of('output_name'))
|
|||||||
opt('start_as_hidden', 'no', option_type='to_bool',
|
opt('start_as_hidden', 'no', option_type='to_bool',
|
||||||
long_text='Whether to start the quick access terminal hidden. Useful if you are starting it as part of system startup.')
|
long_text='Whether to start the quick access terminal hidden. Useful if you are starting it as part of system startup.')
|
||||||
|
|
||||||
opt('focus_policy', 'exclusive', choices=panel_opts['focus_policy']['choices'], long_text=help_of('focus_policy'))
|
opt('focus_policy', 'exclusive', choices=panel_opts['focus_policy'].choices, long_text=help_of('focus_policy'))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
80
kitty/cli.py
80
kitty/cli.py
@@ -15,7 +15,7 @@ from .fast_data_types import parse_cli_from_spec, wcswidth
|
|||||||
from .options.types import Options as KittyOpts
|
from .options.types import Options as KittyOpts
|
||||||
from .simple_cli_definitions import (
|
from .simple_cli_definitions import (
|
||||||
CompletionType,
|
CompletionType,
|
||||||
OptionDict,
|
OptionDefinition,
|
||||||
OptionSpecSeq,
|
OptionSpecSeq,
|
||||||
defval_for_opt,
|
defval_for_opt,
|
||||||
get_option_maps,
|
get_option_maps,
|
||||||
@@ -34,34 +34,34 @@ go_type_map = {
|
|||||||
|
|
||||||
class GoOption:
|
class GoOption:
|
||||||
|
|
||||||
def __init__(self, x: OptionDict) -> None:
|
def __init__(self, x: OptionDefinition) -> None:
|
||||||
flags = sorted(x['aliases'], key=len)
|
flags = sorted(x.aliases, key=len)
|
||||||
short = ''
|
short = ''
|
||||||
self.aliases = []
|
self.aliases = []
|
||||||
if len(flags) > 1 and not flags[0].startswith("--"):
|
if len(flags) > 1 and not flags[0].startswith("--"):
|
||||||
short = flags[0][1:]
|
short = flags[0][1:]
|
||||||
self.short, self.long = short, x['name'].replace('_', '-')
|
self.short, self.long = short, x.name.replace('_', '-')
|
||||||
for f in flags:
|
for f in flags:
|
||||||
q = f[2:] if f.startswith('--') else f[1:]
|
q = f[2:] if f.startswith('--') else f[1:]
|
||||||
self.aliases.append(q)
|
self.aliases.append(q)
|
||||||
self.type = x['type']
|
self.type = x.type
|
||||||
if x['choices']:
|
if x.choices:
|
||||||
self.type = 'choices'
|
self.type = 'choices'
|
||||||
self.default = x['default']
|
self.default = x.default
|
||||||
self.obj_dict = x
|
self.obj_defn = x
|
||||||
self.go_type = go_type_map[self.type]
|
self.go_type = go_type_map[self.type]
|
||||||
if x['dest']:
|
if x.dest:
|
||||||
self.go_var_name = ''.join(x.capitalize() for x in x['dest'].replace('-', '_').split('_'))
|
self.go_var_name = ''.join(x.capitalize() for x in x.dest.replace('-', '_').split('_'))
|
||||||
else:
|
else:
|
||||||
self.go_var_name = ''.join(x.capitalize() for x in self.long.replace('-', '_').split('_'))
|
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())
|
self.help_text = serialize_as_go_string(self.obj_defn.help.strip())
|
||||||
|
|
||||||
def struct_declaration(self) -> str:
|
def struct_declaration(self) -> str:
|
||||||
return f'{self.go_var_name} {self.go_type}'
|
return f'{self.go_var_name} {self.go_type}'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def flags(self) -> list[str]:
|
def flags(self) -> list[str]:
|
||||||
return sorted(self.obj_dict['aliases'])
|
return sorted(self.obj_defn.aliases)
|
||||||
|
|
||||||
def as_option(self, cmd_name: str = 'cmd', depth: int = 0, group: str = '') -> str:
|
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('
|
add = f'AddToGroup("{serialize_as_go_string(group)}", ' if group else 'Add('
|
||||||
@@ -77,8 +77,8 @@ class GoOption:
|
|||||||
cx = ', '.join(f'"{serialize_as_go_string(x)}"' for x in 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'\nChoices: "{serialize_as_go_string(c)}",\n'
|
||||||
ans += f'\nCompleter: cli.NamesCompleter("Choices for {self.long}", {cx}),'
|
ans += f'\nCompleter: cli.NamesCompleter("Choices for {self.long}", {cx}),'
|
||||||
elif self.obj_dict['completion'].type is not CompletionType.none:
|
elif self.obj_defn.completion.type is not CompletionType.none:
|
||||||
ans += ''.join(self.obj_dict['completion'].as_go_code('Completer', ': ')) + ','
|
ans += ''.join(self.obj_defn.completion.as_go_code('Completer', ': ')) + ','
|
||||||
if depth > 0:
|
if depth > 0:
|
||||||
ans += f'\n\tDepth: {depth},\n'
|
ans += f'\n\tDepth: {depth},\n'
|
||||||
if self.default:
|
if self.default:
|
||||||
@@ -111,7 +111,7 @@ class GoOption:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def sorted_choices(self) -> list[str]:
|
def sorted_choices(self) -> list[str]:
|
||||||
choices = sorted(self.obj_dict['choices'])
|
choices = sorted(self.obj_defn.choices)
|
||||||
choices.remove(self.default or '')
|
choices.remove(self.default or '')
|
||||||
choices.insert(0, self.default or '')
|
choices.insert(0, self.default or '')
|
||||||
return choices
|
return choices
|
||||||
@@ -374,7 +374,7 @@ def get_defaults_from_seq(seq: OptionSpecSeq) -> dict[str, Any]:
|
|||||||
ans: dict[str, Any] = {}
|
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)
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
|
||||||
@@ -432,24 +432,24 @@ class PrintHelpForSeq:
|
|||||||
if isinstance(opt, str):
|
if isinstance(opt, str):
|
||||||
a(f'{title(opt)}:')
|
a(f'{title(opt)}:')
|
||||||
continue
|
continue
|
||||||
help_text = opt['help']
|
help_text = opt.help
|
||||||
if help_text == '!':
|
if help_text == '!':
|
||||||
continue # hidden option
|
continue # hidden option
|
||||||
a(' ' + ', '.join(map(green, sorted(opt['aliases'], reverse=True))))
|
a(' ' + ', '.join(map(green, sorted(opt.aliases, reverse=True))))
|
||||||
defval = opt.get('default')
|
defval = opt.default
|
||||||
if (otype := opt.get('type', '')).startswith('bool-'):
|
if (otype := opt.type).startswith('bool-'):
|
||||||
blocks[-1] += italic(f'[={help_defval_for_bool(otype)}]')
|
blocks[-1] += italic(f'[={help_defval_for_bool(otype)}]')
|
||||||
else:
|
else:
|
||||||
dt = f'''=[{italic(defval or '""')}]'''
|
dt = f'''=[{italic(defval or '""')}]'''
|
||||||
blocks[-1] += dt
|
blocks[-1] += dt
|
||||||
if opt.get('help'):
|
if opt.help:
|
||||||
t = help_text.replace('%default', str(defval)).strip()
|
t = help_text.replace('%default', str(defval)).strip()
|
||||||
# replace rst literal code block syntax
|
# replace rst literal code block syntax
|
||||||
t = t.replace('::\n\n', ':\n\n')
|
t = t.replace('::\n\n', ':\n\n')
|
||||||
t = t.replace('#placeholder_for_formatting#', '')
|
t = t.replace('#placeholder_for_formatting#', '')
|
||||||
wa(prettify(t), indent=4)
|
wa(prettify(t), indent=4)
|
||||||
if opt.get('choices'):
|
if opt.choices:
|
||||||
wa('Choices: {}'.format(', '.join(opt['choices'])), indent=4)
|
wa('Choices: {}'.format(', '.join(opt.choices)), indent=4)
|
||||||
a('')
|
a('')
|
||||||
|
|
||||||
text = '\n'.join(blocks) + '\n\n' + version()
|
text = '\n'.join(blocks) + '\n\n' + version()
|
||||||
@@ -510,25 +510,25 @@ def seq_as_rst(
|
|||||||
a(opt)
|
a(opt)
|
||||||
a('~' * (len(opt) + 10))
|
a('~' * (len(opt) + 10))
|
||||||
continue
|
continue
|
||||||
help_text = opt['help']
|
help_text = opt.help
|
||||||
if help_text == '!':
|
if help_text == '!':
|
||||||
continue # hidden option
|
continue # hidden option
|
||||||
defn = '.. option:: '
|
defn = '.. option:: '
|
||||||
if (otype := opt.get('type', '')).startswith('bool-'):
|
if (otype := opt.type).startswith('bool-'):
|
||||||
val_name = f' [={help_defval_for_bool(otype)}]'
|
val_name = f' [={help_defval_for_bool(otype)}]'
|
||||||
else:
|
else:
|
||||||
val_name = ' <{}>'.format(opt['dest'].upper())
|
val_name = ' <{}>'.format(opt.dest.upper())
|
||||||
a(defn + ', '.join(o + val_name for o in sorted(opt['aliases'])))
|
a(defn + ', '.join(o + val_name for o in sorted(opt.aliases)))
|
||||||
if opt.get('help'):
|
if opt.help:
|
||||||
defval = opt.get('default')
|
defval = opt.default
|
||||||
t = help_text.replace('%default', ':code:`' + escape_rst(str(defval)) + '`').strip()
|
t = help_text.replace('%default', ':code:`' + escape_rst(str(defval)) + '`').strip()
|
||||||
t = t.replace('#placeholder_for_formatting#', '')
|
t = t.replace('#placeholder_for_formatting#', '')
|
||||||
a('')
|
a('')
|
||||||
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(f'Default: :code:`{escape_rst(str(defval))}`', ' ' * 4))
|
a(textwrap.indent(f'Default: :code:`{escape_rst(str(defval))}`', ' ' * 4))
|
||||||
if opt.get('choices'):
|
if opt.choices:
|
||||||
a(textwrap.indent('Choices: {}'.format(', '.join(f':code:`{escape_rst(c)}`' for c in sorted(opt['choices']))), ' ' * 4))
|
a(textwrap.indent('Choices: {}'.format(', '.join(f':code:`{escape_rst(c)}`' for c in sorted(opt.choices))), ' ' * 4))
|
||||||
a('')
|
a('')
|
||||||
|
|
||||||
text = '\n'.join(blocks)
|
text = '\n'.join(blocks)
|
||||||
@@ -541,8 +541,8 @@ def as_type_stub(seq: OptionSpecSeq, disabled: OptionSpecSeq, class_name: str, e
|
|||||||
for opt in chain(seq, disabled):
|
for opt in chain(seq, disabled):
|
||||||
if isinstance(opt, str):
|
if isinstance(opt, str):
|
||||||
continue
|
continue
|
||||||
name = opt['dest']
|
name = opt.dest
|
||||||
otype = opt['type'] or 'str'
|
otype = opt.type or 'str'
|
||||||
if otype in ('str', 'int', 'float'):
|
if otype in ('str', 'int', 'float'):
|
||||||
t = otype
|
t = otype
|
||||||
if t == 'str' and defval_for_opt(opt) is None:
|
if t == 'str' and defval_for_opt(opt) is None:
|
||||||
@@ -550,8 +550,8 @@ def as_type_stub(seq: OptionSpecSeq, disabled: OptionSpecSeq, class_name: str, e
|
|||||||
elif otype == 'list':
|
elif otype == 'list':
|
||||||
t = 'typing.Sequence[str]'
|
t = 'typing.Sequence[str]'
|
||||||
elif otype in ('choice', 'choices'):
|
elif otype in ('choice', 'choices'):
|
||||||
if opt['choices']:
|
if opt.choices:
|
||||||
t = 'typing.Literal[{}]'.format(','.join(f'{x!r}' for x in opt['choices']))
|
t = 'typing.Literal[{}]'.format(','.join(f'{x!r}' for x in opt.choices))
|
||||||
else:
|
else:
|
||||||
t = 'str'
|
t = 'str'
|
||||||
elif otype.startswith('bool-'):
|
elif otype.startswith('bool-'):
|
||||||
@@ -616,14 +616,14 @@ def apply_preparsed_cli_flags(
|
|||||||
|
|
||||||
|
|
||||||
def parse_cmdline_inner(
|
def parse_cmdline_inner(
|
||||||
args: list[str], oc: Options, disabled: OptionSpecSeq, names_map: dict[str, OptionDict],
|
args: list[str], oc: Options, disabled: OptionSpecSeq, names_map: dict[str, OptionDefinition],
|
||||||
values_map: dict[str, OptionDict], ans: Any, track_seen_options: dict[str, Any] | None = None
|
values_map: dict[str, OptionDefinition], ans: Any, track_seen_options: dict[str, Any] | None = None
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
preparsed = parse_cli_from_spec(args, names_map, values_map)
|
preparsed = parse_cli_from_spec(args, names_map, values_map)
|
||||||
leftover_args = apply_preparsed_cli_flags(preparsed, ans, lambda: oc, track_seen_options)
|
leftover_args = apply_preparsed_cli_flags(preparsed, ans, lambda: oc, track_seen_options)
|
||||||
for opt in disabled:
|
for opt in disabled:
|
||||||
if not isinstance(opt, str):
|
if not isinstance(opt, str):
|
||||||
setattr(ans, opt['dest'], defval_for_opt(opt))
|
setattr(ans, opt.dest, defval_for_opt(opt))
|
||||||
return leftover_args
|
return leftover_args
|
||||||
|
|
||||||
|
|
||||||
@@ -634,10 +634,10 @@ def parse_cmdline(
|
|||||||
names_map = oc.names_map.copy()
|
names_map = oc.names_map.copy()
|
||||||
values_map = oc.values_map.copy()
|
values_map = oc.values_map.copy()
|
||||||
if 'help' not in names_map:
|
if 'help' not in names_map:
|
||||||
names_map['help'] = {'type': 'bool-set', 'aliases': ('--help', '-h')} # type: ignore
|
names_map['help'] = OptionDefinition(type='bool-set', aliases=('--help', '-h'))
|
||||||
values_map['help'] = False
|
values_map['help'] = False
|
||||||
if 'version' not in names_map:
|
if 'version' not in names_map:
|
||||||
names_map['version'] = {'type': 'bool-set', 'aliases': ('--version', '-v')} # type: ignore
|
names_map['version'] = OptionDefinition(type='bool-set', aliases=('--version', '-v'))
|
||||||
values_map['version'] = False
|
values_map['version'] = False
|
||||||
try:
|
try:
|
||||||
return parse_cmdline_inner(sys.argv[1:] if args is None else args, oc, disabled, names_map, values_map, ans, track_seen_options)
|
return parse_cmdline_inner(sys.argv[1:] if args is None else args, oc, disabled, names_map, values_map, ans, track_seen_options)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from kitty.fonts.render import FontObject
|
|||||||
from kitty.marks import MarkerFunc
|
from kitty.marks import MarkerFunc
|
||||||
from kitty.notifications import MacOSNotificationCategory
|
from kitty.notifications import MacOSNotificationCategory
|
||||||
from kitty.options.types import Options
|
from kitty.options.types import Options
|
||||||
from kitty.simple_cli_definitions import OptionDict
|
from kitty.simple_cli_definitions import OptionDefinition
|
||||||
from kitty.types import LayerShellConfig, SignalInfo
|
from kitty.types import LayerShellConfig, SignalInfo
|
||||||
from kitty.typing_compat import EdgeLiteral, NotRequired, ReadableBuffer, WriteableBuffer
|
from kitty.typing_compat import EdgeLiteral, NotRequired, ReadableBuffer, WriteableBuffer
|
||||||
|
|
||||||
@@ -1720,7 +1720,9 @@ def set_clipboard_data_types(ct: int, mime_types: Tuple[str, ...]) -> None: ...
|
|||||||
def get_clipboard_mime(ct: int, mime: Optional[str], callback: Callable[[bytes], None]) -> None: ...
|
def get_clipboard_mime(ct: int, mime: Optional[str], callback: Callable[[bytes], None]) -> None: ...
|
||||||
def run_with_activation_token(func: Callable[[str], None]) -> bool: ...
|
def run_with_activation_token(func: Callable[[str], None]) -> bool: ...
|
||||||
def toggle_os_window_visibility(os_window_id: int, visible: bool | Literal[-1] = -1, move_to_active_screen: bool = False) -> bool: ...
|
def toggle_os_window_visibility(os_window_id: int, visible: bool | Literal[-1] = -1, move_to_active_screen: bool = False) -> bool: ...
|
||||||
def parse_cli_from_spec(args: list[str], names_map: dict[str, OptionDict], defval_map: dict[str, Any]) -> tuple[dict[str, tuple[Any, bool]], list[str]]: ...
|
def parse_cli_from_spec(
|
||||||
|
args: list[str], names_map: dict[str, OptionDefinition], defval_map: dict[str, Any]
|
||||||
|
) -> tuple[dict[str, tuple[Any, bool]], list[str]]: ...
|
||||||
def layer_shell_config_for_os_window(os_window_id: int) -> dict[str, Any] | None: ...
|
def layer_shell_config_for_os_window(os_window_id: int) -> dict[str, Any] | None: ...
|
||||||
def set_layer_shell_config(os_window_id: int, cfg: LayerShellConfig) -> bool: ...
|
def set_layer_shell_config(os_window_id: int, cfg: LayerShellConfig) -> bool: ...
|
||||||
def wrapped_kitten_names() -> List[str]: ...
|
def wrapped_kitten_names() -> List[str]: ...
|
||||||
|
|||||||
@@ -559,14 +559,16 @@ parse_cli_from_python_spec(PyObject *self, PyObject *args) {
|
|||||||
memcpy(argv[i + 1], src, sz);
|
memcpy(argv[i + 1], src, sz);
|
||||||
}
|
}
|
||||||
argv[++argc] = 0;
|
argv[++argc] = 0;
|
||||||
PyObject *key = NULL, *opt = NULL;
|
PyObject *key = NULL, *optdef = NULL;
|
||||||
Py_ssize_t pos = 0;
|
Py_ssize_t pos = 0;
|
||||||
while (PyDict_Next(names_map, &pos, &key, &opt)) {
|
while (PyDict_Next(names_map, &pos, &key, &optdef)) {
|
||||||
FlagSpec flag = {.dest=PyUnicode_AsUTF8(key)};
|
FlagSpec flag = {.dest=PyUnicode_AsUTF8(key)};
|
||||||
PyObject *pytype = PyDict_GetItemString(opt, "type");
|
RAII_PyObject(pytype, PyObject_GetAttrString(optdef, "type"));
|
||||||
const char *type = pytype ? PyUnicode_AsUTF8(pytype) : "";
|
if (!pytype) return NULL;
|
||||||
|
const char *type = PyUnicode_AsUTF8(pytype);
|
||||||
PyObject *defval = PyDict_GetItemWithError(defval_map, key); if (!defval && PyErr_Occurred()) return NULL;
|
PyObject *defval = PyDict_GetItemWithError(defval_map, key); if (!defval && PyErr_Occurred()) return NULL;
|
||||||
PyObject *pyaliases = PyDict_GetItemString(opt, "aliases");
|
RAII_PyObject(pyaliases, PyObject_GetAttrString(optdef, "aliases"));
|
||||||
|
if (!pyaliases) return NULL;
|
||||||
for (int a = 0; a < PyTuple_GET_SIZE(pyaliases); a++) {
|
for (int a = 0; a < PyTuple_GET_SIZE(pyaliases); a++) {
|
||||||
const char *alias = PyUnicode_AsUTF8(PyTuple_GET_ITEM(pyaliases, a));
|
const char *alias = PyUnicode_AsUTF8(PyTuple_GET_ITEM(pyaliases, a));
|
||||||
if (vt_is_end(vt_insert(&spec.alias_map, alias, flag.dest))) return PyErr_NoMemory();
|
if (vt_is_end(vt_insert(&spec.alias_map, alias, flag.dest))) return PyErr_NoMemory();
|
||||||
@@ -588,7 +590,8 @@ parse_cli_from_python_spec(PyObject *self, PyObject *args) {
|
|||||||
} else if (strcmp(type, "choices") == 0) {
|
} else if (strcmp(type, "choices") == 0) {
|
||||||
flag.defval.type = CLI_VALUE_CHOICE;
|
flag.defval.type = CLI_VALUE_CHOICE;
|
||||||
flag.defval.strval = PyUnicode_AsUTF8(defval);
|
flag.defval.strval = PyUnicode_AsUTF8(defval);
|
||||||
PyObject *pyc = PyDict_GetItemString(opt, "choices");
|
RAII_PyObject(pyc, PyObject_GetAttrString(optdef, "choices"));
|
||||||
|
if (!pyc) return NULL;
|
||||||
flag.defval.listval.items = alloc_for_cli(&spec, PyTuple_GET_SIZE(pyc) * sizeof(char*));
|
flag.defval.listval.items = alloc_for_cli(&spec, PyTuple_GET_SIZE(pyc) * sizeof(char*));
|
||||||
if (!flag.defval.listval.items) return PyErr_NoMemory();
|
if (!flag.defval.listval.items) return PyErr_NoMemory();
|
||||||
flag.defval.listval.count = PyTuple_GET_SIZE(pyc);
|
flag.defval.listval.count = PyTuple_GET_SIZE(pyc);
|
||||||
|
|||||||
@@ -82,8 +82,8 @@ CmdGenerator = Iterator[CmdReturnType]
|
|||||||
PayloadType = Optional[Union[CmdReturnType, CmdGenerator]]
|
PayloadType = Optional[Union[CmdReturnType, CmdGenerator]]
|
||||||
PayloadGetType = PayloadGetter
|
PayloadGetType = PayloadGetter
|
||||||
ArgsType = list[str]
|
ArgsType = list[str]
|
||||||
ImageCompletion = CompletionSpec.from_string('type:file group:"Images"')
|
ImageCompletion = CompletionSpec.from_string('type:file group:"Images"')._replace(
|
||||||
ImageCompletion.extensions = 'png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'tiff'
|
extensions=('png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'tiff'))
|
||||||
SUPPORTED_IMAGE_FORMATS = tuple(x.upper() for x in ImageCompletion.extensions if x != 'jpg')
|
SUPPORTED_IMAGE_FORMATS = tuple(x.upper() for x in ImageCompletion.extensions if x != 'jpg')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from dataclasses import dataclass
|
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from typing import Any, Iterator, Sequence, TypedDict
|
from functools import lru_cache
|
||||||
|
from typing import Any, Iterator, NamedTuple, Sequence
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from kitty.constants import appname, is_macos
|
from kitty.constants import appname, is_macos
|
||||||
@@ -42,8 +42,7 @@ class CompletionRelativeTo(Enum):
|
|||||||
config_dir = auto()
|
config_dir = auto()
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
class CompletionSpec(NamedTuple):
|
||||||
class CompletionSpec:
|
|
||||||
|
|
||||||
type: CompletionType = CompletionType.none
|
type: CompletionType = CompletionType.none
|
||||||
kwds: tuple[str,...] = ()
|
kwds: tuple[str,...] = ()
|
||||||
@@ -54,27 +53,33 @@ class CompletionSpec:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_string(raw: str) -> 'CompletionSpec':
|
def from_string(raw: str) -> 'CompletionSpec':
|
||||||
self = CompletionSpec()
|
typ = CompletionType.none
|
||||||
|
kwds: tuple[str, ...] = ()
|
||||||
|
extensions: tuple[str, ...] = ()
|
||||||
|
mime_patterns: tuple[str, ...] = ()
|
||||||
|
group = ''
|
||||||
|
relative_to = CompletionRelativeTo.cwd
|
||||||
for x in shlex_split(raw):
|
for x in shlex_split(raw):
|
||||||
ck, vv = x.split(':', 1)
|
ck, vv = x.split(':', 1)
|
||||||
if ck == 'type':
|
if ck == 'type':
|
||||||
self.type = getattr(CompletionType, vv)
|
typ = getattr(CompletionType, vv)
|
||||||
elif ck == 'kwds':
|
elif ck == 'kwds':
|
||||||
self.kwds += tuple(vv.split(','))
|
kwds += tuple(vv.split(','))
|
||||||
elif ck == 'ext':
|
elif ck == 'ext':
|
||||||
self.extensions += tuple(vv.split(','))
|
extensions += tuple(vv.split(','))
|
||||||
elif ck == 'group':
|
elif ck == 'group':
|
||||||
self.group = vv
|
group = vv
|
||||||
elif ck == 'mime':
|
elif ck == 'mime':
|
||||||
self.mime_patterns += tuple(vv.split(','))
|
mime_patterns += tuple(vv.split(','))
|
||||||
elif ck == 'relative':
|
elif ck == 'relative':
|
||||||
if vv == 'conf':
|
if vv == 'conf':
|
||||||
self.relative_to = CompletionRelativeTo.config_dir
|
relative_to = CompletionRelativeTo.config_dir
|
||||||
else:
|
else:
|
||||||
raise ValueError(f'Unknown completion relative to value: {vv}')
|
raise ValueError(f'Unknown completion relative to value: {vv}')
|
||||||
else:
|
else:
|
||||||
raise KeyError(f'Unknown completion property: {ck}')
|
raise KeyError(f'Unknown completion property: {ck}')
|
||||||
return self
|
return CompletionSpec(
|
||||||
|
type=typ, kwds=kwds, extensions=extensions, mime_patterns=mime_patterns, group=group, relative_to=relative_to)
|
||||||
|
|
||||||
def as_go_code(self, go_name: str, sep: str = ': ') -> Iterator[str]:
|
def as_go_code(self, go_name: str, sep: str = ': ') -> Iterator[str]:
|
||||||
completers = []
|
completers = []
|
||||||
@@ -106,21 +111,23 @@ class CompletionSpec:
|
|||||||
yield f'{go_name}{sep}{completers[0]}'
|
yield f'{go_name}{sep}{completers[0]}'
|
||||||
|
|
||||||
|
|
||||||
class OptionDict(TypedDict):
|
class OptionDefinition(NamedTuple):
|
||||||
dest: str
|
dest: str = ''
|
||||||
name: str
|
name: str = ''
|
||||||
aliases: tuple[str, ...]
|
aliases: tuple[str, ...] = ()
|
||||||
help: str
|
help: str = ''
|
||||||
choices: tuple[str, ...]
|
choices: tuple[str, ...] = ()
|
||||||
type: str
|
type: str = ''
|
||||||
default: str | None
|
default: str | None = None
|
||||||
condition: bool
|
condition: bool = False
|
||||||
completion: CompletionSpec
|
completion: CompletionSpec = CompletionSpec()
|
||||||
|
|
||||||
|
|
||||||
OptionSpecSeq = Sequence[str | OptionDict]
|
|
||||||
|
OptionSpecSeq = Sequence[str | OptionDefinition]
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(64)
|
||||||
def parse_option_spec(spec: str | None = None) -> tuple[OptionSpecSeq, OptionSpecSeq]:
|
def parse_option_spec(spec: str | None = None) -> tuple[OptionSpecSeq, OptionSpecSeq]:
|
||||||
if spec is None:
|
if spec is None:
|
||||||
spec = kitty_options_spec()
|
spec = kitty_options_spec()
|
||||||
@@ -129,14 +136,10 @@ def parse_option_spec(spec: str | None = None) -> tuple[OptionSpecSeq, OptionSpe
|
|||||||
lines = spec.splitlines()
|
lines = spec.splitlines()
|
||||||
prev_line = ''
|
prev_line = ''
|
||||||
prev_indent = 0
|
prev_indent = 0
|
||||||
seq: list[str | OptionDict] = []
|
seq: list[str | OptionDefinition] = []
|
||||||
disabled: list[str | OptionDict] = []
|
disabled: list[str | OptionDefinition] = []
|
||||||
mpat = re.compile('([a-z]+)=(.+)')
|
mpat = re.compile('([a-z]+)=(.+)')
|
||||||
current_cmd: OptionDict = {
|
current_cmd = empty_cmd = OptionDefinition()
|
||||||
'dest': '', 'aliases': (), 'help': '', 'choices': (),
|
|
||||||
'type': '', 'condition': False, 'default': None, 'completion': CompletionSpec(), 'name': ''
|
|
||||||
}
|
|
||||||
empty_cmd = current_cmd
|
|
||||||
|
|
||||||
def indent_of_line(x: str) -> int:
|
def indent_of_line(x: str) -> int:
|
||||||
return len(x) - len(x.lstrip())
|
return len(x) - len(x.lstrip())
|
||||||
@@ -152,11 +155,7 @@ def parse_option_spec(spec: str | None = None) -> tuple[OptionSpecSeq, OptionSpe
|
|||||||
if line.startswith('--'):
|
if line.startswith('--'):
|
||||||
parts = line.split(' ')
|
parts = line.split(' ')
|
||||||
defdest = parts[0][2:].replace('-', '_')
|
defdest = parts[0][2:].replace('-', '_')
|
||||||
current_cmd = {
|
current_cmd = OptionDefinition(dest=defdest, aliases=tuple(parts), name=defdest, condition=True)
|
||||||
'dest': defdest, 'aliases': tuple(parts), 'help': '',
|
|
||||||
'choices': tuple(), 'type': '', 'name': defdest,
|
|
||||||
'default': None, 'condition': True, 'completion': CompletionSpec(),
|
|
||||||
}
|
|
||||||
state = METADATA
|
state = METADATA
|
||||||
continue
|
continue
|
||||||
raise ValueError(f'Invalid option spec, unexpected line: {line}')
|
raise ValueError(f'Invalid option spec, unexpected line: {line}')
|
||||||
@@ -164,60 +163,61 @@ def parse_option_spec(spec: str | None = None) -> tuple[OptionSpecSeq, OptionSpe
|
|||||||
m = mpat.match(line)
|
m = mpat.match(line)
|
||||||
if m is None:
|
if m is None:
|
||||||
state = HELP
|
state = HELP
|
||||||
current_cmd['help'] += line
|
current_cmd = current_cmd._replace(help=current_cmd.help + line)
|
||||||
else:
|
else:
|
||||||
k, v = m.group(1), m.group(2)
|
k, v = m.group(1), m.group(2)
|
||||||
if k == 'choices':
|
if k == 'choices':
|
||||||
vals = tuple(x.strip() for x in v.split(','))
|
vals = tuple(x.strip() for x in v.split(','))
|
||||||
if not current_cmd['type']:
|
if not current_cmd.type:
|
||||||
current_cmd['type'] = 'choices'
|
current_cmd = current_cmd._replace(type='choices')
|
||||||
if current_cmd['type'] != 'choices':
|
if current_cmd.type != 'choices':
|
||||||
raise ValueError(f'Cannot specify choices for an option of type: {current_cmd["type"]}')
|
raise ValueError(f'Cannot specify choices for an option of type: {current_cmd.type}')
|
||||||
current_cmd['choices'] = tuple(vals)
|
current_cmd = current_cmd._replace(choices=tuple(vals))
|
||||||
if current_cmd['default'] is None:
|
if current_cmd.default is None:
|
||||||
current_cmd['default'] = vals[0]
|
current_cmd = current_cmd._replace(default=vals[0])
|
||||||
else:
|
else:
|
||||||
if k == 'default':
|
if k == 'default':
|
||||||
current_cmd['default'] = v
|
current_cmd = current_cmd._replace(default=v)
|
||||||
elif k == 'type':
|
elif k == 'type':
|
||||||
if v == 'choice':
|
if v == 'choice':
|
||||||
v = 'choices'
|
v = 'choices'
|
||||||
current_cmd['type'] = v
|
current_cmd = current_cmd._replace(type=v)
|
||||||
elif k == 'dest':
|
elif k == 'dest':
|
||||||
current_cmd['dest'] = v
|
current_cmd = current_cmd._replace(dest=v)
|
||||||
elif k == 'condition':
|
elif k == 'condition':
|
||||||
current_cmd['condition'] = bool(eval(v))
|
current_cmd = current_cmd._replace(condition=bool(eval(v)))
|
||||||
elif k == 'completion':
|
elif k == 'completion':
|
||||||
current_cmd['completion'] = CompletionSpec.from_string(v)
|
current_cmd = current_cmd._replace(completion=CompletionSpec.from_string(v))
|
||||||
elif state is HELP:
|
elif state is HELP:
|
||||||
if line:
|
if line:
|
||||||
current_indent = indent_of_line(line)
|
current_indent = indent_of_line(line)
|
||||||
if current_indent > 1:
|
if current_indent > 1:
|
||||||
if prev_indent == 0:
|
if prev_indent == 0:
|
||||||
current_cmd['help'] += '\n'
|
current_cmd = current_cmd._replace(help=current_cmd.help + '\n')
|
||||||
else:
|
else:
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
prev_indent = current_indent
|
prev_indent = current_indent
|
||||||
spc = '' if current_cmd['help'].endswith('\n') else ' '
|
spc = '' if current_cmd.help.endswith('\n') else ' '
|
||||||
current_cmd['help'] += spc + line
|
current_cmd = current_cmd._replace(help=current_cmd.help + spc + line)
|
||||||
else:
|
else:
|
||||||
prev_indent = 0
|
prev_indent = 0
|
||||||
if prev_line:
|
if prev_line:
|
||||||
current_cmd['help'] += '\n' if current_cmd['help'].endswith('::') else '\n\n'
|
h = '\n' if current_cmd.help.endswith('::') else '\n\n'
|
||||||
|
current_cmd = current_cmd._replace(help=current_cmd.help + h)
|
||||||
else:
|
else:
|
||||||
state = NORMAL
|
state = NORMAL
|
||||||
(seq if current_cmd.get('condition', True) else disabled).append(current_cmd)
|
(seq if current_cmd.condition else disabled).append(current_cmd)
|
||||||
current_cmd = empty_cmd
|
current_cmd = empty_cmd
|
||||||
prev_line = line
|
prev_line = line
|
||||||
if current_cmd is not empty_cmd:
|
if current_cmd is not empty_cmd:
|
||||||
(seq if current_cmd.get('condition', True) else disabled).append(current_cmd)
|
(seq if current_cmd.condition else disabled).append(current_cmd)
|
||||||
|
|
||||||
return seq, disabled
|
return seq, disabled
|
||||||
|
|
||||||
|
|
||||||
def defval_for_opt(opt: OptionDict) -> Any:
|
def defval_for_opt(opt: OptionDefinition) -> Any:
|
||||||
dv: Any = opt.get('default')
|
dv: Any = opt.default
|
||||||
typ = opt.get('type', '')
|
typ = opt.type
|
||||||
if typ.startswith('bool-'):
|
if typ.startswith('bool-'):
|
||||||
if dv is None:
|
if dv is None:
|
||||||
dv = False if typ == 'bool-set' else True
|
dv = False if typ == 'bool-set' else True
|
||||||
@@ -230,16 +230,16 @@ def defval_for_opt(opt: OptionDict) -> Any:
|
|||||||
return dv
|
return dv
|
||||||
|
|
||||||
|
|
||||||
def get_option_maps(seq: OptionSpecSeq) -> tuple[dict[str, OptionDict], dict[str, OptionDict], dict[str, Any]]:
|
def get_option_maps(seq: OptionSpecSeq) -> tuple[dict[str, OptionDefinition], dict[str, OptionDefinition], dict[str, Any]]:
|
||||||
names_map: dict[str, OptionDict] = {}
|
names_map: dict[str, OptionDefinition] = {}
|
||||||
alias_map: dict[str, OptionDict] = {}
|
alias_map: dict[str, OptionDefinition] = {}
|
||||||
values_map: dict[str, Any] = {}
|
values_map: dict[str, Any] = {}
|
||||||
for opt in seq:
|
for opt in seq:
|
||||||
if isinstance(opt, str):
|
if isinstance(opt, str):
|
||||||
continue
|
continue
|
||||||
for alias in opt['aliases']:
|
for alias in opt.aliases:
|
||||||
alias_map[alias] = opt
|
alias_map[alias] = opt
|
||||||
name = opt['dest']
|
name = opt.dest
|
||||||
names_map[name] = opt
|
names_map[name] = opt
|
||||||
values_map[name] = defval_for_opt(opt)
|
values_map[name] = defval_for_opt(opt)
|
||||||
return names_map, alias_map, values_map
|
return names_map, alias_map, values_map
|
||||||
@@ -259,9 +259,9 @@ def add_list_values(*values: str) -> Iterator[str]:
|
|||||||
yield f'\tflag.defval.listval.items[{n}] = {c_str(value)};'
|
yield f'\tflag.defval.listval.items[{n}] = {c_str(value)};'
|
||||||
|
|
||||||
|
|
||||||
def generate_c_for_opt(name: str, defval: Any, opt: OptionDict) -> Iterator[str]:
|
def generate_c_for_opt(name: str, defval: Any, opt: OptionDefinition) -> Iterator[str]:
|
||||||
yield f'\tflag = (FlagSpec){{.dest={c_str(name)},}};'
|
yield f'\tflag = (FlagSpec){{.dest={c_str(name)},}};'
|
||||||
match opt['type']:
|
match opt.type:
|
||||||
case 'bool-set' | 'bool-reset':
|
case 'bool-set' | 'bool-reset':
|
||||||
yield '\tflag.defval.type = CLI_VALUE_BOOL;'
|
yield '\tflag.defval.type = CLI_VALUE_BOOL;'
|
||||||
yield f'\tflag.defval.boolval = {"true" if defval else "false"};'
|
yield f'\tflag.defval.boolval = {"true" if defval else "false"};'
|
||||||
@@ -278,7 +278,7 @@ def generate_c_for_opt(name: str, defval: Any, opt: OptionDict) -> Iterator[str]
|
|||||||
case 'choices':
|
case 'choices':
|
||||||
yield '\tflag.defval.type = CLI_VALUE_CHOICE;'
|
yield '\tflag.defval.type = CLI_VALUE_CHOICE;'
|
||||||
yield f'\tflag.defval.strval = {c_str(defval)};'
|
yield f'\tflag.defval.strval = {c_str(defval)};'
|
||||||
yield from add_list_values(*opt['choices'])
|
yield from add_list_values(*opt.choices)
|
||||||
case _:
|
case _:
|
||||||
yield '\tflag.defval.type = CLI_VALUE_STRING;'
|
yield '\tflag.defval.type = CLI_VALUE_STRING;'
|
||||||
if defval is not None:
|
if defval is not None:
|
||||||
@@ -289,22 +289,22 @@ def generate_c_parser_for(funcname: str, spec: str) -> Iterator[str]:
|
|||||||
seq, disabled = parse_option_spec(spec)
|
seq, disabled = parse_option_spec(spec)
|
||||||
names_map, _, defaults_map = get_option_maps(seq)
|
names_map, _, defaults_map = get_option_maps(seq)
|
||||||
if 'help' not in names_map:
|
if 'help' not in names_map:
|
||||||
names_map['help'] = {'type': 'bool-set', 'aliases': ('--help', '-h')} # type: ignore
|
names_map['help'] = OptionDefinition(type='bool-set', aliases=('--help', '-h'))
|
||||||
defaults_map['help'] = False
|
defaults_map['help'] = False
|
||||||
if 'version' not in names_map:
|
if 'version' not in names_map:
|
||||||
names_map['version'] = {'type': 'bool-set', 'aliases': ('--version', '-v')} # type: ignore
|
names_map['version'] = OptionDefinition(type='bool-set', aliases=('--version', '-v'))
|
||||||
defaults_map['version'] = False
|
defaults_map['version'] = False
|
||||||
|
|
||||||
yield f'static void\nparse_cli_for_{funcname}(CLISpec *spec, int argc, char **argv) {{' # }}
|
yield f'static void\nparse_cli_for_{funcname}(CLISpec *spec, int argc, char **argv) {{' # }}
|
||||||
yield '\tFlagSpec flag;'
|
yield '\tFlagSpec flag;'
|
||||||
for name, opt in names_map.items():
|
for name, opt in names_map.items():
|
||||||
for alias in opt['aliases']:
|
for alias in opt.aliases:
|
||||||
yield f'\tif (vt_is_end(vt_insert(&spec->alias_map, {c_str(alias)}, {c_str(name)}))) OOM;'
|
yield f'\tif (vt_is_end(vt_insert(&spec->alias_map, {c_str(alias)}, {c_str(name)}))) OOM;'
|
||||||
yield from generate_c_for_opt(name, defaults_map[name], opt)
|
yield from generate_c_for_opt(name, defaults_map[name], opt)
|
||||||
yield '\tif (vt_is_end(vt_insert(&spec->flag_map, flag.dest, flag))) OOM;'
|
yield '\tif (vt_is_end(vt_insert(&spec->flag_map, flag.dest, flag))) OOM;'
|
||||||
for d in disabled:
|
for d in disabled:
|
||||||
if not isinstance(d, str):
|
if not isinstance(d, str):
|
||||||
yield from generate_c_for_opt(d['dest'], defval_for_opt(d), d)
|
yield from generate_c_for_opt(d.dest, defval_for_opt(d), d)
|
||||||
yield '\tif (vt_is_end(vt_insert(&spec->disabled_map, flag.dest, flag))) OOM;'
|
yield '\tif (vt_is_end(vt_insert(&spec->disabled_map, flag.dest, flag))) OOM;'
|
||||||
|
|
||||||
yield '\tparse_cli_loop(spec, true, argc, argv);'
|
yield '\tparse_cli_loop(spec, true, argc, argv);'
|
||||||
|
|||||||
Reference in New Issue
Block a user