Start work on list-fonts kitten

This commit is contained in:
Kovid Goyal
2024-05-02 18:59:54 +05:30
parent 21f824e825
commit 6e55949094
6 changed files with 411 additions and 73 deletions

View File

@@ -1,12 +1,9 @@
#!/usr/bin/env python
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
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)

View File

@@ -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 ""
}

View File

@@ -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()
},
})
}

View File

@@ -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"`
}

135
tools/cmd/list_fonts/ui.go Normal file
View File

@@ -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
}
// }}}

View File

@@ -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__",