detached 7a3b92c

names.go RawHistoryFolder

1package glyph
2
3import (
4	"bytes"
5	"encoding/binary"
6	"fmt"
7	"slices"
8
9	"golang.org/x/image/font/sfnt"
10	"golang.org/x/sys/unix"
11)
12
13// Names maps the glyph names stored in a font file to the runes that draw
14// them, such as "fa-steam" to U+F1B6 in a Nerd Font. Only the private use
15// areas are indexed: that is where icon fonts put their glyphs.
16//
17// The names stay in the mapped font file; a lookup does not allocate.
18type Names struct {
19	data []byte
20	idx  []nameEntry // sorted by name
21}
22
23type nameEntry struct {
24	off, n uint32 // the name is data[off:off+n]
25	r      rune
26}
27
28// iconRanges are the private use areas: the BMP one and plane 15.
29var iconRanges = [...][2]rune{{0xE000, 0xF8FF}, {0xF0000, 0xFFFFD}}
30
31// LoadNames indexes the glyph names of the font file at path. A font without
32// glyph names (a version 3 "post" table) gives an empty index, not an error.
33func LoadNames(path string) (*Names, error) {
34	data, err := mapFile(path)
35	if err != nil {
36		return nil, fmt.Errorf("glyph: %w", err)
37	}
38	n := &Names{data: data}
39	f, err := sfnt.Parse(data)
40	if err != nil {
41		n.Close()
42		return nil, fmt.Errorf("glyph: parse %s: %w", path, err)
43	}
44	spans := postNames(data)
45	var buf sfnt.Buffer
46	for _, rg := range iconRanges {
47		for r := rg[0]; r <= rg[1]; r++ {
48			gi, err := f.GlyphIndex(&buf, r)
49			if err != nil || gi == 0 || int(gi) >= len(spans) {
50				continue
51			}
52			if s := spans[gi]; s.n > 0 {
53				n.idx = append(n.idx, nameEntry{off: s.off, n: s.n, r: r})
54			}
55		}
56	}
57	slices.SortFunc(n.idx, func(a, b nameEntry) int {
58		return bytes.Compare(n.data[a.off:a.off+a.n], n.data[b.off:b.off+b.n])
59	})
60	return n, nil
61}
62
63// Lookup returns the rune of the glyph called name.
64func (n *Names) Lookup(name string) (rune, bool) {
65	if n == nil {
66		return 0, false
67	}
68	lo, hi := 0, len(n.idx)
69	for lo < hi {
70		m := int(uint(lo+hi) >> 1)
71		e := n.idx[m]
72		// string(bytes) in a comparison does not allocate.
73		switch s := n.data[e.off : e.off+e.n]; {
74		case string(s) < name:
75			lo = m + 1
76		case string(s) > name:
77			hi = m
78		default:
79			return e.r, true
80		}
81	}
82	return 0, false
83}
84
85// Len returns the number of indexed names.
86func (n *Names) Len() int { return len(n.idx) }
87
88// Close unmaps the font file. n must not be used afterwards.
89func (n *Names) Close() {
90	if n != nil && n.data != nil {
91		_ = unix.Munmap(n.data)
92		n.data, n.idx = nil, nil
93	}
94}
95
96type span struct{ off, n uint32 }
97
98// postNames returns, for each glyph index, where its custom name sits in
99// data, from a version 2 "post" table. Glyphs with a standard Macintosh name
100// (index < 258) or no table at all give empty spans. Every offset is checked
101// against the file, so a damaged font gives fewer names, never a panic.
102func postNames(data []byte) []span {
103	u16 := func(off int) (int, bool) {
104		if off < 0 || off+2 > len(data) {
105			return 0, false
106		}
107		return int(binary.BigEndian.Uint16(data[off:])), true
108	}
109	u32 := func(off int) (uint32, bool) {
110		if off < 0 || off+4 > len(data) {
111			return 0, false
112		}
113		return binary.BigEndian.Uint32(data[off:]), true
114	}
115	tables, ok := u16(4)
116	if !ok {
117		return nil
118	}
119	post, end := -1, 0
120	for i := range tables {
121		rec := 12 + 16*i
122		if rec+16 > len(data) {
123			return nil
124		}
125		if string(data[rec:rec+4]) == "post" {
126			off, _ := u32(rec + 8)
127			n, _ := u32(rec + 12)
128			if uint64(off)+uint64(n) > uint64(len(data)) {
129				return nil
130			}
131			post, end = int(off), int(off)+int(n)
132			break
133		}
134	}
135	if v, ok := u32(post); !ok || v != 0x00020000 {
136		return nil
137	}
138	glyphs, ok := u16(post + 32)
139	if !ok || post+34+2*glyphs > end {
140		return nil
141	}
142	// The custom names follow the index array as Pascal strings.
143	var custom []uint32
144	for o := post + 34 + 2*glyphs; o < end; o += 1 + int(data[o]) {
145		if o+1+int(data[o]) > end {
146			break
147		}
148		custom = append(custom, uint32(o))
149	}
150	spans := make([]span, glyphs)
151	for g := range glyphs {
152		k, _ := u16(post + 34 + 2*g)
153		if k < 258 || k-258 >= len(custom) {
154			continue
155		}
156		o := custom[k-258]
157		spans[g] = span{off: o + 1, n: uint32(data[o])}
158	}
159	return spans
160}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 5

Tags 4

↑↓ moveenter openesc close