mirror of
https://github.com/kovidgoyal/kitty.git
synced 2024-08-17 10:30:25 +03:00
Start work on porting unicode input kitten to Go
This commit is contained in:
parent
a2e4efbb14
commit
53e33a80ba
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -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
1
.gitignore
vendored
@ -12,6 +12,7 @@
|
||||
/kitty.app/
|
||||
/glad/out/
|
||||
/kitty/launcher/kitt*
|
||||
/tools/unicode_names/data_generated.bin
|
||||
/*.dSYM/
|
||||
__pycache__/
|
||||
/glfw/wayland-*-client-protocol.[ch]
|
||||
|
@ -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()
|
||||
|
114
gen-wcwidth.py
114
gen-wcwidth.py
@ -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:
|
||||
|
@ -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'
|
||||
|
76397
kittens/unicode_input/names.h
generated
76397
kittens/unicode_input/names.h
generated
File diff suppressed because one or more lines are too long
@ -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);
|
||||
}
|
@ -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
|
@ -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,
|
||||
|
@ -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'))
|
@ -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" ;;
|
||||
|
@ -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__",
|
||||
|
470
tools/cmd/unicode_input/main.go
Normal file
470
tools/cmd/unicode_input/main.go
Normal 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)
|
||||
}
|
233
tools/cmd/unicode_input/table.go
Normal file
233
tools/cmd/unicode_input/table.go
Normal 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
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
|
@ -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
37
tools/tui/ui_kitten.go
Normal 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
37998
tools/unicode_names/names.txt
generated
Normal file
File diff suppressed because it is too large
Load Diff
174
tools/unicode_names/query.go
Normal file
174
tools/unicode_names/query.go
Normal 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))
|
||||
}
|
35
tools/unicode_names/query_test.go
Normal file
35
tools/unicode_names/query_test.go
Normal 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")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user