diff --git a/kittens/tui/line_edit.py b/kittens/tui/line_edit.py new file mode 100644 index 000000000..9313731ac --- /dev/null +++ b/kittens/tui/line_edit.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2018, Kovid Goyal + +from kitty.fast_data_types import truncate_point_for_length, wcswidth +from kitty.key_encoding import RELEASE, HOME, END, BACKSPACE, DELETE, LEFT, RIGHT + + +class LineEdit: + + def __init__(self): + self.clear() + + def clear(self): + self.current_input = '' + self.cursor_pos = 0 + self.pending_bell = False + + def split_at_cursor(self, delta=0): + pos = max(0, self.cursor_pos + delta) + x = truncate_point_for_length(self.current_input, pos) if pos else 0 + before, after = self.current_input[:x], self.current_input[x:] + return before, after + + def write(self, write, prompt=''): + if self.pending_bell: + write('\a') + self.pending_bell = False + write(prompt) + write(self.current_input) + write('\r\x1b[{}C'.format(self.cursor_pos + wcswidth(prompt))) + + def on_text(self, text, in_bracketed_paste): + if self.current_input: + x = truncate_point_for_length(self.current_input, self.cursor_pos) if self.cursor_pos else 0 + self.current_input = self.current_input[:x] + text + self.current_input[x:] + else: + self.current_input = text + self.cursor_pos += wcswidth(text) + + def backspace(self, num=1): + before, after = self.split_at_cursor() + nbefore = before[:-num] + if nbefore != before: + self.current_input = nbefore + after + self.cursor_pos = wcswidth(nbefore) + return True + self.pending_bell = True + return False + + def delete(self, num=1): + before, after = self.split_at_cursor() + nafter = after[num:] + if nafter != after: + self.current_input = before + nafter + self.cursor_pos = wcswidth(before) + return True + self.pending_bell = True + return False + + def _left(self): + if not self.current_input: + self.cursor_pos = 0 + return + if self.cursor_pos: + before, after = self.split_at_cursor(-1) + self.cursor_pos = wcswidth(before) + + def _right(self): + if not self.current_input: + self.cursor_pos = 0 + return + max_pos = wcswidth(self.current_input) + if self.cursor_pos >= max_pos: + self.cursor_pos = max_pos + return + before, after = self.split_at_cursor(1) + self.cursor_pos += 1 + int(wcswidth(before) == self.cursor_pos) + + def _move_loop(self, func, num): + before = self.cursor_pos + while func() and num > 0: + num -= 1 + changed = self.cursor_pos != before + if not changed: + self.pending_bell = True + return changed + + def left(self, num=1): + return self._move_loop(self._left, num) + + def right(self, num=1): + return self._move_loop(self._right, num) + + def home(self): + if self.cursor_pos: + self.cursor_pos = 0 + return True + return False + + def end(self): + orig = self.cursor_pos + self.cursor_pos = wcswidth(self.current_input) + 1 + return self.cursor_pos != orig + + def on_key(self, key_event): + if key_event.type is RELEASE: + return False + elif key_event.key is HOME: + return self.home() + elif key_event.key is END: + return self.end() + elif key_event.key is BACKSPACE: + self.backspace() + return True + elif key_event.key is DELETE: + self.delete() + return True + elif key_event.key is LEFT: + self.left() + return True + elif key_event.key is RIGHT: + self.right() + return True + return False diff --git a/kittens/unicode_input/main.py b/kittens/unicode_input/main.py index 697d7252d..b2b74671b 100644 --- a/kittens/unicode_input/main.py +++ b/kittens/unicode_input/main.py @@ -14,9 +14,10 @@ from kitty.utils import get_editor from kitty.fast_data_types import wcswidth from kitty.key_encoding import ( DOWN, ESCAPE, F1, F2, F3, F4, F12, LEFT, RELEASE, RIGHT, SHIFT, TAB, UP, - backspace_key, enter_key + enter_key ) +from ..tui.line_edit import LineEdit from ..tui.handler import Handler from ..tui.loop import Loop from ..tui.operations import ( @@ -247,8 +248,8 @@ class UnicodeInput(Handler): def __init__(self, cached_values): self.cached_values = cached_values + self.line_edit = LineEdit() self.recent = list(self.cached_values.get('recent', DEFAULT_SET)) - self.current_input = '' self.current_char = None self.prompt_template = '{}> ' self.last_updated_code_point_at = None @@ -269,9 +270,9 @@ class UnicodeInput(Handler): codepoints = load_favorites() q = self.mode, tuple(codepoints) elif self.mode is NAME: - q = self.mode, self.current_input + q = self.mode, self.line_edit.current_input if q != self.last_updated_code_point_at: - words = self.current_input.split() + words = self.line_edit.current_input.split() words = [w for w in words if w != INDEX_CHAR] index_words = [i for i, w in enumerate(words) if i > 0 and is_index(w)] if index_words: @@ -291,10 +292,10 @@ class UnicodeInput(Handler): self.current_char = None if self.mode is HEX: try: - if self.current_input.startswith(INDEX_CHAR) and len(self.current_input) > 1: - self.current_char = chr(self.table.codepoint_at_hint(self.current_input[1:])) + if self.line_edit.current_input.startswith(INDEX_CHAR) and len(self.line_edit.current_input) > 1: + self.current_char = chr(self.table.codepoint_at_hint(self.line_edit.current_input[1:])) else: - code = int(self.current_input, 16) + code = int(self.line_edit.current_input, 16) self.current_char = chr(code) except Exception: pass @@ -304,8 +305,8 @@ class UnicodeInput(Handler): self.current_char = chr(cc) else: try: - if self.current_input: - self.current_char = chr(self.table.codepoint_at_hint(self.current_input.lstrip(INDEX_CHAR))) + if self.line_edit.current_input: + self.current_char = chr(self.table.codepoint_at_hint(self.line_edit.current_input.lstrip(INDEX_CHAR))) except Exception: pass if self.current_char is not None: @@ -366,8 +367,7 @@ class UnicodeInput(Handler): writeln(_('Enter the hex code for the character')) else: writeln(_('Enter the index for the character you want from the list below')) - self.write(self.prompt) - self.write(self.current_input) + self.line_edit.write(self.write, self.prompt) with cursor(self.write): writeln() writeln(self.choice_line) @@ -385,14 +385,34 @@ class UnicodeInput(Handler): self.draw_screen() def on_text(self, text, in_bracketed_paste): - self.current_input += text + self.line_edit.on_text(text, in_bracketed_paste) self.refresh() def on_key(self, key_event): - if key_event is backspace_key: - self.current_input = self.current_input[:-1] + if self.mode is NAME and key_event.type is not RELEASE and not key_event.mods: + if key_event.key is TAB: + if key_event.mods == SHIFT: + self.table.move_current(cols=-1), self.refresh() + elif not key_event.mods: + self.table.move_current(cols=1), self.refresh() + return + elif key_event.key is LEFT and not key_event.mods: + self.table.move_current(cols=-1), self.refresh() + return + elif key_event.key is RIGHT and not key_event.mods: + self.table.move_current(cols=1), self.refresh() + return + elif key_event.key is UP and not key_event.mods: + self.table.move_current(rows=-1), self.refresh() + return + elif key_event.key is DOWN and not key_event.mods: + self.table.move_current(rows=1), self.refresh() + return + + if self.line_edit.on_key(key_event): self.refresh() - elif key_event is enter_key: + return + if key_event is enter_key: self.quit_loop(0) elif key_event.type is RELEASE and not key_event.mods: if key_event.key is ESCAPE: @@ -407,20 +427,6 @@ class UnicodeInput(Handler): self.switch_mode(FAVORITES) elif key_event.key is F12 and self.mode is FAVORITES: self.edit_favorites() - elif self.mode is NAME: - if key_event.key is TAB: - if key_event.mods == SHIFT: - self.table.move_current(cols=-1), self.refresh() - elif not key_event.mods: - self.table.move_current(cols=1), self.refresh() - elif key_event.key is LEFT and not key_event.mods: - self.table.move_current(cols=-1), self.refresh() - elif key_event.key is RIGHT and not key_event.mods: - self.table.move_current(cols=1), self.refresh() - elif key_event.key is UP and not key_event.mods: - self.table.move_current(rows=-1), self.refresh() - elif key_event.key is DOWN and not key_event.mods: - self.table.move_current(rows=1), self.refresh() def edit_favorites(self): if not os.path.exists(favorites_path): @@ -437,7 +443,7 @@ class UnicodeInput(Handler): if mode is not self.mode: self.mode = mode self.cached_values['mode'] = mode - self.current_input = '' + self.line_edit.clear() self.current_char = None self.choice_line = '' self.refresh() diff --git a/kitty_tests/tui.py b/kitty_tests/tui.py new file mode 100644 index 000000000..7b7fee630 --- /dev/null +++ b/kitty_tests/tui.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2018, Kovid Goyal + + +from . import BaseTest + + +class TestTUI(BaseTest): + + def test_line_edit(self): + from kittens.tui.line_edit import LineEdit + le = LineEdit() + le.on_text('abcd', False) + self.ae(le.cursor_pos, 4) + for i in range(5): + self.assertTrue(le.left()) if i < 4 else self.assertFalse(le.left()) + self.ae(le.cursor_pos, max(0, 3 - i)) + self.ae(le.pending_bell, True) + le.clear() + le.on_text('abcd', False), le.home() + self.ae(le.cursor_pos, 0) + for i in range(5): + self.assertTrue(le.right()) if i < 4 else self.assertFalse(le.right()) + self.ae(le.cursor_pos, min(4, i + 1)) + self.ae(le.pending_bell, True) + le.clear() + le.on_text('abcd', False) + self.ae(le.current_input, 'abcd') + self.ae(le.cursor_pos, 4) + self.ae(le.split_at_cursor(), ('abcd', '')) + le.backspace() + self.ae(le.current_input, 'abc') + self.ae(le.cursor_pos, 3) + self.assertFalse(le.pending_bell) + le.backspace(num=2) + self.ae(le.current_input, 'a') + self.ae(le.cursor_pos, 1) + self.assertFalse(le.pending_bell) + le.backspace(num=2) + self.ae(le.current_input, '') + self.ae(le.cursor_pos, 0) + le.backspace() + self.assertTrue(le.pending_bell)