1#!/usr/local/bin/python3.8 2# vim:fileencoding=utf-8 3# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net> 4 5 6import os 7from typing import Any, Callable, Dict, Generator, Optional, Sequence, Tuple 8 9from kitty.fast_data_types import wcswidth 10from kitty.utils import ScreenSize, screen_size_function 11 12from .operations import styled 13 14 15def directory_completions(path: str, qpath: str, prefix: str = '') -> Generator[str, None, None]: 16 try: 17 entries = os.scandir(qpath) 18 except OSError: 19 return 20 for x in entries: 21 try: 22 is_dir = x.is_dir() 23 except OSError: 24 is_dir = False 25 name = x.name + (os.sep if is_dir else '') 26 if not prefix or name.startswith(prefix): 27 if path: 28 yield os.path.join(path, name) 29 else: 30 yield name 31 32 33def expand_path(path: str) -> str: 34 return os.path.abspath(os.path.expandvars(os.path.expanduser(path))) 35 36 37def find_completions(path: str) -> Generator[str, None, None]: 38 if path and path[0] == '~': 39 if path == '~': 40 yield '~' + os.sep 41 return 42 if os.sep not in path: 43 qpath = os.path.expanduser(path) 44 if qpath != path: 45 yield path + os.sep 46 return 47 qpath = expand_path(path) 48 if not path or path.endswith(os.sep): 49 yield from directory_completions(path, qpath) 50 else: 51 yield from directory_completions(os.path.dirname(path), os.path.dirname(qpath), os.path.basename(qpath)) 52 53 54def print_table(items: Sequence[str], screen_size: ScreenSize, dir_colors: Callable[[str, str], str]) -> None: 55 max_width = 0 56 item_widths = {} 57 for item in items: 58 item_widths[item] = w = wcswidth(item) 59 max_width = max(w, max_width) 60 col_width = max_width + 2 61 num_of_cols = max(1, screen_size.cols // col_width) 62 cr = 0 63 at_start = False 64 for item in items: 65 w = item_widths[item] 66 left = col_width - w 67 print(dir_colors(expand_path(item), item), ' ' * left, sep='', end='') 68 at_start = False 69 cr = (cr + 1) % num_of_cols 70 if not cr: 71 print() 72 at_start = True 73 if not at_start: 74 print() 75 76 77class PathCompleter: 78 79 def __init__(self, prompt: str = '> '): 80 self.prompt = prompt 81 self.prompt_len = wcswidth(self.prompt) 82 83 def __enter__(self) -> 'PathCompleter': 84 import readline 85 86 from .dircolors import Dircolors 87 if 'libedit' in readline.__doc__: 88 readline.parse_and_bind("bind -e") 89 readline.parse_and_bind("bind '\t' rl_complete") 90 else: 91 readline.parse_and_bind('tab: complete') 92 readline.parse_and_bind('set colored-stats on') 93 readline.set_completer_delims(' \t\n`!@#$%^&*()-=+[{]}\\|;:\'",<>?') 94 readline.set_completion_display_matches_hook(self.format_completions) 95 self.original_completer = readline.get_completer() 96 readline.set_completer(self) 97 self.cache: Dict[str, Tuple[str, ...]] = {} 98 self.dircolors = Dircolors() 99 return self 100 101 def format_completions(self, substitution: str, matches: Sequence[str], longest_match_length: int) -> None: 102 import readline 103 print() 104 files, dirs = [], [] 105 for m in matches: 106 if m.endswith('/'): 107 if len(m) > 1: 108 m = m[:-1] 109 dirs.append(m) 110 else: 111 files.append(m) 112 113 ss = screen_size_function()() 114 if dirs: 115 print(styled('Directories', bold=True, fg_intense=True)) 116 print_table(dirs, ss, self.dircolors) 117 if files: 118 print(styled('Files', bold=True, fg_intense=True)) 119 print_table(files, ss, self.dircolors) 120 121 buf = readline.get_line_buffer() 122 x = readline.get_endidx() 123 buflen = wcswidth(buf) 124 print(self.prompt, buf, sep='', end='') 125 if x < buflen: 126 pos = x + self.prompt_len 127 print(f"\r\033[{pos}C", end='') 128 print(sep='', end='', flush=True) 129 130 def __call__(self, text: str, state: int) -> Optional[str]: 131 options = self.cache.get(text) 132 if options is None: 133 options = self.cache[text] = tuple(find_completions(text)) 134 if options and state < len(options): 135 return options[state] 136 137 def __exit__(self, *a: Any) -> bool: 138 import readline 139 del self.cache 140 readline.set_completer(self.original_completer) 141 readline.set_completion_display_matches_hook() 142 return True 143 144 def input(self) -> str: 145 with self: 146 return input(self.prompt) 147 148 149def develop() -> None: 150 PathCompleter().input() 151