path_completer.py 4.8 KB

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