diff --git a/kittens/tui/handler.py b/kittens/tui/handler.py index 083ebc3f3..e5d9f3651 100644 --- a/kittens/tui/handler.py +++ b/kittens/tui/handler.py @@ -38,3 +38,6 @@ class Handler: def print(self, *args, sep=' ', end='\r\n'): data = sep.join(map(str, args)) + end self.write(data) + + def suspend(self): + return self._term_manager.suspend() diff --git a/kittens/tui/loop.py b/kittens/tui/loop.py index 434e476c3..9cc08ef2d 100644 --- a/kittens/tui/loop.py +++ b/kittens/tui/loop.py @@ -36,25 +36,6 @@ def log(*a, **kw): fd.flush() -@contextmanager -def non_block(fd): - oldfl = fcntl.fcntl(fd, fcntl.F_GETFL) - fcntl.fcntl(fd, fcntl.F_SETFL, oldfl | os.O_NONBLOCK) - yield - fcntl.fcntl(fd, fcntl.F_SETFL, oldfl) - - -@contextmanager -def raw_terminal(fd): - isatty = os.isatty(fd) - if isatty: - old = termios.tcgetattr(fd) - tty.setraw(fd) - yield - if isatty: - termios.tcsetattr(fd, termios.TCSADRAIN, old) - - def write_all(fd, data): if isinstance(data, str): data = data.encode('utf-8') @@ -65,11 +46,40 @@ def write_all(fd, data): data = data[n:] -@contextmanager -def sanitize_term(output_fd): - write_all(output_fd, init_state()) - yield - write_all(output_fd, reset_state()) +class TermManager: + + def __init__(self, input_fd, output_fd): + self.input_fd = input_fd + self.output_fd = output_fd + self.original_fl = fcntl.fcntl(self.input_fd, fcntl.F_GETFL) + self.isatty = os.isatty(self.input_fd) + if self.isatty: + self.original_termios = termios.tcgetattr(self.input_fd) + + def set_state_for_loop(self): + write_all(self.output_fd, init_state()) + fcntl.fcntl(self.input_fd, fcntl.F_SETFL, self.original_fl | os.O_NONBLOCK) + if self.isatty: + tty.setraw(self.input_fd) + + def reset_state_to_original(self): + if self.isatty: + termios.tcsetattr(self.input_fd, termios.TCSADRAIN, self.original_termios) + fcntl.fcntl(self.input_fd, fcntl.F_SETFL, self.original_fl) + write_all(self.output_fd, reset_state()) + + @contextmanager + def suspend(self): + self.reset_state_to_original() + yield self + self.set_state_for_loop() + + def __enter__(self): + self.set_state_for_loop() + return self + + def __exit__(self, *a): + self.reset_state_to_original() LEFT, MIDDLE, RIGHT, FOURTH, FIFTH = 1, 2, 4, 8, 16 @@ -306,11 +316,12 @@ class Loop: select = self.sel.select tb = None waiting_for_write = True - with closing(self.sel), sanitize_term(self.output_fd), non_block(self.input_fd), non_block(self.output_fd), raw_terminal(self.input_fd): + with closing(self.sel), TermManager(self.input_fd, self.output_fd) as term_manager: signal.signal(signal.SIGWINCH, self._on_sigwinch) signal.signal(signal.SIGTERM, self._on_sigterm) signal.signal(signal.SIGINT, self._on_sigint) handler.write_buf = [] + handler._term_manager = term_manager keep_going = True try: handler.initialize(screen_size(), self.quit, self.wakeup) @@ -337,13 +348,14 @@ class Loop: keep_going = False break if tb is not None: - self._report_error_loop(tb) + self._report_error_loop(tb, term_manager) - def _report_error_loop(self, tb): + def _report_error_loop(self, tb, term_manager): select = self.sel.select waiting_for_write = False handler = UnhandledException(tb) handler.write_buf = [] + handler._term_manager = term_manager handler.initialize(screen_size(), self.quit, self.wakeup) while True: has_data_to_write = bool(handler.write_buf) diff --git a/kittens/unicode_input/main.py b/kittens/unicode_input/main.py index b3574bf91..397af4b0f 100644 --- a/kittens/unicode_input/main.py +++ b/kittens/unicode_input/main.py @@ -2,15 +2,19 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2018, Kovid Goyal +import os +import shlex import string +import subprocess import sys from functools import lru_cache from gettext import gettext as _ from kitty.config import cached_values_for +from kitty.constants import config_dir from kitty.fast_data_types import wcswidth from kitty.key_encoding import ( - DOWN, ESCAPE, F1, F2, F3, LEFT, RELEASE, RIGHT, SHIFT, TAB, UP, + DOWN, ESCAPE, F1, F2, F3, F4, F12, LEFT, RELEASE, RIGHT, SHIFT, TAB, UP, backspace_key, enter_key ) @@ -21,7 +25,12 @@ from ..tui.operations import ( set_window_title, sgr, styled ) -HEX, NAME, EMOTICONS = 'HEX', 'NAME', 'EMOTICONS' +HEX, NAME, EMOTICONS, FAVORITES = 'HEX', 'NAME', 'EMOTICONS', 'FAVORITES' +favorites_path = os.path.join(config_dir, 'unicode-input-favorites.conf') + + +def codepoint_ok(code): + return not (code <= 32 or code == 127 or 128 <= code <= 159 or 0xd800 <= code <= 0xdbff or 0xDC00 <= code <= 0xDFFF) @lru_cache(maxsize=256) @@ -57,6 +66,48 @@ def codepoints_matching_search(text): return ans +def parse_favorites(raw): + for line in raw.splitlines(): + line = line.strip() + if line.startswith('#') or not line: + continue + idx = line.find('#') + if idx > -1: + line = line[:idx] + code_text = line.partition(' ')[0] + try: + code = int(code_text, 16) + except Exception: + pass + else: + if codepoint_ok(code): + yield code + + +def serialize_favorites(favorites): + ans = '''\ +# Favorite characters for unicode input +# Enter the hex code for each favorite character on a new line. Blank lines are +# ignored and anything after a # is considered a comment. + +'''.splitlines() + for cp in favorites: + ans.append('{:x} # {} {}'.format(cp, chr(cp), name(cp))) + return '\n'.join(ans) + + +def load_favorites(refresh=False): + ans = getattr(load_favorites, 'ans', None) + if ans is None or refresh: + try: + with open(favorites_path, 'rb') as f: + raw = f.read().decode('utf-8') + ans = load_favorites.ans = list(parse_favorites(raw)) or list(DEFAULT_SET) + except FileNotFoundError: + ans = load_favorites.ans = list(DEFAULT_SET) + return ans + + FAINT = 242 DEFAULT_SET = tuple(map( ord, @@ -208,6 +259,9 @@ class UnicodeInput(Handler): elif self.mode is EMOTICONS: q = self.mode, None codepoints = list(EMOTICONS_SET) + elif self.mode is FAVORITES: + codepoints = load_favorites() + q = self.mode, tuple(codepoints) elif self.mode is NAME: q = self.mode, self.current_input if q != self.last_updated_code_point_at: @@ -240,7 +294,7 @@ class UnicodeInput(Handler): pass 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: + if not codepoint_ok(code): self.current_char = None def update_prompt(self): @@ -266,6 +320,7 @@ class UnicodeInput(Handler): (_('Code'), 'F1', HEX), (_('Name'), 'F2', NAME), (_('Emoji'), 'F3', EMOTICONS), + (_('Favorites'), 'F4', FAVORITES), ]: entry = ' {} ({}) '.format(name, key) if mode is self.mode: @@ -302,6 +357,8 @@ class UnicodeInput(Handler): 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)) + elif self.mode is FAVORITES: + writeln(styled(_('Press F12 to edit the list of favorites'), fg=FAINT)) self.table_at = y self.write(self.table.layout(self.screen_size.rows - self.table_at, self.screen_size.cols)) @@ -319,7 +376,7 @@ class UnicodeInput(Handler): self.refresh() elif key_event is enter_key: self.quit_loop(0) - elif key_event.type is RELEASE: + elif key_event.type is RELEASE and not key_event.mods: if key_event.key is ESCAPE: self.quit_loop(1) elif key_event.key is F1: @@ -328,6 +385,10 @@ class UnicodeInput(Handler): self.switch_mode(NAME) elif key_event.key is F3: self.switch_mode(EMOTICONS) + elif key_event.key is F4: + 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: @@ -343,6 +404,17 @@ class UnicodeInput(Handler): 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): + with open(favorites_path, 'wb') as f: + f.write(serialize_favorites(load_favorites()).encode('utf-8')) + editor = shlex.split(os.environ.get('EDITOR', 'vim')) + with self.suspend(): + p = subprocess.Popen(editor + [favorites_path]) + if p.wait() == 0: + load_favorites(refresh=True) + self.refresh() + def switch_mode(self, mode): if mode is not self.mode: self.mode = mode