line_edit.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. #!/usr/bin/env python
  2. # License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
  3. from typing import Callable, Tuple
  4. from kitty.fast_data_types import truncate_point_for_length, wcswidth
  5. from kitty.key_encoding import EventType, KeyEvent
  6. from .operations import RESTORE_CURSOR, SAVE_CURSOR, move_cursor_by, set_cursor_shape
  7. class LineEdit:
  8. def __init__(self, is_password: bool = False) -> None:
  9. self.clear()
  10. self.is_password = is_password
  11. def clear(self) -> None:
  12. self.current_input = ''
  13. self.cursor_pos = 0
  14. self.pending_bell = False
  15. def split_at_cursor(self, delta: int = 0) -> Tuple[str, str]:
  16. pos = max(0, self.cursor_pos + delta)
  17. x = truncate_point_for_length(self.current_input, pos) if pos else 0
  18. before, after = self.current_input[:x], self.current_input[x:]
  19. return before, after
  20. def write(self, write: Callable[[str], None], prompt: str = '', screen_cols: int = 0) -> None:
  21. if self.pending_bell:
  22. write('\a')
  23. self.pending_bell = False
  24. ci = self.current_input
  25. if self.is_password:
  26. ci = '*' * wcswidth(ci)
  27. text = prompt + ci
  28. cursor_pos = self.cursor_pos + wcswidth(prompt)
  29. if screen_cols:
  30. write(SAVE_CURSOR + text + RESTORE_CURSOR)
  31. used_lines, last_line_cursor_pos = divmod(cursor_pos, screen_cols)
  32. if used_lines == 0:
  33. if last_line_cursor_pos:
  34. write(move_cursor_by(last_line_cursor_pos, 'right'))
  35. else:
  36. if used_lines:
  37. write(move_cursor_by(used_lines, 'down'))
  38. if last_line_cursor_pos:
  39. write(move_cursor_by(last_line_cursor_pos, 'right'))
  40. else:
  41. write(text)
  42. write('\r')
  43. if cursor_pos:
  44. write(move_cursor_by(cursor_pos, 'right'))
  45. write(set_cursor_shape('beam'))
  46. def add_text(self, text: str) -> None:
  47. if self.current_input:
  48. x = truncate_point_for_length(self.current_input, self.cursor_pos) if self.cursor_pos else 0
  49. self.current_input = self.current_input[:x] + text + self.current_input[x:]
  50. else:
  51. self.current_input = text
  52. self.cursor_pos += wcswidth(text)
  53. def on_text(self, text: str, in_bracketed_paste: bool) -> None:
  54. self.add_text(text)
  55. def backspace(self, num: int = 1) -> bool:
  56. before, after = self.split_at_cursor()
  57. nbefore = before[:-num]
  58. if nbefore != before:
  59. self.current_input = nbefore + after
  60. self.cursor_pos = wcswidth(nbefore)
  61. return True
  62. self.pending_bell = True
  63. return False
  64. def delete(self, num: int = 1) -> bool:
  65. before, after = self.split_at_cursor()
  66. nafter = after[num:]
  67. if nafter != after:
  68. self.current_input = before + nafter
  69. self.cursor_pos = wcswidth(before)
  70. return True
  71. self.pending_bell = True
  72. return False
  73. def _left(self) -> None:
  74. if not self.current_input:
  75. self.cursor_pos = 0
  76. return
  77. if self.cursor_pos:
  78. before, after = self.split_at_cursor(-1)
  79. self.cursor_pos = wcswidth(before)
  80. def _right(self) -> None:
  81. if not self.current_input:
  82. self.cursor_pos = 0
  83. return
  84. max_pos = wcswidth(self.current_input)
  85. if self.cursor_pos >= max_pos:
  86. self.cursor_pos = max_pos
  87. return
  88. before, after = self.split_at_cursor(1)
  89. self.cursor_pos += 1 + int(wcswidth(before) == self.cursor_pos)
  90. def _move_loop(self, func: Callable[[], None], num: int) -> bool:
  91. before = self.cursor_pos
  92. changed = False
  93. while num > 0:
  94. func()
  95. changed = self.cursor_pos != before
  96. if not changed:
  97. break
  98. num -= 1
  99. if not changed:
  100. self.pending_bell = True
  101. return changed
  102. def left(self, num: int = 1) -> bool:
  103. return self._move_loop(self._left, num)
  104. def right(self, num: int = 1) -> bool:
  105. return self._move_loop(self._right, num)
  106. def home(self) -> bool:
  107. if self.cursor_pos:
  108. self.cursor_pos = 0
  109. return True
  110. return False
  111. def end(self) -> bool:
  112. orig = self.cursor_pos
  113. self.cursor_pos = wcswidth(self.current_input)
  114. return self.cursor_pos != orig
  115. def on_key(self, key_event: KeyEvent) -> bool:
  116. if key_event.type is EventType.RELEASE:
  117. return False
  118. if key_event.matches('home') or key_event.matches('ctrl+a'):
  119. return self.home()
  120. if key_event.matches('end') or key_event.matches('ctrl+e'):
  121. return self.end()
  122. if key_event.matches('backspace'):
  123. self.backspace()
  124. return True
  125. if key_event.matches('delete') or key_event.matches('ctrl+d'):
  126. self.delete()
  127. return True
  128. if key_event.matches('left') or key_event.matches('ctrl+b'):
  129. self.left()
  130. return True
  131. if key_event.matches('right') or key_event.matches('ctrl+f'):
  132. self.right()
  133. return True
  134. return False