From 6e5594909413fc160af2170f272f3670af9a7665 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 2 May 2024 18:59:54 +0530 Subject: [PATCH] Start work on list-fonts kitten --- kitty/fonts/list.py | 89 ++++-------------- tools/cmd/list_fonts/family_list.go | 130 +++++++++++++++++++++++++++ tools/cmd/list_fonts/main.go | 65 ++++++++++++++ tools/cmd/list_fonts/types.go | 62 +++++++++++++ tools/cmd/list_fonts/ui.go | 135 ++++++++++++++++++++++++++++ tools/cmd/tool/main.go | 3 + 6 files changed, 411 insertions(+), 73 deletions(-) create mode 100644 tools/cmd/list_fonts/family_list.go create mode 100644 tools/cmd/list_fonts/main.go create mode 100644 tools/cmd/list_fonts/types.go create mode 100644 tools/cmd/list_fonts/ui.go diff --git a/kitty/fonts/list.py b/kitty/fonts/list.py index 16f04d4d4..b93b0488b 100644 --- a/kitty/fonts/list.py +++ b/kitty/fonts/list.py @@ -1,12 +1,9 @@ #!/usr/bin/env python # License: GPL v3 Copyright: 2017, Kovid Goyal -import sys from typing import Dict, List, Sequence -from kittens.tui.operations import styled from kitty.constants import is_macos -from kitty.types import run_once from . import ListedFont from .common import get_variable_data_for_descriptor @@ -17,39 +14,6 @@ else: from .fontconfig import list_fonts, prune_family_group -@run_once -def isatty() -> bool: - return sys.stdout.isatty() - - -def title(x: str) -> str: - if isatty(): - return styled(x, fg='green', bold=True) - return x - - -def italic(x: str) -> str: - if isatty(): - return styled(x, italic=True) - return x - - -def variable_font_label(x: str) -> str: - if isatty(): - return styled(x, fg='yellow') - return x - - -def variable_font_tag(x: str) -> str: - if isatty(): - return styled(x, fg='cyan') - return x - - -def indented(x: str, level: int = 1) -> str: - return ' ' * level + x - - def create_family_groups(monospaced: bool = True) -> Dict[str, List[ListedFont]]: g: Dict[str, List[ListedFont]] = {} for f in list_fonts(): @@ -58,43 +22,22 @@ def create_family_groups(monospaced: bool = True) -> Dict[str, List[ListedFont]] return {k: prune_family_group(v) for k, v in g.items()} -def show_variable(f: ListedFont, psnames: bool) -> None: - vd = get_variable_data_for_descriptor(f['descriptor']) - p = italic(vd['variations_postscript_name_prefix'] or f['family']) - p = f"{p} {variable_font_label('Variable font')}" - print(indented(p)) - print(indented(variable_font_label('Axes of variation'), level=2)) - for a in vd['axes']: - t = variable_font_tag(a['tag']) - n = a['strid'] or '' - if n: - t += f' ({n})' - print(indented(t, level=3) + ':', f'minimum={a["minimum"]:g}', f'maximum={a["maximum"]:g}', f'default={a["default"]:g}') - - if vd['named_styles']: - print(indented(variable_font_label('Named styles'), level=2)) - for ns in vd['named_styles']: - name = ns['name'] or '' - if psnames: - name += f' ({ns["psname"] or ""})' - axes = [] - for axis_tag, val in ns['axis_values'].items(): - axes.append(f'{axis_tag}={val:g}') - p = name + ': ' + ' '.join(axes) - print(indented(p, level=3)) +def as_json() -> str: + import json + groups = create_family_groups() + for g in groups.values(): + for f in g: + if f['is_variable']: + f['variable_data'] = get_variable_data_for_descriptor(f['descriptor']) + return json.dumps(groups, indent=2) def main(argv: Sequence[str]) -> None: - psnames = '--psnames' in argv - groups = create_family_groups() - for k in sorted(groups, key=lambda x: x.lower()): - print(title(k)) - for f in sorted(groups[k], key=lambda x: x['full_name'].lower()): - if f['is_variable']: - show_variable(f, psnames) - continue - p = italic(f['full_name']) - if psnames: - p += ' ({})'.format(f['postscript_name']) - print(indented(p)) - print() + import subprocess + + from kitty.constants import kitten_exe + argv = list(argv) + if '--psnames' in argv: + argv.remove('--psnames') + cp = subprocess.run([kitten_exe(), '__list_fonts__'], input=as_json().encode()) + raise SystemExit(cp.returncode) diff --git a/tools/cmd/list_fonts/family_list.go b/tools/cmd/list_fonts/family_list.go new file mode 100644 index 000000000..f925644af --- /dev/null +++ b/tools/cmd/list_fonts/family_list.go @@ -0,0 +1,130 @@ +package list_fonts + +import ( + "fmt" + "kitty/tools/tui/subseq" + "kitty/tools/utils" + "kitty/tools/wcswidth" +) + +var _ = fmt.Print + +type FamilyList struct { + families, all_families []string + current_search string + display_strings []string + widths []int + max_width, current_idx int +} + +func (self *FamilyList) Len() int { + return len(self.families) +} + +func (self *FamilyList) Next(delta int, allow_wrapping bool) bool { + if len(self.display_strings) == 0 { + return false + } + idx := self.current_idx + delta + if !allow_wrapping && (idx < 0 || idx > self.Len()) { + return false + } + for idx < 0 { + idx += self.Len() + } + self.current_idx = idx % self.Len() + return true +} + +func limit_lengths(text string) string { + t, x := wcswidth.TruncateToVisualLengthWithWidth(text, 31) + if x >= len(text) { + return text + } + return t + "…" +} + +func match(expression string, items []string) []*subseq.Match { + matches := subseq.ScoreItems(expression, items, subseq.Options{Level1: " "}) + matches = utils.StableSort(matches, func(a, b *subseq.Match) int { + if b.Score < a.Score { + return -1 + } + if b.Score > a.Score { + return 1 + } + return 0 + }) + return matches +} + +const ( + MARK_BEFORE = "\033[33m" + MARK_AFTER = "\033[39m" +) + +func apply_search(families []string, expression string, marks ...string) []string { + mark_before, mark_after := MARK_BEFORE, MARK_AFTER + if len(marks) == 2 { + mark_before, mark_after = marks[0], marks[1] + } + results := utils.Filter(match(expression, families), func(x *subseq.Match) bool { return x.Score > 0 }) + ans := make([]string, 0, len(results)) + for _, m := range results { + text := m.Text + positions := m.Positions + for i := len(positions) - 1; i >= 0; i-- { + p := positions[i] + text = text[:p] + mark_before + text[p:p+1] + mark_after + text[p+1:] + } + ans = append(ans, text) + } + return ans +} + +func (self *FamilyList) UpdateFamilies(families []string) { + self.families, self.families = families, families + if self.current_search != "" { + self.display_strings = utils.Map(limit_lengths, apply_search(self.families, self.current_search)) + } else { + self.display_strings = utils.Map(limit_lengths, families) + } + self.widths = utils.Map(wcswidth.Stringwidth, self.display_strings) + self.max_width = utils.Max(0, self.widths...) + self.current_idx = 0 +} + +func (self *FamilyList) UpdateSearch(query string) bool { + if query == self.current_search || len(self.all_families) == 0 { + return false + } + self.current_search = query + self.UpdateFamilies(self.all_families) + return true +} + +type Line struct { + text string + width int + is_current bool +} + +func (self *FamilyList) Lines(num_rows int) []Line { + if num_rows < 1 { + return nil + } + ans := make([]Line, 0, len(self.display_strings)) + before_num := utils.Min(self.current_idx, num_rows-1) + start := self.current_idx - before_num + for i := start; i < utils.Min(start+num_rows, len(self.display_strings)); i++ { + ans = append(ans, Line{self.display_strings[i], self.widths[i], i == self.current_idx}) + } + return ans +} + +func (self *FamilyList) CurrentFamily() string { + if self.current_idx >= 0 && self.current_idx < len(self.families) { + return self.families[self.current_idx] + } + return "" +} diff --git a/tools/cmd/list_fonts/main.go b/tools/cmd/list_fonts/main.go new file mode 100644 index 000000000..f911d0db0 --- /dev/null +++ b/tools/cmd/list_fonts/main.go @@ -0,0 +1,65 @@ +package list_fonts + +import ( + "encoding/json" + "fmt" + "os" + + "kitty/tools/cli" + "kitty/tools/tty" + "kitty/tools/tui/loop" +) + +var _ = fmt.Print +var debugprintln = tty.DebugPrintln + +func main() (rc int, err error) { + d := json.NewDecoder(os.Stdin) + var fonts map[string][]ListedFont + if err = d.Decode(&fonts); err != nil { + return 1, err + } + lp, err := loop.New() + if err != nil { + return 1, err + } + h := &handler{lp: lp, fonts: fonts} + lp.OnInitialize = func() (string, error) { + lp.AllowLineWrapping(false) + lp.SetWindowTitle(`Choose a font for kitty`) + h.initialize() + return "", nil + } + lp.OnWakeup = h.on_wakeup + lp.OnFinalize = func() string { + h.finalize() + lp.SetCursorVisible(true) + return `` + } + lp.OnResize = func(_, _ loop.ScreenSize) error { + return h.draw_screen() + } + lp.OnKeyEvent = h.on_key_event + lp.OnText = h.on_text + err = lp.Run() + if err != nil { + return 1, err + } + ds := lp.DeathSignalName() + if ds != "" { + fmt.Println("Killed by signal: ", ds) + lp.KillIfSignalled() + return 1, nil + } + return lp.ExitCode(), nil +} + +func EntryPoint(root *cli.Command) { + root = root.AddSubCommand(&cli.Command{ + Name: "__list_fonts__", + Hidden: true, + Run: func(cmd *cli.Command, args []string) (rc int, err error) { + return main() + }, + }) +} diff --git a/tools/cmd/list_fonts/types.go b/tools/cmd/list_fonts/types.go new file mode 100644 index 000000000..928c928c4 --- /dev/null +++ b/tools/cmd/list_fonts/types.go @@ -0,0 +1,62 @@ +package list_fonts + +import ( + "fmt" +) + +var _ = fmt.Print + +type VariableAxis struct { + Minimum float64 `json:"minimum"` + Maximum float64 `json:"maximum"` + Default float64 `json:"default"` + Hidden bool `json:"hidden"` + Tag string `json:"tag"` + Strid string `json:"strid"` +} + +type NamedStyle struct { + Axis_values map[string]float64 `json:"axis_values"` + Name string `json:"name"` + Postscript_name string `json:"psname"` +} + +type DesignAxis struct { + Format int `json:"format"` + Flags int `json:"flags"` + Name string `json:"name"` + Value float64 `json:"value"` + Minimum float64 `json:"minimum"` + Maximum float64 `json:"maximum"` + Linked_value float64 `json:"linked_value"` +} + +type AxisValue struct { + Design_index int `json:"design_index"` + Value float64 `json:"value"` +} + +type MultiAxisStyle struct { + Flags int `json:"flags"` + Name string `json:"name"` + Values []AxisValue `json:"values"` +} + +type VariableData struct { + Axes []VariableAxis `json:"axes"` + Named_styles []NamedStyle `json:"named_styles"` + Variations_postscript_name_prefix string `json:"variations_postscript_name_prefix"` + Elided_fallback_name string `json:"elided_fallback_name"` + Design_axes []DesignAxis `json:"design_axes"` + Multi_axis_styles []MultiAxisStyle `json:"multi_axis_styles"` +} + +type ListedFont struct { + Family string `json:"family"` + Fullname string `json:"full_name"` + Postscript_name string `json:"postscript_name"` + Is_monospace bool `json:"is_monospace"` + Is_variable bool `json:"is_variable"` + Variable_data VariableData `json:"variable_data"` + Descriptor map[string]any `json:"descriptor"` +} diff --git a/tools/cmd/list_fonts/ui.go b/tools/cmd/list_fonts/ui.go new file mode 100644 index 000000000..a4d55e053 --- /dev/null +++ b/tools/cmd/list_fonts/ui.go @@ -0,0 +1,135 @@ +package list_fonts + +import ( + "fmt" + "strings" + + "kitty/tools/tui/loop" + "kitty/tools/tui/readline" + "kitty/tools/utils" + + "golang.org/x/exp/maps" +) + +var _ = fmt.Print + +type State int + +const ( + LISTING_FAMILIES State = iota + CHOOSING_FACES +) + +type handler struct { + lp *loop.Loop + fonts map[string][]ListedFont + all_font_families []string + state State + + // Listing + current_font_families []string + rl *readline.Readline +} + +// Listing families {{{ +func (h *handler) draw_search_bar() { + h.lp.SetCursorVisible(true) + sz, err := h.lp.ScreenSize() + if err != nil { + return + } + h.lp.MoveCursorTo(1, int(sz.HeightCells)) + h.lp.ClearToEndOfLine() + h.rl.RedrawNonAtomic() +} + +func (h *handler) draw_listing_screen() (err error) { + sz, err := h.lp.ScreenSize() + if err != nil { + return err + } + _ = sz + h.draw_search_bar() + return +} + +func (h *handler) update_family_search() { + text := h.rl.AllText() + _ = text +} + +func (h *handler) handle_listing_key_event(event *loop.KeyEvent) (err error) { + if event.MatchesPressOrRepeat("ctrl+c") || event.MatchesPressOrRepeat("esc") { + h.lp.Quit(1) + event.Handled = true + return + } + if err = h.rl.OnKeyEvent(event); err != nil { + if err == readline.ErrAcceptInput { + return nil + } + return err + } + if event.Handled { + h.update_family_search() + } + h.draw_search_bar() + return +} + +func (h *handler) handle_listing_text(text string, from_key_event bool, in_bracketed_paste bool) (err error) { + if err = h.rl.OnText(text, from_key_event, in_bracketed_paste); err != nil { + return err + } + h.draw_screen() + return +} + +// }}} + +// Events {{{ +func (h *handler) initialize() { + h.lp.SetCursorVisible(false) + h.all_font_families = utils.StableSortWithKey(maps.Keys(h.fonts), strings.ToLower) + h.current_font_families = h.all_font_families + h.rl = readline.New(h.lp, readline.RlInit{DontMarkPrompts: true, Prompt: "Family: "}) + h.draw_screen() +} + +func (h *handler) finalize() { + h.lp.SetCursorVisible(true) +} + +func (h *handler) draw_screen() (err error) { + h.lp.StartAtomicUpdate() + defer h.lp.EndAtomicUpdate() + h.lp.ClearScreen() + h.lp.AllowLineWrapping(false) + switch h.state { + case LISTING_FAMILIES: + return h.draw_listing_screen() + } + return +} + +func (h *handler) on_wakeup() (err error) { + return +} + +func (h *handler) on_key_event(event *loop.KeyEvent) (err error) { + switch h.state { + case LISTING_FAMILIES: + return h.handle_listing_key_event(event) + } + return +} + +func (h *handler) on_text(text string, from_key_event bool, in_bracketed_paste bool) (err error) { + switch h.state { + case LISTING_FAMILIES: + return h.handle_listing_text(text, from_key_event, in_bracketed_paste) + } + return +} + +// }}} diff --git a/tools/cmd/tool/main.go b/tools/cmd/tool/main.go index 51a25b026..c226fd06e 100644 --- a/tools/cmd/tool/main.go +++ b/tools/cmd/tool/main.go @@ -20,6 +20,7 @@ import ( "kitty/tools/cmd/at" "kitty/tools/cmd/benchmark" "kitty/tools/cmd/edit_in_kitty" + "kitty/tools/cmd/list_fonts" "kitty/tools/cmd/mouse_demo" "kitty/tools/cmd/pytest" "kitty/tools/cmd/run_shell" @@ -78,6 +79,8 @@ func KittyToolEntryPoints(root *cli.Command) { show_error.EntryPoint(root) // __pytest__ pytest.EntryPoint(root) + // __list_fonts__ + list_fonts.EntryPoint(root) // __hold_till_enter__ root.AddSubCommand(&cli.Command{ Name: "__hold_till_enter__",