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