123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432 | # A unicode table viewer TUI
use btui
use <sys/wait.h>
use ./names.tm
use ./themes.tm
BINARY: "unicode"
HELP: "
`unicode` is a Tomo program to view information about the Unicode 3.1 standard
codepoints. The table viewer is an interactive text user interface with the
following controls:
q - Quit the program
j/k or up/down - Move up or down one entry
Ctrl+d/Ctrl+u - Move up or down one page
g/G - Move to top/bottom
Ctrl+c or y - Copy the text of an entry to the clipboard
u - Copy the codepoint of an entry (U+XXXX) to the clipboard
d - Copy the decimal codepoint of an entry to the clipboard
p - Exit and print the unicode entry to stdout
Ctrl+f or / - Search for text (enter to confirm)
n/N - Jump to next/previous search result
i - Toggle info panel
"
MANPAGE_DESCRIPTION: "
`unicode` is a Tomo program to view information about the Unicode 3.1 standard
codepoints. The table viewer is an interactive text user interface.
"
LICENSE: ./LICENSE.md
struct UnicodeBlock(first,last:Int32, description:Text)
UNICODE_BLOCKS : [UnicodeBlock] = UnicodeBlock.load_all()
func load_all(-> [UnicodeBlock])
C_code `
static const char unicode_blocks[] = {
#embed "../UnicodeBlocks.txt"
,0,
};
`
blocks : &[UnicodeBlock] = &[]
for line in C_code:Text`Text$from_str(unicode_blocks)`.lines()
skip if line.length == 0 or line.starts_with("#")
sections := line.split(";")
range := sections[1]!.split("..")
low := Int32.parse(range[1]!, 16)!
high := Int32.parse(range[2]!, 16)!
block := UnicodeBlock(low, high, sections[2]!.trim())
blocks.insert(block)
assert blocks.length > 0
return blocks
func find(codepoint:Int32 -> UnicodeBlock?)
target := UnicodeBlock(codepoint, codepoint, "")
i := UnicodeBlock.UNICODE_BLOCKS.binary_search(func(b:&UnicodeBlock) b[] >= target) or return none
return UnicodeBlock.UNICODE_BLOCKS[i]
struct UnicodeEntry(
codepoint:Int32,
text:Text?=none,
name:Text="",
category:Text="",
combining_class:Text="",
bidi_class:Text="",
decomposition_mapping:Text="",
decimal_digit:Int?=none,
digit:Int?=none,
numeric:Int?=none,
mirrored:Bool=no,
unicode_1_name:Text?=none,
iso_comment:Text?=none,
simple_uppercase:Int32?=none,
simple_lowercase:Int32?=none,
simple_titlecase:Int32?=none,
)
func parse(text:Text -> UnicodeEntry?)
# For format details, see: https://www.unicode.org/L2/L1999/UnicodeData.html
items := text.split(";")
entry := UnicodeEntry(Int32.parse((items[1] or return none), 16) or return none)
entry.text = Text.from_utf32([entry.codepoint])
entry.name = items[2] or return none
entry.category = items[3] or return none
entry.combining_class = items[4] or return none
entry.bidi_class = items[5] or return none
entry.decomposition_mapping = items[6] or return none
junk : Text
entry.decimal_digit = Int.parse(items[7] or return none, remainder=&junk)
entry.digit = Int.parse(items[8] or return none, remainder=&junk)
entry.numeric = Int.parse(items[9] or return none, remainder=&junk)
entry.mirrored = items[10] == "Y"
entry.unicode_1_name = (if items[11]!.length > 0 then items[11]!)
entry.iso_comment = items[12]
entry.simple_uppercase = Int32.parse((items[13] or return none), 16, &junk)
entry.simple_lowercase = Int32.parse((items[14] or return none), 16, &junk)
entry.simple_titlecase = Int32.parse((items[15] or return none), 16, &junk)
return entry
func info(self:UnicodeEntry -> {Text:Text})
block := UnicodeBlock.find(self.codepoint)
return {
"Symbol": (if self.codepoint > 32 then self.text or "" else ""),
"Block": (if b := block then b.description else ""),
"UTF32": "$(self.codepoint.hex()) ($(self.codepoint))",
"UTF16": (if text := self.text then " ".join([u.hex() for u in text.utf16()]) ++ " (" ++ " ".join([Text(u) for u in text.utf16()]) ++ ")" else "")
"UTF8": (if text := self.text then " ".join([b.hex() for b in text.utf8()]) else "")
"Name": self.name,
"Unicode 1 name": self.unicode_1_name or "",
"Category": named(self.category, CATEGORY_NAMES),
"Combining class": named(self.combining_class, COMBINING_NAMES),
"Bidi class": named(self.bidi_class, BIDI_NAMES),
"Decomposition": named(self.decomposition_mapping, DECOMPOSITION_NAMES),
"Digit": (if d := self.digit then Text(d) else ""),
"Mirrored": Text(self.mirrored),
"ISO comment": self.iso_comment or "",
"Uppercase": (if u := self.simple_uppercase then Text.from_utf32([u])! else ""),
"Lowercase": (if l := self.simple_lowercase then Text.from_utf32([l])! else ""),
"Titlecase": (if t := self.simple_titlecase then Text.from_utf32([t])! else ""),
}
func draw(self:UnicodeEntry, y:Int, theme:Theme=Dark, highlighted=no)
columns := [
"$(
if theme == Theme.None and highlighted then ">" else " "
)U+$(self.codepoint.hex(digits=5, prefix=no))",
(
if text := self.text
if self.codepoint > 32
text
else
""
else
""
)
(
do
name := if self.name then self.name else "No name"
if desc := self.unicode_1_name
name ++= " "++desc
name.title()
),
]
styles := [
func() theme.row_codepoint(highlighted)
func() theme.row_character(highlighted)
func() theme.row_description(highlighted)
]
widths := [10, 6, 32]
x := 0
for i,column in columns
styles[i]!()
write(" $column ", ScreenVec2(x, y))
clear(Right)
x += widths[i]!
struct TableViewer(
entries:[Text],
_top:Int=1,
_cursor:Int=1,
quit:Bool=no,
show_info:Bool=yes,
search_start:Int?=none,
search:Text?=none,
message:Text?=none,
theme:Theme=Theme.Dark,
)
func draw(self:TableViewer)
size := get_size()
self.theme.header()
write(" Codepoint Symbol Description ", ScreenVec2(0,0))
clear(Right)
for y in (1).to(size.y - 1)
row := self._top + y - 1
entry := self.get_entry(row) or skip
entry.draw(y, theme=self.theme, highlighted=(row == self._cursor))
if self.show_info
if entry := self.get_entry()
info := entry.info()
height := info.length + 2
label_width := (_max_: k.width() for k in info.keys)!
value_width := (_max_: v.width() for v in info.values)! _max_ 50
width := label_width + 3 + value_width
top_left := ScreenVec2(size.x - width - 1, 1)
self.theme.box()
fill_box(top_left, ScreenVec2(width, height))
for i,label in info.keys
write(label, pos=top_left + ScreenVec2(label_width + 1, i), Right)
self.theme.box_details()
for i,value in info.values
write(value, pos=top_left + ScreenVec2(label_width + 2, i), Left)
if search := self.search
self.theme.search_label(self.search_start != none)
write(" Search: ", ScreenVec2(0, size.y-1))
self.theme.search_text(self.search_start != none)
write(" "++search)
clear(Right)
if message := self.message
self.theme.message_theme()
write(" $message ", ScreenVec2(size.x-1, size.y-1), Right)
clear(Right)
# Scroll bar
scroll_height := size.y-2
scroll_top := 1 + (self._top * scroll_height)/self.entries.length
scroll_bottom := 1 + ((self._top + scroll_height - 1) * scroll_height)/self.entries.length
self.theme.scroll_bg()
for y in (1).to(scroll_top-1, step=1)
write(" ", ScreenVec2(size.x-1, y))
self.theme.scroll_bar()
for y in (scroll_top).to(scroll_bottom, step=1)
write(" ", ScreenVec2(size.x-1, y))
self.theme.scroll_bg()
for y in (scroll_bottom+1).to(size.y-1, step=1)
write(" ", ScreenVec2(size.x-1, y))
flush()
func update(self:&TableViewer)
if self.search_start
self.update_search()
return
size := get_size()
mouse_pos := ScreenVec2(0, 0)
key := get_key(&mouse_pos)
when key
is "j", "Down"
self.move_cursor(1)
is "Mouse wheel down"
self.move_scroll(1)
is "k", "Up"
self.move_cursor(-1)
is "Mouse wheel up"
self.move_scroll(-1)
is "Left press", "Left drag"
if 1 <= mouse_pos.y and mouse_pos.y <= size.y-2
if mouse_pos.x >= size.x-1
self.set_cursor((self.entries.length * (mouse_pos.y - 1)) / (size.y - 2))
# Prevent spamming the console too much
sleep(0.01)
else if key == "Left press"
self.set_cursor(self._top + mouse_pos.y - 1)
is "g"
self.move_cursor(-self.entries.length)
is "G"
self.move_cursor(self.entries.length)
is "q"
self.quit = yes
is "Escape"
if self.search != none
self.search = none
else
self.quit = yes
is "Ctrl-d"
self.move_scroll(size.y/2)
is "Ctrl-u"
self.move_scroll(-size.y/2)
is "Ctrl-c", "y"
if entry := self.get_entry()
if text := entry.text
if copy_to_clipboard(text)
self.message = "Copied text!"
else
self.message = "Failed to copy to clipboard!"
is "p"
if entry := self.get_entry()
if text := entry.text
disable()
print(text)
exit()
is "u"
if entry := self.get_entry()
if copy_to_clipboard("U+$(entry.codepoint.hex())")
self.message = "Copied U+$(entry.codepoint.hex())!"
else
self.message = "Failed to copy to clipboard!"
is "d"
if entry := self.get_entry()
if copy_to_clipboard("$(entry.codepoint)")
self.message = "Copied $(entry.codepoint)!"
else
self.message = "Failed to copy to clipboard!"
is "i"
self.show_info = not self.show_info
is "/", "Ctrl-f"
self.search = ""
self.search_start = self._cursor
self.message = none
is "n"
if search := self.search
for offset in (0).to(self.entries.length-1)
index := (self._cursor + 1 + offset) mod1 self.entries.length
line := self.entries[index]!
if line.lower().has(search)
self.set_cursor(index)
stop
is "N"
if search := self.search
for offset in (self.entries.length-1).to(0)
index := (self._cursor - 1 + offset) mod1 self.entries.length
line := self.entries[index]!
if line.lower().has(search)
self.set_cursor(index)
stop
func get_entry(self:TableViewer, row:Int?=none -> UnicodeEntry?)
return UnicodeEntry.parse(self.entries[row or self._cursor] or return none)
func update_search(self:&TableViewer)
key := get_key()
search := self.search or ""
when key
is "Escape", "Ctrl-c"
self.search = none
self.search_start = none
return
is "Enter"
# keep self.search so we can use 'n' to find it later
self.search_start = none
return
is "Space"
search = search ++ " "
is "Backspace"
search = search.to(-2)
else if key.length == 1
search = search ++ key
search = search.lower()
self.search = search
search_start := self.search_start or 1
for offset in (0).to(self.entries.length-1)
index := (search_start + offset) mod1 self.entries.length
line := self.entries[index] or skip
if search.length == 1
# Single letter searches should find exact match
if (self.get_entry(index) or skip).text == search
self.set_cursor(index)
stop
else if line.lower().has(search)
# Otherwise look for text match on the whole record
self.set_cursor(index)
stop
func move_cursor(self:&TableViewer, delta:Int)
self.set_cursor(self._cursor + delta)
func set_cursor(self:&TableViewer, pos:Int)
size := get_size()
self._cursor = Int.clamped(pos, 1, self.entries.length)
table_height := size.y - 2
self._top = Int.clamped(Int.clamped(self._top, self._cursor - table_height + 5, self._cursor + - 5), 1, self.entries.length - table_height)
func move_scroll(self:&TableViewer, delta:Int)
size := get_size()
self._top = Int.clamped(self._top + delta, 1, self.entries.length)
table_height := size.y - 2
self._cursor = Int.clamped(self._cursor, self._top + 5, self._top + table_height - 5)
func copy_to_clipboard(text:Text -> Bool)
success := no
C_code `
int fds[2];
pipe(fds);
pid_t child = fork();
if (child == 0) {
close(fds[1]);
dup2(fds[0], STDIN_FILENO);
#ifdef __APPLE__
execlp("pbcopy", "pbcopy", NULL);
#else
execlp("xclip", "xclip", "-selection", "clipboard", NULL);
#endif
errx(1, "Could not exec!");
}
close(fds[0]);
const char *str = @(text.as_c_string());
write(fds[1], str, strlen(str));
close(fds[1]);
int status;
waitpid(child, &status, 0);
@success = (WIFEXITED(status) && WEXITSTATUS(status) == 0);
`
return success
func main(codepoint|c:Int32?=none, text|t:Text?=none, theme|T:Theme?=none)
C_code `
static const char unicode_table[] = {
#embed "../UnicodeData.txt"
,0,
};
`
table_lines := C_code:Text`Text$from_str(unicode_table)`.lines()
set_mode(TUI)
hide_cursor()
viewer := TableViewer(table_lines, theme=theme or Theme.guess())
# Set cursor position according to CLI flags
if c := codepoint
start_text := c.hex(digits=4, prefix=no) ++ ";"
for i,line in viewer.entries
if line.starts_with(start_text)
viewer.set_cursor(i)
stop
else if t := text
for i,line in viewer.entries
entry := UnicodeEntry.parse(line) or skip
if t.starts_with(entry.text or skip)
viewer.set_cursor(i)
stop
viewer.draw()
while not viewer.quit
prev := viewer
viewer.update()
if viewer != prev
viewer.draw()
disable()
|