diff --git a/__main__.py b/__main__.py index 51e4173b6..c848929c8 100644 --- a/__main__.py +++ b/__main__.py @@ -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] diff --git a/kitty/complete.py b/kitty/complete.py index c930c5249..0d7ed345a 100644 --- a/kitty/complete.py +++ b/kitty/complete.py @@ -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]