Use the new completionspec for rc cmds
This commit is contained in:
parent
d84efe105c
commit
26d4f5bcc9
65
kitty/cli.py
65
kitty/cli.py
@ -7,6 +7,7 @@ import shlex
|
||||
import socket
|
||||
import sys
|
||||
from collections import deque
|
||||
from enum import Enum, auto
|
||||
from typing import (
|
||||
Any, Callable, Dict, FrozenSet, Iterator, List, Match, Optional, Sequence,
|
||||
Tuple, Type, TypeVar, Union, cast
|
||||
@ -15,8 +16,8 @@ from typing import (
|
||||
from .cli_stub import CLIOptions
|
||||
from .conf.utils import resolve_config
|
||||
from .constants import (
|
||||
appname, clear_handled_signals, config_dir, defconf, is_macos, str_version,
|
||||
website_url, default_pager_for_help
|
||||
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
|
||||
@ -24,6 +25,55 @@ from .types import run_once
|
||||
from .typing import BadLineType, TypedDict
|
||||
|
||||
|
||||
class CompletionType(Enum):
|
||||
file = auto()
|
||||
directory = auto()
|
||||
keyword = auto()
|
||||
none = auto()
|
||||
|
||||
|
||||
class CompletionRelativeTo(Enum):
|
||||
cwd = auto()
|
||||
config_dir = auto()
|
||||
|
||||
|
||||
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) -> Iterator[str]:
|
||||
pass
|
||||
|
||||
|
||||
class OptionDict(TypedDict):
|
||||
dest: str
|
||||
name: str
|
||||
@ -33,7 +83,7 @@ class OptionDict(TypedDict):
|
||||
type: str
|
||||
default: Optional[str]
|
||||
condition: bool
|
||||
completion: Dict[str, str]
|
||||
completion: CompletionSpec
|
||||
|
||||
|
||||
def serialize_as_go_string(x: str) -> str:
|
||||
@ -343,7 +393,7 @@ def parse_option_spec(spec: Optional[str] = None) -> Tuple[OptionSpecSeq, Option
|
||||
mpat = re.compile('([a-z]+)=(.+)')
|
||||
current_cmd: OptionDict = {
|
||||
'dest': '', 'aliases': frozenset(), 'help': '', 'choices': frozenset(),
|
||||
'type': '', 'condition': False, 'default': None, 'completion': {}, 'name': ''
|
||||
'type': '', 'condition': False, 'default': None, 'completion': CompletionSpec(), 'name': ''
|
||||
}
|
||||
empty_cmd = current_cmd
|
||||
|
||||
@ -364,7 +414,7 @@ def parse_option_spec(spec: Optional[str] = None) -> Tuple[OptionSpecSeq, Option
|
||||
current_cmd = {
|
||||
'dest': defdest, 'aliases': frozenset(parts), 'help': '',
|
||||
'choices': frozenset(), 'type': '', 'name': defdest,
|
||||
'default': None, 'condition': True, 'completion': {}
|
||||
'default': None, 'condition': True, 'completion': CompletionSpec(),
|
||||
}
|
||||
state = METADATA
|
||||
continue
|
||||
@ -391,10 +441,7 @@ def parse_option_spec(spec: Optional[str] = None) -> Tuple[OptionSpecSeq, Option
|
||||
elif k == 'condition':
|
||||
current_cmd['condition'] = bool(eval(v))
|
||||
elif k == 'completion':
|
||||
cv = current_cmd['completion'] = {}
|
||||
for x in shlex.split(v):
|
||||
ck, vv = x.split(':', 1)
|
||||
cv[ck] = vv
|
||||
current_cmd['completion'] = CompletionSpec.from_string(v)
|
||||
elif state is HELP:
|
||||
if line:
|
||||
current_indent = indent_of_line(line)
|
||||
|
||||
@ -14,12 +14,13 @@ from kittens.runner import (
|
||||
)
|
||||
|
||||
from .cli import (
|
||||
OptionDict, options_for_completion, parse_option_spec, prettify
|
||||
CompletionSpec, CompletionType, OptionDict, options_for_completion,
|
||||
parse_option_spec, prettify
|
||||
)
|
||||
from .remote_control import global_options_spec
|
||||
from .constants import config_dir, shell_integration_dir
|
||||
from .fast_data_types import truncate_point_for_length, wcswidth
|
||||
from .rc.base import all_command_names, command_for_name
|
||||
from .rc.base import all_command_names
|
||||
from .remote_control import global_options_spec
|
||||
from .shell import options_for_cmd
|
||||
from .types import run_once
|
||||
from .utils import screen_size_function
|
||||
@ -491,19 +492,7 @@ def global_options_for_remote_cmd() -> Dict[str, OptionDict]:
|
||||
|
||||
def complete_remote_command(ans: Completions, cmd_name: str, words: Sequence[str], new_word: bool) -> None:
|
||||
aliases, alias_map = options_for_cmd(cmd_name)
|
||||
try:
|
||||
args_completion = command_for_name(cmd_name).args.completion
|
||||
except KeyError:
|
||||
return
|
||||
args_completer: CompleteArgsFunc = basic_option_arg_completer
|
||||
if args_completion:
|
||||
if 'files' in args_completion:
|
||||
title, matchers = args_completion['files']
|
||||
if isinstance(matchers, tuple):
|
||||
args_completer = remote_files_completer(title, matchers)
|
||||
elif 'names' in args_completion:
|
||||
title, q = args_completion['names']
|
||||
args_completer = remote_args_completer(title, q() if callable(q) else q)
|
||||
complete_alias_map(ans, words, new_word, alias_map, complete_args=args_completer)
|
||||
|
||||
|
||||
@ -566,17 +555,17 @@ def complete_files_and_dirs(
|
||||
ans.add_match_group(files_group_name, files, is_files=True)
|
||||
|
||||
|
||||
def filter_files_from_completion_spec(spec: Dict[str, str]) -> Callable[['os.DirEntry[str]', str], bool]:
|
||||
def filter_files_from_completion_spec(spec: CompletionSpec) -> Callable[['os.DirEntry[str]', str], bool]:
|
||||
|
||||
if 'ext' in spec:
|
||||
extensions = frozenset(os.extsep + x.lower() for x in spec['ext'].split(','))
|
||||
if spec.extensions:
|
||||
extensions = frozenset(os.extsep + x.lower() for x in spec.extensions)
|
||||
else:
|
||||
extensions = frozenset()
|
||||
|
||||
if 'mime' in spec:
|
||||
if spec.mime_patterns:
|
||||
import re
|
||||
from fnmatch import translate
|
||||
mimes = tuple(re.compile(translate(x)) for x in spec['mime'].split(','))
|
||||
mimes = tuple(re.compile(translate(x)) for x in spec.mime_patterns)
|
||||
from .guess_mime_type import guess_type
|
||||
else:
|
||||
mimes = ()
|
||||
@ -602,14 +591,12 @@ def filter_files_from_completion_spec(spec: Dict[str, str]) -> Callable[['os.Dir
|
||||
return check_file
|
||||
|
||||
|
||||
def complete_file_path(ans: Completions, spec: Dict[str, str], prefix: str, only_dirs: bool = False) -> None:
|
||||
def complete_file_path(ans: Completions, spec: CompletionSpec, prefix: str, only_dirs: bool = False) -> None:
|
||||
prefix = prefix.replace(r'\ ', ' ')
|
||||
relative_to = spec.get('relative', '')
|
||||
if relative_to:
|
||||
if relative_to == 'conf':
|
||||
relative_to = config_dir
|
||||
else:
|
||||
if spec.relative_to is spec.relative_to.__class__.cwd:
|
||||
relative_to = os.getcwd()
|
||||
else:
|
||||
relative_to = config_dir
|
||||
src_dir = relative_to
|
||||
check_against = prefix
|
||||
prefix_result_with = prefix
|
||||
@ -641,26 +628,25 @@ def complete_file_path(ans: Completions, spec: Dict[str, str], prefix: str, only
|
||||
if dirs:
|
||||
ans.add_match_group('Directories', dirs, trailing_space=False, is_files=True)
|
||||
if not only_dirs and files:
|
||||
ans.add_match_group(spec.get('group') or 'Files', files, is_files=True)
|
||||
ans.add_match_group(spec.group or 'Files', files, is_files=True)
|
||||
|
||||
|
||||
def complete_path(ans: Completions, opt: OptionDict, prefix: str) -> None:
|
||||
spec = opt['completion']
|
||||
t = spec['type']
|
||||
if 'kwds' in spec:
|
||||
kwds = [x for x in spec['kwds'].split(',') if x.startswith(prefix)]
|
||||
if spec.kwds:
|
||||
kwds = [x for x in spec.kwds if x.startswith(prefix)]
|
||||
if kwds:
|
||||
ans.add_match_group('Keywords', kwds)
|
||||
if t == 'file':
|
||||
if spec.type is CompletionType.file:
|
||||
complete_file_path(ans, spec, prefix)
|
||||
elif t == 'directory':
|
||||
elif spec.type is CompletionType.directory:
|
||||
complete_file_path(ans, spec, prefix, only_dirs=True)
|
||||
|
||||
|
||||
def complete_basic_option_args(ans: Completions, opt: OptionDict, prefix: str) -> None:
|
||||
if opt['choices']:
|
||||
ans.add_match_group(f'Choices for {opt["dest"]}', tuple(k for k in opt['choices'] if k.startswith(prefix)))
|
||||
elif opt['completion'].get('type') in ('file', 'directory'):
|
||||
elif opt['completion'].type is not CompletionType.none:
|
||||
complete_path(ans, opt, prefix)
|
||||
|
||||
|
||||
|
||||
@ -6,10 +6,10 @@ from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from typing import (
|
||||
TYPE_CHECKING, Any, Callable, Dict, FrozenSet, Iterable, Iterator,
|
||||
List, NoReturn, Optional, Set, Tuple, Type, Union, cast, Mapping
|
||||
List, NoReturn, Optional, Set, Tuple, Type, Union, cast
|
||||
)
|
||||
|
||||
from kitty.cli import get_defaults_from_seq, parse_args, parse_option_spec
|
||||
from kitty.cli import get_defaults_from_seq, parse_args, parse_option_spec, CompletionSpec
|
||||
from kitty.cli_stub import RCOptions as R
|
||||
from kitty.constants import appname, list_kitty_resources, running_in_kitty
|
||||
from kitty.types import AsyncResponse
|
||||
@ -88,7 +88,8 @@ CmdGenerator = Iterator[CmdReturnType]
|
||||
PayloadType = Optional[Union[CmdReturnType, CmdGenerator]]
|
||||
PayloadGetType = PayloadGetter
|
||||
ArgsType = List[str]
|
||||
ImageCompletion: Dict[str, Tuple[str, Tuple[str, ...]]] = {'files': ('Images', ('*.png', '*.jpg', '*.jpeg', '*.webp', '*.gif', '*.bmp', '*.tiff'))}
|
||||
ImageCompletion = CompletionSpec.from_string('type:file group:"Images"')
|
||||
ImageCompletion.extensions = 'png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'tiff'
|
||||
|
||||
|
||||
MATCH_WINDOW_OPTION = '''\
|
||||
@ -184,7 +185,7 @@ class ArgsHandling:
|
||||
json_field: str = ''
|
||||
count: Optional[int] = None
|
||||
spec: str = ''
|
||||
completion: Optional[Mapping[str, Tuple[str, Union[Callable[[], Iterable[str]], Tuple[str, ...]]]]] = None
|
||||
completion: CompletionSpec = CompletionSpec()
|
||||
value_if_unspecified: Tuple[str, ...] = ()
|
||||
minimum_count: int = -1
|
||||
first_rest: Optional[Tuple[str, str]] = None
|
||||
@ -198,24 +199,11 @@ class ArgsHandling:
|
||||
return self.count
|
||||
|
||||
def as_go_completion_code(self, go_name: str) -> Iterator[str]:
|
||||
from ..cli import serialize_as_go_string
|
||||
c = self.args_count
|
||||
if c is not None:
|
||||
yield f'{go_name}.Stop_processing_at_arg = {c}'
|
||||
if self.completion:
|
||||
for k, v in self.completion.items():
|
||||
if k == 'files':
|
||||
title, pats = v
|
||||
assert isinstance(pats, tuple)
|
||||
gpats = (f'"{serialize_as_go_string(x)}"' for x in pats)
|
||||
yield f'{go_name}.Completion_for_arg = fnmatch_completer("{serialize_as_go_string(title)}", {", ".join(gpats)})'
|
||||
elif k == 'names':
|
||||
title, gen = v
|
||||
assert callable(gen)
|
||||
gpats = (f'"{serialize_as_go_string(x)}"' for x in gen())
|
||||
yield f'{go_name}.Completion_for_arg = names_completer("{serialize_as_go_string(title)}", {", ".join(gpats)})'
|
||||
else:
|
||||
raise KeyError(f'Unknown args completion type: {k}')
|
||||
yield from self.completion.as_go_code(go_name)
|
||||
|
||||
def as_go_code(self, cmd_name: str, field_types: Dict[str, str], handled_fields: Set[str]) -> Iterator[str]:
|
||||
c = self.args_count
|
||||
@ -313,6 +301,7 @@ class StreamInFlight:
|
||||
|
||||
class RemoteCommand:
|
||||
Args = ArgsHandling
|
||||
CompletionSpec = CompletionSpec
|
||||
|
||||
name: str = ''
|
||||
short_desc: str = ''
|
||||
|
||||
@ -30,7 +30,10 @@ class GotoLayout(RemoteCommand):
|
||||
' You can use special match value :code:`all` to set the layout in all tabs.'
|
||||
)
|
||||
options_spec = MATCH_TAB_OPTION
|
||||
args = RemoteCommand.Args(spec='LAYOUT_NAME', count=1, completion={'names': ('Layouts', layout_names)}, json_field='layout', args_choices=layout_names)
|
||||
args = RemoteCommand.Args(
|
||||
spec='LAYOUT_NAME', count=1, json_field='layout',
|
||||
completion=RemoteCommand.CompletionSpec.from_string('type:keyword group:"Layout" kwds:' + ','.join(layout_names())),
|
||||
args_choices=layout_names)
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
if len(args) != 1:
|
||||
|
||||
@ -89,8 +89,8 @@ type=bool-set
|
||||
Restore all colors to the values they had at kitty startup. Note that if you specify
|
||||
this option, any color arguments are ignored and :option:`kitty @ set-colors --configured` and :option:`kitty @ set-colors --all` are implied.
|
||||
''' + '\n\n' + MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t')
|
||||
args = RemoteCommand.Args(spec='COLOR_OR_FILE ...', json_field='colors', special_parse='parse_colors_and_files(args)', completion={
|
||||
'files': ('CONF files', ('*.conf',))})
|
||||
args = RemoteCommand.Args(spec='COLOR_OR_FILE ...', json_field='colors', special_parse='parse_colors_and_files(args)',
|
||||
completion=RemoteCommand.CompletionSpec.from_string('type:file group:"CONF files", ext:conf'))
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
final_colors: Dict[str, Optional[int]] = {}
|
||||
|
||||
@ -42,7 +42,9 @@ Change the default enabled layout value so that the new value takes effect for a
|
||||
as well.
|
||||
'''
|
||||
args = RemoteCommand.Args(
|
||||
spec='LAYOUT ...', minimum_count=1, json_field='layouts', completion={'names': ('Layouts', layout_names)}, args_choices=layout_names)
|
||||
spec='LAYOUT ...', minimum_count=1, json_field='layouts',
|
||||
completion=RemoteCommand.CompletionSpec.from_string('type:keyword group:"Layout" kwds:' + ','.join(layout_names())),
|
||||
args_choices=layout_names)
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
if len(args) < 1:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user