path_completer.py 4.8 KB

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