diff --git a/kittens/tui/operations.py b/kittens/tui/operations.py index 7e4215e6e..ce0dbdb3d 100644 --- a/kittens/tui/operations.py +++ b/kittens/tui/operations.py @@ -62,7 +62,7 @@ UNDERLINE_STYLES = {name: i + 1 for i, name in enumerate( 'straight double curly'.split())} -def _color(color, intense=False, base=30): +def color_code(color, intense=False, base=30): if isinstance(color, str): e = str((base + 60 if intense else base) + STANDARD_COLORS[color]) elif isinstance(color, int): @@ -72,23 +72,27 @@ def _color(color, intense=False, base=30): return e -def colored(text, color, intense=False): - e = _color(color, intense) - return '\033[{}m{}\033[39m'.format(e, text) +def sgr(*parts): + return '\033[{}m'.format(';'.join(parts)) + + +def colored(text, color, intense=False, reset_to=None, reset_to_intense=False): + e = color_code(color, intense) + return '\033[{}m{}\033[{}m'.format(e, text, 39 if reset_to is None else color_code(reset_to, reset_to_intense)) def styled(text, fg=None, bg=None, fg_intense=False, bg_intense=False, italic=None, bold=None, underline=None, underline_color=None, reverse=None): start, end = [], [] if fg is not None: - start.append(_color(fg, fg_intense)) + start.append(color_code(fg, fg_intense)) end.append('39') if bg is not None: - start.append(_color(bg, bg_intense, 40)) + start.append(color_code(bg, bg_intense, 40)) end.append('49') if underline_color is not None: if isinstance(underline_color, str): underline_color = STANDARD_COLORS[underline_color] - start.append(_color(underline_color, base=50)) + start.append(color_code(underline_color, base=50)) end.append('59') if underline is not None: start.append('4:{}'.format(UNDERLINE_STYLES[underline])) diff --git a/kittens/unicode_input/main.py b/kittens/unicode_input/main.py index ec0680a39..3f4dcf4a6 100644 --- a/kittens/unicode_input/main.py +++ b/kittens/unicode_input/main.py @@ -10,13 +10,15 @@ from gettext import gettext as _ from kitty.config import cached_values_for from kitty.fast_data_types import wcswidth from kitty.key_encoding import ( - ESCAPE, F1, F2, RELEASE, backspace_key, enter_key + DOWN, ESCAPE, F1, F2, LEFT, RELEASE, RIGHT, SHIFT, TAB, UP, backspace_key, + enter_key ) from ..tui.handler import Handler from ..tui.loop import Loop from ..tui.operations import ( - clear_screen, colored, cursor, set_line_wrapping, set_window_title, styled + clear_screen, color_code, colored, cursor, set_line_wrapping, + set_window_title, sgr, styled ) HEX, NAME = 'HEX', 'NAME' @@ -25,10 +27,10 @@ HEX, NAME = 'HEX', 'NAME' @lru_cache(maxsize=256) def points_for_word(w): from .unicode_names import codepoints_for_word - return codepoints_for_word(w.lower()) + return codepoints_for_word(w) -@lru_cache(maxsize=256) +@lru_cache(maxsize=4096) def name(cp): from .unicode_names import name_for_codepoint if isinstance(cp, str): @@ -36,6 +38,25 @@ def name(cp): return (name_for_codepoint(cp) or '').capitalize() +@lru_cache(maxsize=256) +def codepoints_matching_search(text): + parts = text.lower().split() + ans = [] + if parts and parts[0]: + codepoints = points_for_word(parts[0]) + for word in parts[1:]: + pts = points_for_word(word) + if pts: + intersection = codepoints & pts + if intersection: + codepoints = intersection + continue + codepoints = {c for c in codepoints if word in name(c).lower()} + if codepoints: + ans = list(sorted(codepoints)) + return ans + + FAINT = 242 DEFAULT_SET = tuple(map( ord, @@ -65,14 +86,21 @@ class Table: self.layout_dirty = True self.last_rows = self.last_cols = -1 self.codepoints = [] + self.current_idx = 0 self.text = '' self.num_cols = self.num_rows = 0 self.mode = HEX + @property + def current_codepoint(self): + if self.codepoints: + return self.codepoints[self.current_idx] + def set_codepoints(self, codepoints, mode=HEX): self.codepoints = codepoints - self.mode = HEX + self.mode = mode self.layout_dirty = True + self.current_idx = 0 def codepoint_at_hint(self, hint): return self.codepoints[decode_hint(hint)] @@ -83,8 +111,37 @@ class Table: self.last_cols, self.last_rows = cols, rows self.layout_dirty = False - def as_parts(i, codepoint): - return encode_hint(i).ljust(idx_size), chr(codepoint), name(codepoint) + if self.mode is NAME: + def as_parts(i, codepoint): + return encode_hint(i).ljust(idx_size), chr(codepoint), name(codepoint) + + def cell(i, idx, c, desc): + is_current = i == self.current_idx + if is_current: + yield sgr(color_code('gray', base=40)) + yield colored(idx, 'green') + ' ' + yield colored(c, 'black' if is_current else 'gray', True) + ' ' + w = wcswidth(c) + if w < 2: + yield ' ' * (2 - w) + if len(desc) > space_for_desc: + desc = desc[:space_for_desc - 1] + '…' + yield colored(desc, FAINT) + extra = space_for_desc - len(desc) + if extra > 0: + yield ' ' * extra + if is_current: + yield sgr('49') + else: + def as_parts(i, codepoint): + return encode_hint(i).ljust(idx_size), chr(codepoint), '' + + def cell(i, idx, c, desc): + yield colored(idx, 'green') + ' ' + yield colored(c, 'gray', True) + ' ' + w = wcswidth(c) + if w < 2: + yield ' ' * (2 - w) num = len(self.codepoints) if num < 1: @@ -94,35 +151,40 @@ class Table: idx_size = len(encode_hint(num - 1)) parts = [as_parts(i, c) for i, c in enumerate(self.codepoints)] - sizes = [idx_size + 2 + len(p[2]) + 2 for p in parts] + if self.mode is NAME: + sizes = [idx_size + 2 + len(p[2]) + 2 for p in parts] + else: + sizes = [idx_size + 3 for p in parts] longest = max(sizes) if sizes else 0 col_width = longest + 2 col_width = min(col_width, 40) space_for_desc = col_width - 2 - idx_size - 4 - num_cols = cols // col_width + num_cols = self.num_cols = cols // col_width buf = [] - rows_left = rows a = buf.append + rows_left = self.num_rows = rows + for i, (idx, c, desc) in enumerate(parts): if i > 0 and i % num_cols == 0: rows_left -= 1 if rows_left == 0: break - a('\r\n') - a(colored(idx, 'green')), a(' '), a(colored(c, 'gray', True)), a(' ') - w = wcswidth(c) - if w < 2: - a(' ' * (2 - w)) - if len(desc) > space_for_desc: - desc = desc[:space_for_desc - 1] + '…' - a(colored(desc, FAINT)) - extra = space_for_desc - len(desc) - if extra > 0: - a(' ' * extra) + buf.append('\r\n') + buf.extend(cell(i, idx, c, desc)) a(' ') self.text = ''.join(buf) return self.text + def move_current(self, rows=0, cols=0): + if cols: + self.current_idx = (self.current_idx + len(self.codepoints) + cols) % len(self.codepoints) + self.layout_dirty = True + if rows: + amt = rows * self.num_cols + self.current_idx += amt + self.current_idx = max(0, min(self.current_idx, len(self.codepoints) - 1)) + self.layout_dirty = True + class UnicodeInput(Handler): @@ -132,14 +194,27 @@ class UnicodeInput(Handler): self.current_input = '' self.current_char = None self.prompt_template = '{}> ' + self.last_updated_code_point_at = None self.choice_line = '' self.mode = globals().get(cached_values.get('mode', 'HEX'), 'HEX') self.table = Table() - if self.mode is HEX: - self.table.set_codepoints(self.recent, self.mode) self.update_prompt() + def update_codepoints(self): + codepoints = None + if self.mode is HEX: + q = self.mode, None + codepoints = self.recent + elif self.mode is NAME: + q = self.mode, self.current_input + if q != self.last_updated_code_point_at: + codepoints = codepoints_matching_search(self.current_input) + if q != self.last_updated_code_point_at: + self.last_updated_code_point_at = q + self.table.set_codepoints(codepoints, self.mode) + def update_current_char(self): + self.update_codepoints() self.current_char = None if self.mode is HEX: try: @@ -151,17 +226,9 @@ class UnicodeInput(Handler): except Exception: pass else: - parts = self.current_input.split() - if parts and parts[0]: - codepoints = points_for_word(parts[0]) - for word in parts[1:]: - pts = points_for_word(word) - if pts: - codepoints &= pts - if codepoints: - codepoints = tuple(sorted(codepoints)) - self.current_char = chr(codepoints[0]) - # name_map = {c: name(c) for c in codepoints} + cc = self.table.current_codepoint + if cc: + self.current_char = chr(cc) if self.current_char is not None: code = ord(self.current_char) if code <= 32 or code == 127 or 128 <= code <= 159 or 0xd800 <= code <= 0xdbff or 0xDC00 <= code <= 0xDFFF: @@ -210,18 +277,21 @@ class UnicodeInput(Handler): self.print(text) y += 1 - if self.mode is HEX: + if self.mode is NAME: writeln(_('Enter the hex code for the character')) - else: + elif self.mode is HEX: writeln(_('Enter words from the name of 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) with cursor(self.write): writeln() - if self.choice_line: - writeln(self.choice_line) + writeln(self.choice_line) if self.mode is HEX: - writeln(styled('Use r followed by the index for the recent entries below', fg=FAINT)) + writeln(styled(_('Type {} followed by the index for the recent entries below').format('r'), fg=FAINT)) + elif self.mode is NAME: + writeln(styled(_('Use Tab or the arrow keys to choose a character from below'), fg=FAINT)) self.table_at = y self.write(self.table.layout(self.screen_size.rows - self.table_at, self.screen_size.cols)) @@ -246,6 +316,20 @@ class UnicodeInput(Handler): self.switch_mode(HEX) elif key_event.key is F2: self.switch_mode(NAME) + 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 switch_mode(self, mode): if mode is not self.mode: diff --git a/kittens/unicode_input/unicode_names.c b/kittens/unicode_input/unicode_names.c index 684cfbf44..4debf073f 100644 --- a/kittens/unicode_input/unicode_names.c +++ b/kittens/unicode_input/unicode_names.c @@ -27,7 +27,7 @@ codepoints_for_word(const char *word, size_t len) { for (unsigned short i = 1; i <= words[0]; i++) { unsigned short word_idx = words[i]; const char *w = idx_to_word[word_idx]; - if(strncmp(word, w, len) == 0) { + if (strncmp(word, w, len) == 0 && strlen(w) == len) { const char_type* codepoints = codepoints_for_word_idx[word_idx]; for (char_type i = 1; i <= codepoints[0]; i++) { PyObject *t = PyLong_FromUnsignedLong(codepoints[i]); if (t == NULL) { Py_DECREF(ans); return NULL; }