Implement favorites for unicode input
This commit is contained in:
parent
a36f4a4670
commit
67e0d3723a
@ -38,3 +38,6 @@ class Handler:
|
|||||||
def print(self, *args, sep=' ', end='\r\n'):
|
def print(self, *args, sep=' ', end='\r\n'):
|
||||||
data = sep.join(map(str, args)) + end
|
data = sep.join(map(str, args)) + end
|
||||||
self.write(data)
|
self.write(data)
|
||||||
|
|
||||||
|
def suspend(self):
|
||||||
|
return self._term_manager.suspend()
|
||||||
|
|||||||
@ -36,25 +36,6 @@ def log(*a, **kw):
|
|||||||
fd.flush()
|
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):
|
def write_all(fd, data):
|
||||||
if isinstance(data, str):
|
if isinstance(data, str):
|
||||||
data = data.encode('utf-8')
|
data = data.encode('utf-8')
|
||||||
@ -65,11 +46,40 @@ def write_all(fd, data):
|
|||||||
data = data[n:]
|
data = data[n:]
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
class TermManager:
|
||||||
def sanitize_term(output_fd):
|
|
||||||
write_all(output_fd, init_state())
|
def __init__(self, input_fd, output_fd):
|
||||||
yield
|
self.input_fd = input_fd
|
||||||
write_all(output_fd, reset_state())
|
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
|
LEFT, MIDDLE, RIGHT, FOURTH, FIFTH = 1, 2, 4, 8, 16
|
||||||
@ -306,11 +316,12 @@ class Loop:
|
|||||||
select = self.sel.select
|
select = self.sel.select
|
||||||
tb = None
|
tb = None
|
||||||
waiting_for_write = True
|
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.SIGWINCH, self._on_sigwinch)
|
||||||
signal.signal(signal.SIGTERM, self._on_sigterm)
|
signal.signal(signal.SIGTERM, self._on_sigterm)
|
||||||
signal.signal(signal.SIGINT, self._on_sigint)
|
signal.signal(signal.SIGINT, self._on_sigint)
|
||||||
handler.write_buf = []
|
handler.write_buf = []
|
||||||
|
handler._term_manager = term_manager
|
||||||
keep_going = True
|
keep_going = True
|
||||||
try:
|
try:
|
||||||
handler.initialize(screen_size(), self.quit, self.wakeup)
|
handler.initialize(screen_size(), self.quit, self.wakeup)
|
||||||
@ -337,13 +348,14 @@ class Loop:
|
|||||||
keep_going = False
|
keep_going = False
|
||||||
break
|
break
|
||||||
if tb is not None:
|
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
|
select = self.sel.select
|
||||||
waiting_for_write = False
|
waiting_for_write = False
|
||||||
handler = UnhandledException(tb)
|
handler = UnhandledException(tb)
|
||||||
handler.write_buf = []
|
handler.write_buf = []
|
||||||
|
handler._term_manager = term_manager
|
||||||
handler.initialize(screen_size(), self.quit, self.wakeup)
|
handler.initialize(screen_size(), self.quit, self.wakeup)
|
||||||
while True:
|
while True:
|
||||||
has_data_to_write = bool(handler.write_buf)
|
has_data_to_write = bool(handler.write_buf)
|
||||||
|
|||||||
@ -2,15 +2,19 @@
|
|||||||
# vim:fileencoding=utf-8
|
# vim:fileencoding=utf-8
|
||||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
import string
|
import string
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
|
|
||||||
from kitty.config import cached_values_for
|
from kitty.config import cached_values_for
|
||||||
|
from kitty.constants import config_dir
|
||||||
from kitty.fast_data_types import wcswidth
|
from kitty.fast_data_types import wcswidth
|
||||||
from kitty.key_encoding import (
|
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
|
backspace_key, enter_key
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -21,7 +25,12 @@ from ..tui.operations import (
|
|||||||
set_window_title, sgr, styled
|
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)
|
@lru_cache(maxsize=256)
|
||||||
@ -57,6 +66,48 @@ def codepoints_matching_search(text):
|
|||||||
return ans
|
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
|
FAINT = 242
|
||||||
DEFAULT_SET = tuple(map(
|
DEFAULT_SET = tuple(map(
|
||||||
ord,
|
ord,
|
||||||
@ -208,6 +259,9 @@ class UnicodeInput(Handler):
|
|||||||
elif self.mode is EMOTICONS:
|
elif self.mode is EMOTICONS:
|
||||||
q = self.mode, None
|
q = self.mode, None
|
||||||
codepoints = list(EMOTICONS_SET)
|
codepoints = list(EMOTICONS_SET)
|
||||||
|
elif self.mode is FAVORITES:
|
||||||
|
codepoints = load_favorites()
|
||||||
|
q = self.mode, tuple(codepoints)
|
||||||
elif self.mode is NAME:
|
elif self.mode is NAME:
|
||||||
q = self.mode, self.current_input
|
q = self.mode, self.current_input
|
||||||
if q != self.last_updated_code_point_at:
|
if q != self.last_updated_code_point_at:
|
||||||
@ -240,7 +294,7 @@ class UnicodeInput(Handler):
|
|||||||
pass
|
pass
|
||||||
if self.current_char is not None:
|
if self.current_char is not None:
|
||||||
code = ord(self.current_char)
|
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
|
self.current_char = None
|
||||||
|
|
||||||
def update_prompt(self):
|
def update_prompt(self):
|
||||||
@ -266,6 +320,7 @@ class UnicodeInput(Handler):
|
|||||||
(_('Code'), 'F1', HEX),
|
(_('Code'), 'F1', HEX),
|
||||||
(_('Name'), 'F2', NAME),
|
(_('Name'), 'F2', NAME),
|
||||||
(_('Emoji'), 'F3', EMOTICONS),
|
(_('Emoji'), 'F3', EMOTICONS),
|
||||||
|
(_('Favorites'), 'F4', FAVORITES),
|
||||||
]:
|
]:
|
||||||
entry = ' {} ({}) '.format(name, key)
|
entry = ' {} ({}) '.format(name, key)
|
||||||
if mode is self.mode:
|
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))
|
writeln(styled(_('Type {} followed by the index for the recent entries below').format('r'), fg=FAINT))
|
||||||
elif self.mode is NAME:
|
elif self.mode is NAME:
|
||||||
writeln(styled(_('Use Tab or the arrow keys to choose a character from below'), fg=FAINT))
|
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.table_at = y
|
||||||
self.write(self.table.layout(self.screen_size.rows - self.table_at, self.screen_size.cols))
|
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()
|
self.refresh()
|
||||||
elif key_event is enter_key:
|
elif key_event is enter_key:
|
||||||
self.quit_loop(0)
|
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:
|
if key_event.key is ESCAPE:
|
||||||
self.quit_loop(1)
|
self.quit_loop(1)
|
||||||
elif key_event.key is F1:
|
elif key_event.key is F1:
|
||||||
@ -328,6 +385,10 @@ class UnicodeInput(Handler):
|
|||||||
self.switch_mode(NAME)
|
self.switch_mode(NAME)
|
||||||
elif key_event.key is F3:
|
elif key_event.key is F3:
|
||||||
self.switch_mode(EMOTICONS)
|
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:
|
elif self.mode is NAME:
|
||||||
if key_event.key is TAB:
|
if key_event.key is TAB:
|
||||||
if key_event.mods == SHIFT:
|
if key_event.mods == SHIFT:
|
||||||
@ -343,6 +404,17 @@ class UnicodeInput(Handler):
|
|||||||
elif key_event.key is DOWN and not key_event.mods:
|
elif key_event.key is DOWN and not key_event.mods:
|
||||||
self.table.move_current(rows=1), self.refresh()
|
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):
|
def switch_mode(self, mode):
|
||||||
if mode is not self.mode:
|
if mode is not self.mode:
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user