diff --git a/kittens/tui/path_completer.py b/kittens/tui/path_completer.py new file mode 100644 index 000000000..0a7502131 --- /dev/null +++ b/kittens/tui/path_completer.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPLv3 Copyright: 2020, Kovid Goyal + + +import os +from typing import Any, Dict, Generator, Optional, Sequence, Tuple + +from kitty.fast_data_types import wcswidth +from kitty.utils import ScreenSize, screen_size_function + +from .operations import styled + + +def directory_completions(path: str, qpath: str, prefix: str = '') -> Generator[str, None, None]: + try: + entries = os.scandir(qpath) + except OSError: + return + for x in entries: + try: + is_dir = x.is_dir() + except OSError: + is_dir = False + name = x.name + (os.sep if is_dir else '') + if not prefix or name.startswith(prefix): + if path: + yield os.path.join(path, name) + else: + yield name + + +def expand_path(path: str) -> str: + return os.path.abspath(os.path.expandvars(os.path.expanduser(path))) + + +def find_completions(path: str) -> Generator[str, None, None]: + qpath = expand_path(path) + if not path or path.endswith(os.sep): + yield from directory_completions(path, qpath) + else: + yield from directory_completions(os.path.dirname(path), os.path.dirname(qpath), os.path.basename(qpath)) + + +def print_table(items: Sequence[str], screen_size: ScreenSize) -> None: + max_width = 0 + item_widths = {} + for item in items: + item_widths[item] = w = wcswidth(item) + max_width = max(w, max_width) + col_width = max_width + 2 + num_of_cols = max(1, screen_size.cols // col_width) + cr = 0 + at_start = False + for item in items: + w = item_widths[item] + left = col_width - w + print(item, ' ' * left, sep='', end='') + at_start = False + cr = (cr + 1) % num_of_cols + if not cr: + print() + at_start = True + if not at_start: + print() + + +class PathCompleter: + + def __init__(self, prompt: str = '> '): + self.prompt = prompt + + def __enter__(self) -> 'PathCompleter': + import readline + if 'libedit' in readline.__doc__: + readline.parse_and_bind("bind -e") + readline.parse_and_bind("bind '\t' rl_complete") + else: + readline.parse_and_bind('tab: complete') + readline.parse_and_bind('set colored-stats on') + readline.set_completer_delims(' \t\n`!@#$%^&*()-=+[{]}\\|;:\'",<>?') + readline.set_completion_display_matches_hook(self.format_completions) + self.original_completer = readline.get_completer() + readline.set_completer(self) + self.cache: Dict[str, Tuple[str, ...]] = {} + return self + + def format_completions(self, substitution: str, matches: Sequence[str], longest_match_length: int) -> None: + import readline + print() + files, dirs = [], [] + for m in matches: + if m.endswith('/'): + if len(m) > 1: + m = m[:-1] + dirs.append(m) + else: + files.append(m) + + ss = screen_size_function()() + if dirs: + print(styled('Directories', bold=True, fg_intense=True)) + print_table(dirs, ss) + if files: + print(styled('Files', bold=True, fg_intense=True)) + print_table(files, ss) + + print(self.prompt, readline.get_line_buffer(), sep='', end='', flush=True) + + def __call__(self, text: str, state: int) -> Optional[str]: + options = self.cache.get(text) + if options is None: + options = self.cache[text] = tuple(find_completions(text)) + if options and state < len(options): + return options[state] + + def __exit__(self, *a: Any) -> bool: + import readline + del self.cache + readline.set_completer(self.original_completer) + readline.set_completion_display_matches_hook() + return True + + def input(self) -> str: + with self: + return input(self.prompt) + + +def develop() -> None: + PathCompleter().input()