(432 lines)
1 # A unicode table viewer TUI8 HELP: "9 `unicode` is a Tomo program to view information about the Unicode 3.1 standard10 codepoints. The table viewer is an interactive text user interface with the11 following controls:13 q - Quit the program14 j/k or up/down - Move up or down one entry15 Ctrl+d/Ctrl+u - Move up or down one page16 g/G - Move to top/bottom17 Ctrl+c or y - Copy the text of an entry to the clipboard18 u - Copy the codepoint of an entry (U+XXXX) to the clipboard19 d - Copy the decimal codepoint of an entry to the clipboard20 p - Exit and print the unicode entry to stdout21 Ctrl+f or / - Search for text (enter to confirm)22 n/N - Jump to next/previous search result23 i - Toggle info panel24 "25 MANPAGE_DESCRIPTION: "26 `unicode` is a Tomo program to view information about the Unicode 3.1 standard27 codepoints. The table viewer is an interactive text user interface.28 "29 LICENSE: ./LICENSE.md35 C_code `36 static const char unicode_blocks[] = {39 };40 `41 blocks : &[UnicodeBlock] = &[]54 target := UnicodeBlock(codepoint, codepoint, "")55 i := UnicodeBlock.UNICODE_BLOCKS.binary_search(func(b:&UnicodeBlock) b[] >= target) or return none60 codepoint:Int32,62 name:Text="",63 category:Text="",64 combining_class:Text="",65 bidi_class:Text="",66 decomposition_mapping:Text="",76 )87 junk : Text106 "UTF16": (if text := self.text then " ".join([u.hex() for u in text.utf16()]) ++ " (" ++ " ".join([Text(u) for u in text.utf16()]) ++ ")" else "")120 }123 columns := [128 (129 if text := self.text130 if self.codepoint > 32131 text132 else133 ""134 else135 ""136 )138 (139 do140 name := if self.name then self.name else "No name"141 if desc := self.unicode_1_name142 name ++= " "++desc143 name.title()144 ),145 ]147 styles := [148 func() theme.row_codepoint(highlighted)149 func() theme.row_character(highlighted)150 func() theme.row_description(highlighted)151 ]153 widths := [10, 6, 32]155 x := 0156 for i,column in columns157 styles[i]!()159 clear(Right)160 x += widths[i]!162 struct TableViewer(163 entries:[Text],164 _top:Int=1,165 _cursor:Int=1,166 quit:Bool=no,167 show_info:Bool=yes,168 search_start:Int?=none,169 search:Text?=none,170 message:Text?=none,171 theme:Theme=Theme.Dark,172 )173 func draw(self:TableViewer)174 size := get_size()175 self.theme.header()176 write(" Codepoint Symbol Description ", ScreenVec2(0,0))177 clear(Right)179 for y in (1).to(size.y - 1)180 row := self._top + y - 1181 entry := self.get_entry(row) or skip182 entry.draw(y, theme=self.theme, highlighted=(row == self._cursor))184 if self.show_info185 if entry := self.get_entry()186 info := entry.info()187 height := info.length + 2188 label_width := (_max_: k.width() for k in info.keys)!189 value_width := (_max_: v.width() for v in info.values)! _max_ 50190 width := label_width + 3 + value_width191 top_left := ScreenVec2(size.x - width - 1, 1)192 self.theme.box()193 fill_box(top_left, ScreenVec2(width, height))194 for i,label in info.keys195 write(label, pos=top_left + ScreenVec2(label_width + 1, i), Right)196 self.theme.box_details()197 for i,value in info.values198 write(value, pos=top_left + ScreenVec2(label_width + 2, i), Left)200 if search := self.search201 self.theme.search_label(self.search_start != none)202 write(" Search: ", ScreenVec2(0, size.y-1))203 self.theme.search_text(self.search_start != none)204 write(" "++search)205 clear(Right)207 if message := self.message208 self.theme.message_theme()210 clear(Right)212 # Scroll bar213 scroll_height := size.y-2214 scroll_top := 1 + (self._top * scroll_height)/self.entries.length215 scroll_bottom := 1 + ((self._top + scroll_height - 1) * scroll_height)/self.entries.length216 self.theme.scroll_bg()217 for y in (1).to(scroll_top-1, step=1)218 write(" ", ScreenVec2(size.x-1, y))219 self.theme.scroll_bar()220 for y in (scroll_top).to(scroll_bottom, step=1)221 write(" ", ScreenVec2(size.x-1, y))222 self.theme.scroll_bg()223 for y in (scroll_bottom+1).to(size.y-1, step=1)224 write(" ", ScreenVec2(size.x-1, y))226 flush()228 func update(self:&TableViewer)229 if self.search_start230 self.update_search()231 return233 size := get_size()234 mouse_pos := ScreenVec2(0, 0)235 key := get_key(&mouse_pos)236 when key237 is "j", "Down"238 self.move_cursor(1)239 is "Mouse wheel down"240 self.move_scroll(1)241 is "k", "Up"242 self.move_cursor(-1)243 is "Mouse wheel up"244 self.move_scroll(-1)245 is "Left press", "Left drag"246 if 1 <= mouse_pos.y and mouse_pos.y <= size.y-2247 if mouse_pos.x >= size.x-1248 self.set_cursor((self.entries.length * (mouse_pos.y - 1)) / (size.y - 2))249 # Prevent spamming the console too much250 sleep(0.01)251 else if key == "Left press"252 self.set_cursor(self._top + mouse_pos.y - 1)253 is "g"254 self.move_cursor(-self.entries.length)255 is "G"256 self.move_cursor(self.entries.length)257 is "q"258 self.quit = yes259 is "Escape"260 if self.search != none261 self.search = none262 else263 self.quit = yes264 is "Ctrl-d"265 self.move_scroll(size.y/2)266 is "Ctrl-u"267 self.move_scroll(-size.y/2)268 is "Ctrl-c", "y"269 if entry := self.get_entry()270 if text := entry.text271 if copy_to_clipboard(text)272 self.message = "Copied text!"273 else274 self.message = "Failed to copy to clipboard!"275 is "p"276 if entry := self.get_entry()277 if text := entry.text278 disable()279 print(text)280 exit()282 is "u"283 if entry := self.get_entry()286 else287 self.message = "Failed to copy to clipboard!"288 is "d"289 if entry := self.get_entry()292 else293 self.message = "Failed to copy to clipboard!"294 is "i"295 self.show_info = not self.show_info296 is "/", "Ctrl-f"297 self.search = ""298 self.search_start = self._cursor299 self.message = none300 is "n"301 if search := self.search302 for offset in (0).to(self.entries.length-1)303 index := (self._cursor + 1 + offset) mod1 self.entries.length304 line := self.entries[index]!305 if line.lower().has(search)306 self.set_cursor(index)307 stop308 is "N"309 if search := self.search310 for offset in (self.entries.length-1).to(0)311 index := (self._cursor - 1 + offset) mod1 self.entries.length312 line := self.entries[index]!313 if line.lower().has(search)314 self.set_cursor(index)315 stop317 func get_entry(self:TableViewer, row:Int?=none -> UnicodeEntry?)318 return UnicodeEntry.parse(self.entries[row or self._cursor] or return none)320 func update_search(self:&TableViewer)321 key := get_key()322 search := self.search or ""323 when key324 is "Escape", "Ctrl-c"325 self.search = none326 self.search_start = none327 return328 is "Enter"329 # keep self.search so we can use 'n' to find it later330 self.search_start = none331 return332 is "Space"333 search = search ++ " "334 is "Backspace"335 search = search.to(-2)336 else if key.length == 1337 search = search ++ key339 search = search.lower()340 self.search = search342 search_start := self.search_start or 1343 for offset in (0).to(self.entries.length-1)344 index := (search_start + offset) mod1 self.entries.length345 line := self.entries[index] or skip346 if search.length == 1347 # Single letter searches should find exact match348 if (self.get_entry(index) or skip).text == search349 self.set_cursor(index)350 stop351 else if line.lower().has(search)352 # Otherwise look for text match on the whole record353 self.set_cursor(index)354 stop356 func move_cursor(self:&TableViewer, delta:Int)357 self.set_cursor(self._cursor + delta)359 func set_cursor(self:&TableViewer, pos:Int)360 size := get_size()361 self._cursor = Int.clamped(pos, 1, self.entries.length)362 table_height := size.y - 2363 self._top = Int.clamped(Int.clamped(self._top, self._cursor - table_height + 5, self._cursor + - 5), 1, self.entries.length - table_height)365 func move_scroll(self:&TableViewer, delta:Int)366 size := get_size()367 self._top = Int.clamped(self._top + delta, 1, self.entries.length)368 table_height := size.y - 2369 self._cursor = Int.clamped(self._cursor, self._top + 5, self._top + table_height - 5)371 func copy_to_clipboard(text:Text -> Bool)372 success := no373 C_code `374 int fds[2];375 pipe(fds);376 pid_t child = fork();377 if (child == 0) {378 close(fds[1]);379 dup2(fds[0], STDIN_FILENO);380 #ifdef __APPLE__381 execlp("pbcopy", "pbcopy", NULL);382 #else383 execlp("xclip", "xclip", "-selection", "clipboard", NULL);384 #endif385 errx(1, "Could not exec!");386 }387 close(fds[0]);388 const char *str = @(text.as_c_string());389 write(fds[1], str, strlen(str));390 close(fds[1]);391 int status;392 waitpid(child, &status, 0);393 @success = (WIFEXITED(status) && WEXITSTATUS(status) == 0);394 `395 return success397 func main(codepoint|c:Int32?=none, text|t:Text?=none, theme|T:Theme?=none)398 C_code `399 static const char unicode_table[] = {400 #embed "../UnicodeData.txt"401 ,0,402 };403 `406 set_mode(TUI)407 hide_cursor()409 viewer := TableViewer(table_lines, theme=theme or Theme.guess())411 # Set cursor position according to CLI flags412 if c := codepoint413 start_text := c.hex(digits=4, prefix=no) ++ ";"414 for i,line in viewer.entries415 if line.starts_with(start_text)416 viewer.set_cursor(i)417 stop418 else if t := text419 for i,line in viewer.entries420 entry := UnicodeEntry.parse(line) or skip421 if t.starts_with(entry.text or skip)422 viewer.set_cursor(i)423 stop425 viewer.draw()426 while not viewer.quit427 prev := viewer428 viewer.update()429 if viewer != prev430 viewer.draw()432 disable()