more typing work

This commit is contained in:
Kovid Goyal 2020-03-12 13:51:51 +05:30
parent 8a3e71d3d0
commit cda1e28b32
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 97 additions and 64 deletions

View File

@ -4,54 +4,56 @@
import sys
import os
from typing import List
def icat(args):
from kittens.runner import run_kitten
def icat(args: List[str]) -> None:
from kittens.runner import run_kitten as rk
sys.argv = args
run_kitten('icat')
rk('icat')
def list_fonts(args):
def list_fonts(args: List[str]) -> None:
from kitty.fonts.list import main as list_main
list_main(args)
def remote_control(args):
def remote_control(args: List[str]) -> None:
from kitty.remote_control import main as rc_main
rc_main(args)
def runpy(args):
def runpy(args: List[str]) -> None:
sys.argv = ['kitty'] + args[2:]
exec(args[1])
def hold(args):
def hold(args: List[str]) -> None:
import subprocess
ret = subprocess.Popen(args[1:]).wait()
sys.stdin.read()
raise SystemExit(ret)
def complete(args):
def complete(args: List[str]) -> None:
from kitty.complete import main as complete_main
complete_main(args[1:], entry_points, namespaced_entry_points)
def launch(args):
def launch(args: List[str]) -> None:
import runpy
sys.argv = args[1:]
exe = args[1]
if exe.startswith(':'):
import shutil
exe = shutil.which(exe[1:])
if not exe:
q = shutil.which(exe[1:])
if not q:
raise SystemExit('{} not found in PATH'.format(args[1][1:]))
exe = q
runpy.run_path(exe, run_name='__main__')
def run_kitten(args):
def run_kitten(args: List[str]) -> None:
try:
kitten = args[1]
except IndexError:
@ -59,11 +61,11 @@ def run_kitten(args):
list_kittens()
raise SystemExit(1)
sys.argv = args[1:]
from kittens.runner import run_kitten
run_kitten(kitten)
from kittens.runner import run_kitten as rk
rk(kitten)
def namespaced(args):
def namespaced(args: List[str]) -> None:
func = namespaced_entry_points[args[1]]
func(args[1:])
@ -84,7 +86,7 @@ namespaced_entry_points['hold'] = hold
namespaced_entry_points['complete'] = complete
def setup_openssl_environment():
def setup_openssl_environment() -> None:
# Workaround for Linux distros that have still failed to get their heads
# out of their asses and implement a common location for SSL certificates.
# It's not that hard people, there exists a wonderful tool called the symlink
@ -98,7 +100,7 @@ def setup_openssl_environment():
setattr(sys, 'kitty_ssl_env_var', 'SSL_CERT_DIR')
def main():
def main() -> None:
if getattr(sys, 'frozen', False) and 'darwin' not in sys.platform.lower():
setup_openssl_environment()
first_arg = '' if len(sys.argv) < 2 else sys.argv[1]

View File

@ -6,7 +6,9 @@ import os
import shlex
import sys
from functools import lru_cache
from typing import Callable, Dict, Iterable, Optional, Tuple
from typing import (
Any, Callable, Dict, Iterable, List, Optional, Sequence, Set, Tuple
)
from kittens.runner import all_kitten_names, get_kitten_cli_docs
@ -37,19 +39,20 @@ them into something your shell will understand.
parsers: Dict[str, Callable] = {}
serializers: Dict[str, Callable] = {}
MathGroup = Dict[str, str]
def debug(*a, **kw):
def debug(*a: Any, **kw: Any) -> None:
kw['file'] = sys.stderr
print(*a, **kw)
class Completions:
def __init__(self):
self.match_groups = {}
self.no_space_groups = set()
self.files_groups = set()
def __init__(self) -> None:
self.match_groups: Dict[str, MathGroup] = {}
self.no_space_groups: Set[str] = set()
self.files_groups: Set[str] = set()
@lru_cache(maxsize=2)
@ -97,40 +100,44 @@ complete -f -c kitty -a "(__kitty_completions)"
''',
}
ParseResult = Tuple[List[str], bool]
ParserFunc = Callable[[str], ParseResult]
SerializerFunc = Callable[[Completions], str]
def input_parser(func):
def input_parser(func: ParserFunc) -> ParserFunc:
name = func.__name__.split('_')[0]
parsers[name] = func
return func
def output_serializer(func):
def output_serializer(func: SerializerFunc) -> SerializerFunc:
name = func.__name__.split('_')[0]
serializers[name] = func
return func
@input_parser
def zsh_input_parser(data):
def zsh_input_parser(data: str) -> ParseResult:
new_word = data.endswith('\n\n')
words = data.rstrip().splitlines()
return words, new_word
@input_parser
def bash_input_parser(data):
def bash_input_parser(data: str) -> ParseResult:
new_word = data.endswith('\n\n')
words = data.rstrip().splitlines()
return words, new_word
@input_parser
def fish_input_parser(data):
def fish_input_parser(data: str) -> ParseResult:
return data.rstrip().splitlines(), True
@output_serializer
def zsh_output_serializer(ans):
def zsh_output_serializer(ans: Completions) -> str:
lines = []
for description, matches in ans.match_groups.items():
cmd = ['compadd', '-U', '-J', shlex.quote(description), '-X', shlex.quote(description)]
@ -143,7 +150,7 @@ def zsh_output_serializer(ans):
cmd.extend(('-p', shlex.quote(common_prefix)))
matches = {k[len(common_prefix):]: v for k, v in matches.items()}
cmd.append('--')
for word, description in matches.items():
for word in matches:
cmd.append(shlex.quote(word))
lines.append(' '.join(cmd) + ';')
# debug('\n'.join(lines))
@ -151,7 +158,7 @@ def zsh_output_serializer(ans):
@output_serializer
def bash_output_serializer(ans):
def bash_output_serializer(ans: Completions) -> str:
lines = []
for description, matches in ans.match_groups.items():
needs_space = description not in ans.no_space_groups
@ -164,7 +171,7 @@ def bash_output_serializer(ans):
@output_serializer
def fish_output_serializer(ans):
def fish_output_serializer(ans: Completions) -> str:
lines = []
for matches in ans.match_groups.values():
for word in matches:
@ -174,16 +181,16 @@ def fish_output_serializer(ans):
# }}}
def completions_for_first_word(ans, prefix, entry_points, namespaced_entry_points):
def completions_for_first_word(ans: Completions, prefix: str, entry_points: Iterable[str], namespaced_entry_points: Iterable[str]) -> None:
cmds = ['@' + c for c in remote_control_command_names()]
ans.match_groups['Entry points'] = {
k: None for k in
k: '' for k in
list(entry_points) + cmds + ['+' + k for k in namespaced_entry_points]
if not prefix or k.startswith(prefix)
}
def kitty_cli_opts(ans, prefix=None):
def kitty_cli_opts(ans: Completions, prefix: Optional[str] = None) -> None:
matches = {}
for opt in options_for_completion():
if isinstance(opt, str):
@ -194,7 +201,7 @@ def kitty_cli_opts(ans, prefix=None):
ans.match_groups['Options'] = matches
def complete_kitty_cli_arg(ans, opt, prefix):
def complete_kitty_cli_arg(ans: Completions, opt: Optional[OptionDict], prefix: str) -> None:
prefix = prefix or ''
if not opt:
return
@ -202,11 +209,11 @@ def complete_kitty_cli_arg(ans, opt, prefix):
if dest == 'override':
from kitty.config import option_names_for_completion
k = 'Config directives'
ans.match_groups[k] = {k+'=': None for k in option_names_for_completion() if k.startswith(prefix)}
ans.match_groups[k] = {k+'=': '' for k in option_names_for_completion() if k.startswith(prefix)}
ans.no_space_groups.add(k)
elif dest == 'config':
def is_conf_file(x):
def is_conf_file(x: str) -> bool:
if os.path.isdir(x):
return True
return x.lower().endswith('.conf')
@ -229,14 +236,23 @@ def complete_kitty_cli_arg(ans, opt, prefix):
complete_files_and_dirs(ans, prefix[len('unix:'):], files_group_name='UNIX sockets', add_prefix='unix:')
def complete_alias_map(ans, words, new_word, option_map, complete_args=None):
CompleteArgsFunc = Callable[[Completions, Optional[OptionDict], str], None]
def complete_alias_map(
ans: Completions,
words: Sequence[str],
new_word: str,
option_map: Dict[str, OptionDict],
complete_args: Optional[CompleteArgsFunc] = None
) -> None:
expecting_arg = False
opt: Optional[OptionDict] = None
last_word = words[-1] if words else ''
for w in words:
if expecting_arg:
if w is last_word and not new_word:
if opt is not None:
if opt is not None and complete_args is not None:
complete_args(ans, opt, w)
return
expecting_arg = False
@ -264,7 +280,13 @@ def complete_alias_map(ans, words, new_word, option_map, complete_args=None):
ans.match_groups['Options'] = {k: opt['help'] for k, opt in option_map.items() if k.startswith(prefix)}
def complete_cli(ans, words, new_word, seq: OptionSpecSeq, complete_args=lambda *a: None):
def complete_cli(
ans: Completions,
words: Sequence[str],
new_word: str,
seq: OptionSpecSeq,
complete_args: Optional[CompleteArgsFunc] = None
) -> None:
option_map = {}
for opt in seq:
if not isinstance(opt, str):
@ -273,18 +295,18 @@ def complete_cli(ans, words, new_word, seq: OptionSpecSeq, complete_args=lambda
complete_alias_map(ans, words, new_word, option_map, complete_args)
def complete_remote_command(ans, cmd_name, words, new_word):
def complete_remote_command(ans: Completions, cmd_name: str, words: Sequence[str], new_word: str) -> None:
aliases, alias_map = options_for_cmd(cmd_name)
if not alias_map:
return
args_completer = None
args_completer: Optional[CompleteArgsFunc] = None
args_completion = command_for_name(cmd_name).args_completion
if args_completion and 'files' in args_completion:
args_completer = remote_files_completer(args_completion['files'])
complete_alias_map(ans, words, new_word, alias_map, complete_args=args_completer)
def path_completion(prefix=''):
def path_completion(prefix: str = '') -> Tuple[List[str], List[str]]:
prefix = prefix.replace(r'\ ', ' ')
dirs, files = [], []
base = '.'
@ -314,61 +336,70 @@ def path_completion(prefix=''):
return dirs, files
def complete_files_and_dirs(ans, prefix, files_group_name='Files', predicate=None, add_prefix=None):
dirs, files = path_completion(prefix or '')
files = filter(predicate, files)
def complete_files_and_dirs(
ans: Completions,
prefix: str,
files_group_name: str = 'Files',
predicate: Optional[Callable[[str], bool]] = None,
add_prefix: Optional[str] = None
) -> None:
dirs, files_ = path_completion(prefix or '')
files = filter(predicate, files_)
if add_prefix:
dirs = (add_prefix + x for x in dirs)
dirs = list(add_prefix + x for x in dirs)
files = (add_prefix + x for x in files)
if dirs:
ans.match_groups['Directories'] = dict.fromkeys(dirs)
ans.files_groups.add('Directories'), ans.no_space_groups.add('Directories')
ans.files_groups.add('Directories')
ans.no_space_groups.add('Directories')
if files:
ans.match_groups[files_group_name] = dict.fromkeys(files)
ans.files_groups.add(files_group_name)
def complete_icat_args(ans, opt, prefix):
def complete_icat_args(ans: Completions, opt: Optional[OptionDict], prefix: str) -> None:
from mimetypes import guess_type
def icat_file_predicate(filename):
def icat_file_predicate(filename: str) -> bool:
mt = guess_type(filename)[0]
if mt and mt.startswith('image/'):
return True
return False
if opt is None:
complete_files_and_dirs(ans, prefix, 'Images', icat_file_predicate)
def remote_files_completer(spec):
def remote_files_completer(spec: Tuple[str, Tuple[str, ...]]) -> CompleteArgsFunc:
name, matchers = spec
def complete_files_map(ans, opt, prefix):
def complete_files_map(ans: Completions, opt: Optional[OptionDict], prefix: str) -> None:
def predicate(filename):
def predicate(filename: str) -> bool:
for m in matchers:
if isinstance(m, str):
from fnmatch import fnmatch
return fnmatch(filename, m)
return False
if opt is None:
complete_files_and_dirs(ans, prefix, name, predicate)
return complete_files_map
def config_file_predicate(filename):
def config_file_predicate(filename: str) -> bool:
return filename.endswith('.conf')
def complete_diff_args(ans, opt, prefix):
def complete_diff_args(ans: Completions, opt: Optional[OptionDict], prefix: str) -> None:
if opt is None:
complete_files_and_dirs(ans, prefix, 'Files')
elif opt['dest'] == 'config':
complete_files_and_dirs(ans, prefix, 'Config Files', config_file_predicate)
def complete_kitten(ans, kitten, words, new_word):
def complete_kitten(ans: Completions, kitten: str, words: Sequence[str], new_word: str) -> None:
try:
cd = get_kitten_cli_docs(kitten)
except SystemExit:
@ -388,7 +419,7 @@ def complete_kitten(ans, kitten, words, new_word):
}.get(kitten))
def find_completions(words, new_word, entry_points, namespaced_entry_points):
def find_completions(words: Sequence[str], new_word: str, entry_points: Iterable[str], namespaced_entry_points: Iterable[str]) -> Completions:
ans = Completions()
if not words or words[0] != 'kitty':
return ans
@ -401,20 +432,20 @@ def find_completions(words, new_word, entry_points, namespaced_entry_points):
if words[0] == '@':
if len(words) == 1 or (len(words) == 2 and not new_word):
prefix = words[1] if len(words) > 1 else ''
ans.match_groups['Remote control commands'] = {c: None for c in remote_control_command_names() if c.startswith(prefix)}
ans.match_groups['Remote control commands'] = {c: '' for c in remote_control_command_names() if c.startswith(prefix)}
else:
complete_remote_command(ans, words[1], words[2:], new_word)
return ans
if words[0].startswith('@'):
if len(words) == 1 and not new_word:
prefix = words[0]
ans.match_groups['Remote control commands'] = {'@' + c: None for c in remote_control_command_names() if c.startswith(prefix)}
ans.match_groups['Remote control commands'] = {'@' + c: '' for c in remote_control_command_names() if c.startswith(prefix)}
else:
complete_remote_command(ans, words[0][1:], words[1:], new_word)
if words[0] == '+':
if len(words) == 1 or (len(words) == 2 and not new_word):
prefix = words[1] if len(words) > 1 else ''
ans.match_groups['Entry points'] = {c: None for c in namespaced_entry_points if c.startswith(prefix)}
ans.match_groups['Entry points'] = {c: '' for c in namespaced_entry_points if c.startswith(prefix)}
else:
if words[1] == 'kitten':
if len(words) == 2 or (len(words) == 3 and not new_word):
@ -429,7 +460,7 @@ def find_completions(words, new_word, entry_points, namespaced_entry_points):
ans.match_groups['Kittens'] = dict.fromkeys(all_kitten_names())
else:
prefix = words[0]
ans.match_groups['Entry points'] = {c: None for c in namespaced_entry_points if c.startswith(prefix)}
ans.match_groups['Entry points'] = {c: '' for c in namespaced_entry_points if c.startswith(prefix)}
else:
if len(words) == 2 and not new_word:
ans.match_groups['Kittens'] = dict.fromkeys(k for k in all_kitten_names() if k.startswith(words[1]))
@ -442,11 +473,11 @@ def find_completions(words, new_word, entry_points, namespaced_entry_points):
return ans
def setup(cstyle):
def setup(cstyle: str) -> None:
print(completion_scripts[cstyle])
def main(args, entry_points, namespaced_entry_points):
def main(args: Sequence[str], entry_points: Iterable[str], namespaced_entry_points: Iterable[str]) -> None:
if not args:
raise SystemExit('Must specify completion style')
cstyle = args[0]