Start work on porting unicode input kitten to Go

This commit is contained in:
Kovid Goyal 2023-02-09 19:07:55 +05:30
parent a2e4efbb14
commit 53e33a80ba
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
20 changed files with 39037 additions and 76658 deletions

1
.gitattributes vendored
View File

@ -17,6 +17,7 @@ glfw/*.c linguist-vendored=true
glfw/*.h linguist-vendored=true
kittens/unicode_input/names.h linguist-generated=true
tools/wcswidth/std.go linguist-generated=true
tools/unicode_names/names.txt linguist-generated=true
*.py text diff=python
*.m text diff=objc

1
.gitignore vendored
View File

@ -12,6 +12,7 @@
/kitty.app/
/glad/out/
/kitty/launcher/kitt*
/tools/unicode_names/data_generated.bin
/*.dSYM/
__pycache__/
/glfw/wayland-*-client-protocol.[ch]

View File

@ -4,11 +4,13 @@
import io
import json
import os
import struct
import subprocess
import sys
import zlib
from contextlib import contextmanager, suppress
from functools import lru_cache
from typing import Any, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Union
from typing import Any, BinaryIO, Dict, Iterator, List, Optional, Sequence, Set, TextIO, Tuple, Union
import kitty.constants as kc
from kittens.tui.operations import Mode
@ -31,6 +33,19 @@
changed: List[str] = []
def newer(dest: str, *sources: str) -> bool:
try:
dtime = os.path.getmtime(dest)
except OSError:
return True
for s in sources:
with suppress(FileNotFoundError):
if os.path.getmtime(s) >= dtime:
return True
return False
# Utils {{{
def serialize_go_dict(x: Union[Dict[str, int], Dict[int, str], Dict[int, int], Dict[str, str]]) -> str:
@ -583,6 +598,25 @@ def generate_textual_mimetypes() -> str:
return '\n'.join(ans)
def generate_unicode_names(src: TextIO, dest: BinaryIO) -> None:
num_names, num_of_words = map(int, next(src).split())
gob = io.BytesIO()
gob.write(struct.pack('<II', num_names, num_of_words))
for line in src:
line = line.strip()
if line:
a, aliases = line.partition('\t')[::2]
cp, name = a.partition(' ')[::2]
ename = name.encode()
record = struct.pack('<IH', int(cp), len(ename)) + ename
if aliases:
record += aliases.encode()
gob.write(struct.pack('<H', len(record)) + record)
data = gob.getvalue()
dest.write(struct.pack('<I', len(data)))
dest.write(zlib.compress(data, zlib.Z_BEST_COMPRESSION))
def main() -> None:
with replace_if_needed('constants_generated.go') as f:
f.write(generate_constants())
@ -596,6 +630,9 @@ def main() -> None:
f.write(generate_mimetypes())
with replace_if_needed('tools/utils/mimetypes_textual_generated.go') as f:
f.write(generate_textual_mimetypes())
if newer('tools/unicode_names/data_generated.bin', 'tools/unicode_names/names.txt'):
with open('tools/unicode_names/data_generated.bin', 'wb') as dest, open('tools/unicode_names/names.txt') as src:
generate_unicode_names(src, dest)
update_completion()
update_at_commands()

View File

@ -444,104 +444,22 @@ def gen_ucd() -> None:
def gen_names() -> None:
with create_header('kittens/unicode_input/names.h') as p:
mark_to_cp = list(sorted(name_map))
cp_to_mark = {cp: m for m, cp in enumerate(mark_to_cp)}
# Mapping of mark to codepoint name
p(f'static const char* name_map[{len(mark_to_cp)}] = {{' ' // {{{')
for cp in mark_to_cp:
w = name_map[cp].replace('"', '\\"')
p(f'\t"{w}",')
p("}; // }}}\n")
# Mapping of mark to codepoint
p(f'static const char_type mark_to_cp[{len(mark_to_cp)}] = {{' ' // {{{')
p(', '.join(map(str, mark_to_cp)))
p('}; // }}}\n')
# Function to get mark number for codepoint
p('static char_type mark_for_codepoint(char_type c) {')
codepoint_to_mark_map(p, mark_to_cp)
p('}\n')
p('static inline const char* name_for_codepoint(char_type cp) {')
p('\tchar_type m = mark_for_codepoint(cp); if (m == 0) return NULL;')
p('\treturn name_map[m];')
p('}\n')
# Array of all words
word_map = tuple(sorted(word_search_map))
word_rmap = {w: i for i, w in enumerate(word_map)}
p(f'static const char* all_words_map[{len(word_map)}] = {{' ' // {{{')
cwords = (w.replace('"', '\\"') for w in word_map)
p(', '.join(f'"{w}"' for w in cwords))
p('}; // }}}\n')
# Array of sets of marks for each word
word_to_marks = {word_rmap[w]: frozenset(map(cp_to_mark.__getitem__, cps)) for w, cps in word_search_map.items()}
all_mark_groups = frozenset(word_to_marks.values())
array = [0]
mg_to_offset = {}
for mg in all_mark_groups:
mg_to_offset[mg] = len(array)
array.append(len(mg))
array.extend(sorted(mg))
p(f'static const char_type mark_groups[{len(array)}] = {{' ' // {{{')
p(', '.join(map(str, array)))
p('}; // }}}\n')
offsets_array = []
for wi, w in enumerate(word_map):
mg = word_to_marks[wi]
offsets_array.append(mg_to_offset[mg])
p(f'static const char_type mark_to_offset[{len(offsets_array)}] = {{' ' // {{{')
p(', '.join(map(str, offsets_array)))
p('}; // }}}\n')
# The trie
p('typedef struct { uint32_t children_offset; uint32_t match_offset; } word_trie;\n')
all_trie_nodes: List['TrieNode'] = []
class TrieNode:
def __init__(self) -> None:
self.match_offset = 0
self.children_offset = 0
self.children: Dict[int, int] = {}
def add_letter(self, letter: int) -> int:
if letter not in self.children:
self.children[letter] = len(all_trie_nodes)
all_trie_nodes.append(TrieNode())
return self.children[letter]
def __str__(self) -> str:
return f'{{ .children_offset={self.children_offset}, .match_offset={self.match_offset} }}'
root = TrieNode()
all_trie_nodes.append(root)
def add_word(word_idx: int, word: str) -> None:
parent = root
for letter in map(ord, word):
idx = parent.add_letter(letter)
parent = all_trie_nodes[idx]
parent.match_offset = offsets_array[word_idx]
for i, word in enumerate(word_map):
add_word(i, word)
children_array = [0]
for node in all_trie_nodes:
if node.children:
node.children_offset = len(children_array)
children_array.append(len(node.children))
for letter, child_offset in node.children.items():
children_array.append((child_offset << 8) | (letter & 0xff))
p(f'static const word_trie all_trie_nodes[{len(all_trie_nodes)}] = {{' ' // {{{')
p(',\n'.join(map(str, all_trie_nodes)))
p('\n}; // }}}\n')
p(f'static const uint32_t children_array[{len(children_array)}] = {{' ' // {{{')
p(', '.join(map(str, children_array)))
p('}; // }}}\n')
aliases_map: Dict[int, Set[str]] = {}
for word, codepoints in word_search_map.items():
for cp in codepoints:
aliases_map.setdefault(cp, set()).add(word)
if len(name_map) > 0xffff:
raise Exception('Too many named codepoints')
with open('tools/unicode_names/names.txt', 'w') as f:
print(len(name_map), len(word_search_map), file=f)
for cp in sorted(name_map):
name = name_map[cp]
words = name.lower().split()
aliases = aliases_map.get(cp, set()) - set(words)
end = '\n'
if aliases:
end = '\t' + ' '.join(sorted(aliases)) + end
print(cp, *words, end=end, file=f)
def gen_wcwidth() -> None:

View File

@ -589,11 +589,10 @@ def handle_result(args: List[str], current_char: str, target_window_id: int, bos
if __name__ == '__main__':
ans = main(sys.argv)
if ans:
print(ans)
raise SystemExit('This should be run as kitten unicode_input')
elif __name__ == '__doc__':
cd = sys.cli_docs # type: ignore
cd['usage'] = usage
cd['options'] = OPTIONS
cd['help_text'] = help_text
cd['short_desc'] = 'Browse and select unicode characters by name'

File diff suppressed because one or more lines are too long

View File

@ -1,114 +0,0 @@
/*
* unicode_names.c
* Copyright (C) 2018 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "names.h"
static PyObject*
all_words(PYNOARG) {
PyObject *ans = PyTuple_New(arraysz(all_words_map));
if (!ans) return NULL;
for (size_t i = 0; i < arraysz(all_words_map); i++) {
PyObject *w = PyUnicode_FromString(all_words_map[i]);
if (w == NULL) { Py_DECREF(ans); return NULL; }
PyTuple_SET_ITEM(ans, i, w);
}
return ans;
}
static void
add_matches(const word_trie *wt, PyObject *ans) {
size_t num = mark_groups[wt->match_offset];
for (size_t i = wt->match_offset + 1; i < wt->match_offset + 1 + num; i++) {
PyObject *t = PyLong_FromUnsignedLong(mark_to_cp[mark_groups[i]]);
if (!t) return;
int ret = PySet_Add(ans, t);
Py_DECREF(t);
if (ret != 0) return;
}
}
static void
process_trie_node(const word_trie *wt, PyObject *ans) {
if (wt->match_offset) { add_matches(wt, ans); if (PyErr_Occurred()) return; }
size_t num_children = children_array[wt->children_offset];
if (!num_children) return;
for (size_t c = wt->children_offset + 1; c < wt->children_offset + 1 + num_children; c++) {
uint32_t x = children_array[c];
process_trie_node(&all_trie_nodes[x >> 8], ans);
if (PyErr_Occurred()) return;
}
}
static PyObject*
codepoints_for_word(const char *word, size_t len) {
const word_trie *wt = all_trie_nodes;
for (size_t i = 0; i < len; i++) {
unsigned char ch = word[i];
size_t num_children = children_array[wt->children_offset];
if (!num_children) return PyFrozenSet_New(NULL);
bool found = false;
for (size_t c = wt->children_offset + 1; c < wt->children_offset + 1 + num_children; c++) {
uint32_t x = children_array[c];
if ((x & 0xff) == ch) {
found = true;
wt = &all_trie_nodes[x >> 8];
break;
}
}
if (!found) return PyFrozenSet_New(NULL);
}
PyObject *ans = PyFrozenSet_New(NULL);
if (!ans) return NULL;
process_trie_node(wt, ans);
if (PyErr_Occurred()) return NULL;
return ans;
}
static PyObject*
cfw(PyObject *self UNUSED, PyObject *args) {
const char *word;
if (!PyArg_ParseTuple(args, "s", &word)) return NULL;
return codepoints_for_word(word, strlen(word));
}
static PyObject*
nfc(PyObject *self UNUSED, PyObject *args) {
unsigned int cp;
if (!PyArg_ParseTuple(args, "I", &cp)) return NULL;
const char *n = name_for_codepoint(cp);
if (n == NULL) Py_RETURN_NONE;
return PyUnicode_FromString(n);
}
static PyMethodDef module_methods[] = {
{"all_words", (PyCFunction)all_words, METH_NOARGS, ""},
{"codepoints_for_word", (PyCFunction)cfw, METH_VARARGS, ""},
{"name_for_codepoint", (PyCFunction)nfc, METH_VARARGS, ""},
{NULL, NULL, 0, NULL} /* Sentinel */
};
static int
exec_module(PyObject *m UNUSED) {
return 0;
}
IGNORE_PEDANTIC_WARNINGS
static PyModuleDef_Slot slots[] = { {Py_mod_exec, (void*)exec_module}, {0, NULL} };
END_IGNORE_PEDANTIC_WARNINGS
static struct PyModuleDef module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "unicode_names", /* name of module */
.m_doc = NULL,
.m_slots = slots,
.m_methods = module_methods
};
EXPORTED PyMODINIT_FUNC
PyInit_unicode_names(void) {
return PyModuleDef_Init(&module);
}

View File

@ -1,12 +0,0 @@
from typing import FrozenSet, Optional, Tuple
def all_words() -> Tuple[str, ...]:
pass
def codepoints_for_word(word: str) -> FrozenSet[int]:
pass
def name_for_codepoint(cp: int) -> Optional[str]:
pass

View File

@ -49,6 +49,7 @@
handled_signals,
is_macos,
is_wayland,
kitten_exe,
kitty_exe,
logo_png_file,
supports_primary_selection,
@ -110,6 +111,7 @@
toggle_fullscreen,
toggle_maximized,
toggle_secure_input,
wrapped_kitten_names,
)
from .key_encoding import get_name_to_functional_number_map
from .keys import get_shortcut, shortcut_matches
@ -1730,17 +1732,23 @@ def run_kitten_with_metadata(
if sel:
x = sel
final_args.append(x)
env = {
'KITTY_COMMON_OPTS': json.dumps(copts),
'KITTY_CHILD_PID': str(w.child.pid),
'OVERLAID_WINDOW_LINES': str(w.screen.lines),
'OVERLAID_WINDOW_COLS': str(w.screen.columns),
}
if kitten in wrapped_kitten_names():
cmd = [kitten_exe(), kitten]
env['KITTEN_RUNNING_AS_UI'] = '1'
else:
cmd = [kitty_exe(), '+runpy', 'from kittens.runner import main; main()']
env['PYTHONWARNINGS'] = 'ignore'
overlay_window = tab.new_special_window(
SpecialWindow(
[kitty_exe(), '+runpy', 'from kittens.runner import main; main()'] + final_args,
cmd + final_args,
stdin=data,
env={
'KITTY_COMMON_OPTS': json.dumps(copts),
'KITTY_CHILD_PID': str(w.child.pid),
'PYTHONWARNINGS': 'ignore',
'OVERLAID_WINDOW_LINES': str(w.screen.lines),
'OVERLAID_WINDOW_COLS': str(w.screen.columns),
},
env=env,
cwd=w.cwd_of_child,
overlay_for=w.id,
overlay_behind=end_kitten.has_ready_notification,

View File

@ -1,22 +0,0 @@
#!/usr/bin/env python3
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
from . import BaseTest
class TestUnicodeInput(BaseTest):
def test_word_trie(self):
from kittens.unicode_input.unicode_names import codepoints_for_word
def matches(a, *words):
ans = codepoints_for_word(a)
for w in words:
ans &= codepoints_for_word(w)
return set(ans)
self.ae(matches('horiz', 'ell'), {0x2026, 0x22ef, 0x2b2c, 0x2b2d, 0xfe19})
self.ae(matches('horizontal', 'ell'), {0x2026, 0x22ef, 0x2b2c, 0x2b2d, 0xfe19})
self.assertFalse(matches('sfgsfgsfgfgsdg'))
self.assertIn(0x1f41d, matches('bee'))

View File

@ -24,7 +24,7 @@ exec_kitty() {
is_wrapped_kitten() {
wrapped_kittens="clipboard icat"
wrapped_kittens="clipboard icat unicode_input"
[ -n "$1" ] && {
case " $wrapped_kittens " in
*" $1 "*) printf "%s" "$1" ;;

View File

@ -10,6 +10,7 @@ import (
"kitty/tools/cmd/clipboard"
"kitty/tools/cmd/edit_in_kitty"
"kitty/tools/cmd/icat"
"kitty/tools/cmd/unicode_input"
"kitty/tools/cmd/update_self"
"kitty/tools/tui"
)
@ -29,6 +30,8 @@ func KittyToolEntryPoints(root *cli.Command) {
clipboard.EntryPoint(root)
// icat
icat.EntryPoint(root)
// unicode_input
unicode_input.EntryPoint(root)
// __hold_till_enter__
root.AddSubCommand(&cli.Command{
Name: "__hold_till_enter__",

View File

@ -0,0 +1,470 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package unicode_input
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"unicode"
"kitty/tools/cli"
"kitty/tools/tui"
"kitty/tools/tui/loop"
"kitty/tools/tui/readline"
"kitty/tools/unicode_names"
"kitty/tools/utils"
"kitty/tools/utils/style"
"kitty/tools/wcswidth"
"golang.org/x/exp/slices"
)
var _ = fmt.Print
const INDEX_CHAR string = "."
const INDEX_BASE = 36
const InvalidChar rune = unicode.MaxRune + 1
const default_set_of_symbols string = `
«» 😀😛😇😈😉😍😎😮👍👎 §©® ·°±×÷¼½½¾
µ¢£¿¡¨´¸ˆ˜ ÀÁÂÃÄÅÆÇÈÉÊË ÌÍÎÏÐÑÒÓÔÕÖØ ŒŠÙÚÛÜÝŸÞßàá âãäåæçèéêëìí
îïðñòóôõöøœš ùúûüýÿþªºαΩ
`
var DEFAULT_SET []rune
var EMOTICONS_SET []rune
const DEFAULT_MODE string = "HEX"
func build_sets() {
DEFAULT_SET = make([]rune, 0, len(default_set_of_symbols))
for _, ch := range default_set_of_symbols {
if !unicode.IsSpace(ch) {
DEFAULT_SET = append(DEFAULT_SET, ch)
}
}
EMOTICONS_SET = make([]rune, 0, 0x1f64f-0x1f600+1)
for i := 0x1f600; i <= 0x1f64f; i++ {
DEFAULT_SET = append(DEFAULT_SET, rune(i))
}
}
func codepoint_ok(code rune) bool {
return !(code <= 32 || code == 127 || (128 <= code && code <= 159) || (0xd800 <= code && code <= 0xdbff) || (0xDC00 <= code && code <= 0xDFFF) || code > unicode.MaxRune)
}
func parse_favorites(raw string) (ans []rune) {
ans = make([]rune, 0, 128)
for _, line := range utils.Splitlines(raw) {
line = strings.TrimSpace(line)
if len(line) == 0 || strings.HasPrefix(line, "#") {
continue
}
idx := strings.Index(line, "#")
if idx > -1 {
line = line[:idx]
}
code_text, _, _ := strings.Cut(line, " ")
code, err := strconv.ParseUint(code_text, 16, 32)
if err == nil && codepoint_ok(rune(code)) {
ans = append(ans, rune(code))
}
}
return
}
func serialize_favorites(favs []rune) string {
b := strings.Builder{}
b.Grow(8192)
b.WriteString(`# 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.
`)
for _, ch := range favs {
b.WriteString(fmt.Sprintf("%x # %s %s", ch, string(ch), unicode_names.NameForCodePoint(ch)))
}
return b.String()
}
var loaded_favorites []rune
func favorites_path() string {
return filepath.Join(utils.ConfigDir(), "unicode-input-favorites.conf")
}
func load_favorites(refresh bool) []rune {
if refresh || loaded_favorites == nil {
raw, err := os.ReadFile(favorites_path())
if err == nil {
loaded_favorites = parse_favorites(utils.UnsafeBytesToString(raw))
} else {
loaded_favorites = parse_favorites("")
}
}
return loaded_favorites
}
type CachedData struct {
Recent []rune `json:"recent,omitempty"`
Mode string `json:"mode,omitempty"`
}
var cached_data *CachedData
type Mode int
const (
HEX Mode = iota
NAME
EMOTICONS
FAVORITES
)
type ModeData struct {
mode Mode
key string
title string
}
var all_modes [4]ModeData
type checkpoints_key struct {
mode Mode
text string
codepoints []rune
}
func (self *checkpoints_key) clear() {
*self = checkpoints_key{}
}
func (self *checkpoints_key) is_equal(other checkpoints_key) bool {
return self.mode == other.mode && self.text == other.text && slices.Equal(self.codepoints, other.codepoints)
}
type handler struct {
mode Mode
recent []rune
current_char rune
err error
lp *loop.Loop
ctx style.Context
rl *readline.Readline
choice_line string
emoji_variation string
checkpoints_key checkpoints_key
table table
current_tab_formatter, tab_bar_formatter, chosen_formatter, chosen_name_formatter, dim_formatter func(...any) string
}
func (self *handler) initialize() {
self.lp.AllowLineWrapping(false)
self.table.initialize(self.emoji_variation, self.ctx)
self.lp.SetWindowTitle("Unicode input")
self.ctx.AllowEscapeCodes = true
self.current_char = InvalidChar
self.current_tab_formatter = self.ctx.SprintFunc("reverse=false bold=true")
self.tab_bar_formatter = self.ctx.SprintFunc("reverse=true")
self.chosen_formatter = self.ctx.SprintFunc("fg=green")
self.chosen_name_formatter = self.ctx.SprintFunc("italic=true dim=true")
self.dim_formatter = self.ctx.SprintFunc("dim=true")
self.rl = readline.New(self.lp, readline.RlInit{Prompt: "> "})
self.rl.Start()
self.draw_screen()
}
func (self *handler) finalize() string {
self.rl.End()
self.rl.Shutdown()
return ""
}
func (self *handler) resolved_char() string {
if self.current_char == InvalidChar {
return ""
}
return resolved_char(self.current_char, self.emoji_variation)
}
func is_index(word string) bool {
if !strings.HasSuffix(word, INDEX_CHAR) {
return false
}
word = strings.TrimLeft(word, INDEX_CHAR)
_, err := strconv.ParseUint(word, 36, 32)
return err == nil
}
func (self *handler) update_codepoints() {
var codepoints []rune
var index_word int
var q checkpoints_key
q.mode = self.mode
switch self.mode {
case HEX:
codepoints = self.recent
case EMOTICONS:
codepoints = EMOTICONS_SET
case FAVORITES:
codepoints = load_favorites(false)
q.codepoints = codepoints
case NAME:
q.text = self.rl.AllText()
if !q.is_equal(self.checkpoints_key) {
words := strings.Split(q.text, " ")
words = utils.RemoveAll(words, INDEX_CHAR)
words = utils.Filter(words, is_index)
if len(words) > 0 {
iw := strings.TrimLeft(words[0], INDEX_CHAR)
words = words[1:]
n, err := strconv.ParseUint(iw, INDEX_BASE, 32)
if err == nil {
index_word = int(n)
}
}
codepoints = unicode_names.CodePointsForQuery(strings.Join(words, " "))
}
}
if !q.is_equal(self.checkpoints_key) {
self.checkpoints_key = q
self.table.set_codepoints(codepoints, self.mode, index_word)
}
}
func (self *handler) update_current_char() {
self.update_codepoints()
self.current_char = InvalidChar
text := self.rl.AllText()
switch self.mode {
case HEX:
if strings.HasPrefix(text, INDEX_CHAR) {
if len(text) > 1 {
self.current_char = self.table.codepoint_at_hint(text[1:])
}
} else if len(text) > 0 {
code, err := strconv.ParseUint(text, 16, 32)
if err == nil && code <= unicode.MaxRune {
self.current_char = rune(code)
}
}
case NAME:
cc := self.table.current_codepoint()
if cc > 0 && cc <= unicode.MaxRune {
self.current_char = rune(cc)
}
default:
if len(text) > 0 {
self.current_char = self.table.codepoint_at_hint(strings.TrimLeft(text, INDEX_CHAR))
}
}
if !codepoint_ok(self.current_char) {
self.current_char = InvalidChar
}
}
func (self *handler) update_prompt() {
self.update_current_char()
ch := "??"
color := "red"
self.choice_line = ""
if self.current_char != InvalidChar {
ch, color = self.resolved_char(), "green"
self.choice_line = fmt.Sprintf(
"Chosen: %s U+%x %s", self.chosen_formatter(ch), self.current_char,
self.chosen_name_formatter(unicode_names.NameForCodePoint(self.current_char)))
}
prompt := fmt.Sprintf("%s> ", self.ctx.SprintFunc("fg="+color)(ch))
self.rl.SetPrompt(prompt)
}
func (self *handler) draw_title_bar() {
entries := make([]string, 0, len(all_modes))
for _, md := range all_modes {
entry := fmt.Sprintf(" %s (%s) ", md.title, md.key)
if md.mode == self.mode {
entry = self.current_tab_formatter(entry)
}
entries = append(entries, entry)
}
sz, _ := self.lp.ScreenSize()
text := fmt.Sprintf("Search by:%s", strings.Join(entries, ""))
extra := int(sz.WidthCells) - wcswidth.Stringwidth(text)
if extra > 0 {
text += strings.Repeat(" ", extra)
}
self.lp.Println(self.tab_bar_formatter(text))
}
func (self *handler) draw_screen() {
self.lp.StartAtomicUpdate()
defer self.lp.EndAtomicUpdate()
self.lp.ClearScreen()
self.draw_title_bar()
y := 1
writeln := func(text ...any) {
self.lp.Println(text...)
y += 1
}
switch self.mode {
case NAME:
writeln("Enter words from the name of the character")
case HEX:
writeln("Enter the hex code for the character")
default:
writeln("Enter the index for the character you want from the list below")
}
self.rl.RedrawNonAtomic()
self.lp.SaveCursorPosition()
defer self.lp.RestoreCursorPosition()
writeln()
writeln(self.choice_line)
switch self.mode {
case HEX:
writeln(self.dim_formatter(fmt.Sprintf("Type %s followed by the index for the recent entries below", INDEX_CHAR)))
case NAME:
writeln(self.dim_formatter(fmt.Sprintf("Use Tab or arrow keys to choose a character. Type space and %s to select by index", INDEX_CHAR)))
case FAVORITES:
writeln(self.dim_formatter("Press F12 to edit the list of favorites"))
}
sz, _ := self.lp.ScreenSize()
q := self.table.layout(int(sz.HeightCells)-y, int(sz.WidthCells))
if q != "" {
self.lp.QueueWriteString(q)
}
}
func (self *handler) on_text(text string, from_key_event, in_bracketed_paste bool) error {
err := self.rl.OnText(text, from_key_event, in_bracketed_paste)
if err != nil {
return err
}
self.refresh()
return nil
}
func (self *handler) on_key_event(event *loop.KeyEvent) (err error) {
// TODO: Implement rest of this
err = self.rl.OnKeyEvent(event)
if err != nil {
if err == readline.ErrAcceptInput {
self.refresh()
self.lp.Quit(0)
return nil
}
return err
}
if event.Handled {
self.refresh()
}
return
}
func (self *handler) refresh() {
self.update_prompt()
self.draw_screen()
}
func run_loop(opts *Options) (lp *loop.Loop, err error) {
output := tui.KittenOutputSerializer()
lp, err = loop.New()
if err != nil {
return
}
cv := utils.NewCachedValues("unicode-input", &CachedData{Recent: DEFAULT_SET, Mode: DEFAULT_MODE})
cached_data = cv.Load()
defer cv.Save()
h := handler{recent: cached_data.Recent, lp: lp, emoji_variation: opts.EmojiVariation}
switch cached_data.Mode {
case "HEX":
h.mode = HEX
case "NAME":
h.mode = NAME
case "EMOTICONS":
h.mode = EMOTICONS
case "FAVORITES":
h.mode = FAVORITES
}
all_modes[0] = ModeData{mode: HEX, title: "Code", key: "F1"}
all_modes[1] = ModeData{mode: NAME, title: "Name", key: "F2"}
all_modes[2] = ModeData{mode: EMOTICONS, title: "Emoticons", key: "F3"}
all_modes[3] = ModeData{mode: FAVORITES, title: "Favorites", key: "F4"}
lp.OnInitialize = func() (string, error) {
h.initialize()
return "", nil
}
lp.OnResize = func(old_size, new_size loop.ScreenSize) error {
h.refresh()
return nil
}
lp.OnResumeFromStop = func() error {
h.refresh()
return nil
}
lp.OnText = h.on_text
lp.OnFinalize = h.finalize
lp.OnKeyEvent = h.on_key_event
err = lp.Run()
if err != nil {
return
}
if h.err == nil {
switch h.mode {
case HEX:
cached_data.Mode = "HEX"
case NAME:
cached_data.Mode = "NAME"
case EMOTICONS:
cached_data.Mode = "EMOTICONS"
case FAVORITES:
cached_data.Mode = "FAVORITES"
}
if h.current_char != InvalidChar {
cached_data.Recent = h.recent
idx := slices.Index(cached_data.Recent, h.current_char)
if idx > -1 {
cached_data.Recent = slices.Delete(cached_data.Recent, idx, idx+1)
}
cached_data.Recent = slices.Insert(cached_data.Recent, 0, h.current_char)[:len(DEFAULT_SET)]
ans := h.resolved_char()
o, err := output(ans)
if err != nil {
return lp, err
}
fmt.Println(o)
}
}
err = h.err
return
}
func main(cmd *cli.Command, o *Options, args []string) (rc int, err error) {
go unicode_names.Initialize() // start parsing name data in the background
build_sets()
lp, err := run_loop(o)
if err != nil {
return 1, err
}
ds := lp.DeathSignalName()
if ds != "" {
fmt.Println("Killed by signal: ", ds)
lp.KillIfSignalled()
return 1, nil
}
return
}
func EntryPoint(parent *cli.Command) {
create_cmd(parent, main)
}

View File

@ -0,0 +1,233 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package unicode_input
import (
"fmt"
"strconv"
"strings"
"kitty/tools/unicode_names"
"kitty/tools/utils"
"kitty/tools/utils/style"
"kitty/tools/wcswidth"
)
var _ = fmt.Print
func resolved_char(ch rune, emoji_variation string) string {
ans := string(ch)
if wcswidth.IsEmojiPresentationBase(ch) {
switch emoji_variation {
case "text":
ans += "\ufe0e"
case "graphic":
ans += "\ufe0f"
}
}
return ans
}
func decode_hint(text string) int {
x, err := strconv.ParseUint(text, INDEX_BASE, 32)
if err != nil {
return -1
}
return int(x)
}
func encode_hint(num int) string {
return strconv.FormatUint(uint64(num), INDEX_BASE)
}
func ljust(s string, sz int) string {
x := wcswidth.Stringwidth(s)
if x < sz {
s += strings.Repeat(" ", sz-x)
}
return s
}
type table struct {
emoji_variation string
layout_dirty bool
last_rows, last_cols int
codepoints []rune
current_idx, scroll_rows int
text string
num_cols, num_rows int
mode Mode
green, reversed, not_reversed, intense_gray func(...any) string
}
func (self *table) initialize(emoji_variation string, ctx style.Context) {
self.emoji_variation = emoji_variation
self.layout_dirty = true
self.last_cols, self.last_rows = -1, -1
self.green = ctx.SprintFunc("fg=green")
self.reversed = ctx.SprintFunc("reverse=true")
self.not_reversed = ctx.SprintFunc("reverse=false")
self.intense_gray = ctx.SprintFunc("fg=intense-gray")
}
func (self *table) current_codepoint() rune {
if len(self.codepoints) > 0 {
return self.codepoints[self.current_idx]
}
return InvalidChar
}
func (self *table) set_codepoints(codepoints []rune, mode Mode, current_idx int) {
self.codepoints = codepoints
self.mode = mode
self.layout_dirty = true
if self.current_idx >= len(self.codepoints) {
self.current_idx = 0
}
self.scroll_rows = 0
}
func (self *table) codepoint_at_hint(hint string) rune {
idx := decode_hint(hint)
if idx >= 0 && idx < len(self.codepoints) {
return self.codepoints[idx]
}
return InvalidChar
}
type cell_data struct {
idx, ch, desc string
}
func (self *table) layout(rows, cols int) string {
if !self.layout_dirty && self.last_cols == cols && self.last_rows == rows {
return self.text
}
self.last_cols, self.last_rows = cols, rows
self.layout_dirty = false
var as_parts func(int, rune) cell_data
var cell func(int, cell_data)
var idx_size, space_for_desc int
output := strings.Builder{}
output.Grow(4096)
switch self.mode {
case NAME:
as_parts = func(i int, codepoint rune) cell_data {
return cell_data{idx: ljust(encode_hint(i), idx_size), ch: resolved_char(codepoint, self.emoji_variation), desc: unicode_names.NameForCodePoint(codepoint)}
}
cell = func(i int, cd cell_data) {
is_current := i == self.current_idx
text := self.green(cd.idx) + " \x1b[49m" + cd.ch + " "
w := wcswidth.Stringwidth(cd.ch)
if w < 2 {
text += strings.Repeat(" ", (2 - w))
}
if len(cd.desc) > space_for_desc {
text += cd.desc[:space_for_desc-1] + "…"
} else {
text += cd.desc
extra := space_for_desc - len(cd.desc)
if extra > 0 {
text += strings.Repeat(" ", extra)
}
}
if is_current {
text = self.reversed(text)
} else {
text = self.not_reversed(text)
}
output.WriteString(text)
}
default:
as_parts = func(i int, codepoint rune) cell_data {
return cell_data{idx: ljust(encode_hint(i), idx_size), ch: resolved_char(codepoint, self.emoji_variation)}
}
cell = func(i int, cd cell_data) {
output.WriteString(self.green(cd.idx))
output.WriteString(" ")
output.WriteString(self.intense_gray(cd.ch))
w := wcswidth.Stringwidth(cd.ch)
if w < 2 {
output.WriteString(strings.Repeat(" ", (2 - w)))
}
}
}
num := len(self.codepoints)
if num < 1 {
self.text = ""
self.num_cols = 0
self.num_rows = 0
return self.text
}
idx_size = len(encode_hint(num - 1))
parts := make([]cell_data, len(self.codepoints))
for i, ch := range self.codepoints {
parts[i] = as_parts(i, ch)
}
longest := 0
switch self.mode {
case NAME:
for _, p := range parts {
longest = utils.Max(longest, idx_size+2+len(p.desc)+2)
}
default:
longest = idx_size + 3
}
col_width := longest + 2
col_width = utils.Min(col_width, 40)
space_for_desc = col_width - 2 - idx_size - 4
self.num_cols = utils.Max(cols/col_width, 1)
self.num_rows = rows
rows_left := rows
skip_scroll := self.scroll_rows * self.num_cols
for i, cd := range parts {
if skip_scroll > 0 {
skip_scroll -= 1
continue
}
cell(i, cd)
output.WriteString(" ")
if i > 0 && (i+1)%self.num_cols == 0 {
rows_left -= 1
if rows_left == 0 {
break
}
output.WriteString("\r\n")
}
}
self.text = output.String()
return self.text
}
func (self *table) move_current(rows, cols int) {
if len(self.codepoints) == 0 {
return
}
if cols != 0 {
self.current_idx = (self.current_idx + len(self.codepoints) + cols) % len(self.codepoints)
self.layout_dirty = true
}
if rows != 0 {
amt := rows * self.num_cols
self.current_idx += amt
self.current_idx = utils.Max(0, utils.Min(self.current_idx, len(self.codepoints)-1))
self.layout_dirty = true
}
first_visible := self.scroll_rows * self.num_cols
last_visible := first_visible + ((self.num_cols * self.num_rows) - 1)
scroll_amount := self.num_rows
if self.current_idx < first_visible {
self.scroll_rows = utils.Max(self.scroll_rows-scroll_amount, 0)
}
if self.current_idx > last_visible {
self.scroll_rows += scroll_amount
}
}

View File

@ -195,11 +195,11 @@ func (self *Loop) Println(args ...any) {
self.QueueWriteString("\r\n")
}
func (self *Loop) SaveCursor() {
func (self *Loop) SaveCursorPosition() {
self.QueueWriteString("\x1b7")
}
func (self *Loop) RestoreCursor() {
func (self *Loop) RestoreCursorPosition() {
self.QueueWriteString("\x1b8")
}
@ -341,6 +341,12 @@ func (self *Loop) AllowLineWrapping(allow bool) {
}
}
func (self *Loop) SetWindowTitle(title string) {
title = strings.ReplaceAll(title, "\033", "")
title = strings.ReplaceAll(title, "\x9c", "")
self.QueueWriteString("\033]2;" + title + "\033\\")
}
func (self *Loop) ClearScreen() {
self.QueueWriteString("\x1b[H\x1b[2J")
}

View File

@ -168,6 +168,10 @@ func New(loop *loop.Loop, r RlInit) *Readline {
return ans
}
func (self *Readline) SetPrompt(prompt string) {
self.prompt = self.make_prompt(prompt, false)
}
func (self *Readline) Shutdown() {
self.history.Shutdown()
}

37
tools/tui/ui_kitten.go Normal file
View File

@ -0,0 +1,37 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package tui
import (
"encoding/json"
"fmt"
"os"
"kitty/tools/utils"
)
var _ = fmt.Print
func KittenOutputSerializer() func(any) (string, error) {
write_with_escape_code := os.Getenv("KITTEN_RUNNING_AS_UI") != ""
os.Unsetenv("KITTEN_RUNNING_AS_UI")
if write_with_escape_code {
return func(what any) (string, error) {
data, err := json.Marshal(what)
if err != nil {
return "", err
}
return "\x1bP@kitty-kitten-result|" + utils.UnsafeBytesToString(data) + "\x1b\\", nil
}
}
return func(what any) (string, error) {
if sval, ok := what.(string); ok {
return sval, nil
}
data, err := json.MarshalIndent(what, "", " ")
if err != nil {
return "", err
}
return utils.UnsafeBytesToString(data), nil
}
}

37998
tools/unicode_names/names.txt generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,174 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package unicode_names
import (
"bytes"
"compress/zlib"
_ "embed"
"encoding/binary"
"fmt"
"io"
"strings"
"sync"
"time"
"kitty/tools/utils"
"kitty/tools/utils/images"
)
type mark_set = *utils.Set[uint16]
//go:embed data_generated.bin
var unicode_name_data string
var _ = fmt.Print
var names map[rune]string
var marks []rune
var word_map map[string][]uint16
func add_word(codepoint uint16, word []byte) {
w := utils.UnsafeBytesToString(word)
word_map[w] = append(word_map[w], codepoint)
}
func add_words(codepoint uint16, raw []byte) {
for len(raw) > 0 {
idx := bytes.IndexByte(raw, ' ')
if idx < 0 {
add_word(codepoint, raw)
break
}
if idx > 0 {
add_word(codepoint, raw[:idx])
}
raw = raw[idx+1:]
}
}
func parse_record(record []byte, mark uint16) {
codepoint := rune(binary.LittleEndian.Uint32(record))
record = record[4:]
marks[mark] = codepoint
namelen := binary.LittleEndian.Uint16(record)
record = record[2:]
name := utils.UnsafeBytesToString(record[:namelen])
names[codepoint] = name
add_words(mark, record[:namelen])
if len(record) > int(namelen) {
add_words(mark, record[namelen:])
}
}
var parse_once sync.Once
func read_all(r io.Reader, expected_size int) ([]byte, error) {
b := make([]byte, 0, expected_size)
for {
if len(b) == cap(b) {
// Add more capacity (let append pick how much).
b = append(b, 0)[:len(b)]
}
n, err := r.Read(b[len(b):cap(b)])
b = b[:len(b)+n]
if err != nil {
if err == io.EOF {
err = nil
}
return b, err
}
}
}
func parse_data() {
compressed := utils.UnsafeStringToBytes(unicode_name_data)
uncompressed_size := binary.LittleEndian.Uint32(compressed)
r, _ := zlib.NewReader(bytes.NewReader(compressed[4:]))
defer r.Close()
raw, err := read_all(r, int(uncompressed_size))
if err != nil {
panic(err)
}
num_of_lines := binary.LittleEndian.Uint32(raw)
raw = raw[4:]
num_of_words := binary.LittleEndian.Uint32(raw)
raw = raw[4:]
names = make(map[rune]string, num_of_lines)
word_map = make(map[string][]uint16, num_of_words)
marks = make([]rune, num_of_lines)
var mark uint16
for len(raw) > 0 {
record_len := binary.LittleEndian.Uint16(raw)
raw = raw[2:]
parse_record(raw[:record_len], mark)
mark += 1
raw = raw[record_len:]
}
}
func Initialize() {
parse_once.Do(parse_data)
}
func NameForCodePoint(cp rune) string {
Initialize()
return names[cp]
}
func find_matching_codepoints(prefix string) (ans mark_set) {
for q, marks := range word_map {
if strings.HasPrefix(q, prefix) {
if ans == nil {
ans = utils.NewSet[uint16](len(marks) * 2)
}
ans.AddItems(marks...)
}
}
return ans
}
func marks_for_query(query string) (ans mark_set) {
Initialize()
prefixes := strings.Split(strings.ToLower(query), " ")
results := make(chan mark_set, len(prefixes))
ctx := images.Context{}
ctx.Parallel(0, len(prefixes), func(nums <-chan int) {
for i := range nums {
results <- find_matching_codepoints(prefixes[i])
}
})
close(results)
for x := range results {
if ans == nil {
ans = x
} else {
ans = ans.Intersect(x)
}
}
if ans == nil {
ans = utils.NewSet[uint16](0)
}
return
}
func CodePointsForQuery(query string) (ans []rune) {
x := marks_for_query(query)
ans = make([]rune, x.Len())
i := 0
for m := range x.Iterable() {
ans[i] = marks[m]
i += 1
}
return
}
func Develop() {
start := time.Now()
Initialize()
fmt.Println("Parsing unicode name data took:", time.Since(start))
start = time.Now()
num := CodePointsForQuery("arr")
fmt.Println("Querying arr took:", time.Since(start), "and found:", len(num))
start = time.Now()
num = CodePointsForQuery("arr right")
fmt.Println("Querying arr right took:", time.Since(start), "and found:", len(num))
}

View File

@ -0,0 +1,35 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package unicode_names
import (
"fmt"
"kitty/tools/utils"
"testing"
"github.com/google/go-cmp/cmp"
"golang.org/x/exp/slices"
)
var _ = fmt.Print
func TestUnicodeInputQueries(t *testing.T) {
ts := func(query string, expected ...rune) {
if expected == nil {
expected = make([]rune, 0)
}
expected = utils.Sort(expected, func(a, b rune) bool { return a < b })
actual := CodePointsForQuery(query)
actual = utils.Sort(actual, func(a, b rune) bool { return a < b })
diff := cmp.Diff(expected, actual)
if diff != "" {
t.Fatalf("Failed query: %#v\n%s", query, diff)
}
}
ts("horiz ell", 0x2026, 0x22ef, 0x2b2c, 0x2b2d, 0xfe19)
ts("horizontal ell", 0x2026, 0x22ef, 0x2b2c, 0x2b2d, 0xfe19)
ts("kfjhgkjdsfhgkjds")
if slices.Index(CodePointsForQuery("bee"), 0x1f41d) < 0 {
t.Fatalf("The query bee did not match the codepoint: 0x1f41d")
}
}