// License: GPLv3 Copyright: 2023, Kovid Goyal, package unicode_input import ( "fmt" "strconv" "strings" "kitty/tools/unicode_names" "kitty/tools/utils" "kitty/tools/utils/style" "kitty/tools/wcswidth" "golang.org/x/exp/slices" ) 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, 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.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 if self.codepoints != nil { slices.Sort(self.codepoints) } self.mode = mode self.layout_dirty = true if current_idx > -1 && current_idx < len(self.codepoints) { self.current_idx = current_idx } 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 title(x string) string { if len(x) > 1 { x = strings.ToUpper(x[:1]) + x[1:] } return x } 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: title(unicode_names.NameForCodePoint(codepoint))} } cell = func(i int, cd cell_data) { is_current := i == self.current_idx text := self.green(cd.idx) + " " + cd.ch + " " w := wcswidth.Stringwidth(cd.ch) if w < 2 { text += strings.Repeat(" ", (2 - w)) } desc_width := wcswidth.Stringwidth(cd.desc) if desc_width > space_for_desc { text += cd.desc[:space_for_desc-1] + "…" } else { text += cd.desc extra := space_for_desc - desc_width if extra > 0 { text += strings.Repeat(" ", extra) } } if is_current { text = self.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 } }