Allow specifying completion for command line arguments that expect paths
This commit is contained in:
parent
f15a2f0c1f
commit
d4df3f67b6
24
kitty/cli.py
24
kitty/cli.py
@ -2,6 +2,7 @@
|
||||
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
from collections import deque
|
||||
from typing import (
|
||||
@ -26,6 +27,7 @@ class OptionDict(TypedDict):
|
||||
type: str
|
||||
default: Optional[str]
|
||||
condition: bool
|
||||
completion: Dict[str, str]
|
||||
|
||||
|
||||
CONFIG_HELP = '''\
|
||||
@ -172,7 +174,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
|
||||
'type': '', 'condition': False, 'default': None, 'completion': {},
|
||||
}
|
||||
empty_cmd = current_cmd
|
||||
|
||||
@ -189,7 +191,7 @@ def parse_option_spec(spec: Optional[str] = None) -> Tuple[OptionSpecSeq, Option
|
||||
current_cmd = {
|
||||
'dest': parts[0][2:].replace('-', '_'), 'aliases': frozenset(parts), 'help': '',
|
||||
'choices': frozenset(), 'type': '',
|
||||
'default': None, 'condition': True
|
||||
'default': None, 'condition': True, 'completion': {}
|
||||
}
|
||||
state = METADATA
|
||||
continue
|
||||
@ -212,6 +214,11 @@ def parse_option_spec(spec: Optional[str] = None) -> Tuple[OptionSpecSeq, Option
|
||||
current_cmd['dest'] = v
|
||||
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
|
||||
elif state is HELP:
|
||||
if line:
|
||||
spc = '' if current_cmd['help'].endswith('\n') else ' '
|
||||
@ -444,6 +451,8 @@ def as_type_stub(seq: OptionSpecSeq, disabled: OptionSpecSeq, class_name: str, e
|
||||
t = 'typing.Optional[str]'
|
||||
elif otype == 'list':
|
||||
t = 'typing.Sequence[str]'
|
||||
elif otype == 'path':
|
||||
t = 'str'
|
||||
elif otype in ('choice', 'choices'):
|
||||
if opt['choices']:
|
||||
t = 'typing.Literal[{}]'.format(','.join(f'{x!r}' for x in opt['choices']))
|
||||
@ -526,6 +535,8 @@ class Options:
|
||||
raise SystemExit('{} is not a valid value for the {} option. Valid values are: {}'.format(
|
||||
val, emph(alias), ', '.join(choices)))
|
||||
self.values_map[name] = val
|
||||
elif typ == 'path':
|
||||
self.values_map[name] = val
|
||||
elif typ in nmap:
|
||||
f = nmap[typ]
|
||||
try:
|
||||
@ -616,6 +627,8 @@ Syntax: :italic:`name=value`. For example: :option:`{appname} -o` font_size=20
|
||||
|
||||
--directory --working-directory -d
|
||||
default=.
|
||||
type=path
|
||||
completion=type:directory
|
||||
Change to the specified directory when launching.
|
||||
|
||||
|
||||
@ -626,9 +639,12 @@ Detach from the controlling terminal, if any.
|
||||
|
||||
|
||||
--session
|
||||
type=path
|
||||
completion=ext:session relative:conf group:"Session files"
|
||||
Path to a file containing the startup :italic:`session` (tabs, windows, layout,
|
||||
programs). Use - to read from STDIN. See the :file:`README` file for details and
|
||||
an example.
|
||||
an example. Environment variables are expanded, relative paths are resolved relative
|
||||
to the kitty configuration directory.
|
||||
|
||||
|
||||
--hold
|
||||
@ -732,6 +748,8 @@ present in the main font.
|
||||
|
||||
|
||||
--watcher
|
||||
type=path
|
||||
completion=ext:py relative:conf group:"Watcher files"
|
||||
This option is deprecated in favor of the :opt:`watcher` option in
|
||||
:file:`{conf_name}.conf` and should not be used.
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ from .cli import (
|
||||
OptionDict, OptionSpecSeq, options_for_completion, parse_option_spec,
|
||||
prettify
|
||||
)
|
||||
from .constants import shell_integration_dir
|
||||
from .constants import shell_integration_dir, config_dir
|
||||
from .fast_data_types import truncate_point_for_length, wcswidth
|
||||
from .rc.base import all_command_names, command_for_name
|
||||
from .shell import options_for_cmd
|
||||
@ -395,12 +395,6 @@ def complete_kitty_cli_arg(ans: Completions, opt: Optional[OptionDict], prefix:
|
||||
return x.lower().endswith('.conf')
|
||||
|
||||
complete_files_and_dirs(ans, prefix, files_group_name='Config files', predicate=is_conf_file)
|
||||
elif dest == 'session':
|
||||
complete_files_and_dirs(ans, prefix, files_group_name='Session files')
|
||||
elif dest == 'watcher':
|
||||
complete_files_and_dirs(ans, prefix, files_group_name='Watcher files')
|
||||
elif dest == 'directory':
|
||||
complete_dirs(ans, prefix)
|
||||
elif dest == 'listen_on':
|
||||
if ':' not in prefix:
|
||||
k = 'Address type'
|
||||
@ -564,9 +558,98 @@ 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]:
|
||||
|
||||
if 'ext' in spec:
|
||||
extensions = frozenset(os.extsep + x.lower() for x in spec['ext'].split(','))
|
||||
else:
|
||||
extensions = frozenset()
|
||||
|
||||
if 'mime' in spec:
|
||||
from fnmatch import translate
|
||||
import re
|
||||
mimes = tuple(re.compile(translate(x)) for x in spec['mime'].split(','))
|
||||
from .guess_mime_type import guess_type
|
||||
else:
|
||||
mimes = ()
|
||||
|
||||
if mimes or extensions:
|
||||
def check_file(x: 'os.DirEntry[str]', result: str) -> bool:
|
||||
if extensions:
|
||||
q = result.lower()
|
||||
for ext in extensions:
|
||||
if q.endswith(ext):
|
||||
return True
|
||||
if mimes:
|
||||
mq = guess_type(result)
|
||||
if mq:
|
||||
for mime in mimes:
|
||||
if mime.match(mq):
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
def check_file(x: 'os.DirEntry[str]', result: str) -> bool:
|
||||
return True
|
||||
|
||||
return check_file
|
||||
|
||||
|
||||
def complete_file_path(ans: Completions, spec: Dict[str, str], 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:
|
||||
relative_to = os.getcwd()
|
||||
src_dir = relative_to
|
||||
check_against = prefix
|
||||
prefix_result_with = prefix
|
||||
files, dirs = [], []
|
||||
if prefix:
|
||||
expanded_prefix = os.path.expandvars(os.path.expanduser(prefix))
|
||||
check_against = os.path.basename(expanded_prefix)
|
||||
prefix_result_with = os.path.dirname(expanded_prefix).rstrip(os.sep) + os.sep
|
||||
if os.path.isabs(expanded_prefix):
|
||||
src_dir = os.path.dirname(expanded_prefix)
|
||||
elif os.sep in expanded_prefix or (os.altsep and os.altsep in expanded_prefix):
|
||||
src_dir = os.path.join(relative_to, os.path.dirname(expanded_prefix))
|
||||
else:
|
||||
prefix_result_with = ''
|
||||
try:
|
||||
items: Iterable['os.DirEntry[str]'] = os.scandir(src_dir)
|
||||
except OSError:
|
||||
items = ()
|
||||
check_file = filter_files_from_completion_spec(spec)
|
||||
for x in items:
|
||||
if not x.name.startswith(check_against):
|
||||
continue
|
||||
result = prefix_result_with + x.name
|
||||
if x.is_dir():
|
||||
dirs.append(result.rstrip(os.sep) + os.sep)
|
||||
else:
|
||||
if check_file(x, result):
|
||||
files.append(result)
|
||||
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)
|
||||
|
||||
|
||||
def complete_path(ans: Completions, opt: OptionDict, prefix: str) -> None:
|
||||
spec = opt['completion']
|
||||
t = spec.get('type', 'file')
|
||||
if t == 'file':
|
||||
complete_file_path(ans, spec, prefix)
|
||||
elif t == '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['type'] == 'path':
|
||||
complete_path(ans, opt, prefix)
|
||||
|
||||
|
||||
def complete_dirs(ans: Completions, prefix: str = '') -> None:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user