v0.4.0 b6be3a8

cursor.go RawHistoryFolder

1package app
2
3import (
4	"math"
5	"os"
6	"strconv"
7	"sync"
8
9	"github.com/bnema/neferwl/internal/adapters/xcursor"
10	"github.com/bnema/neferwl/internal/ports"
11)
12
13// cursorSize is XCURSOR_SIZE (logical pixels), 24 by default.
14func cursorSize() int {
15	if n, err := strconv.Atoi(os.Getenv("XCURSOR_SIZE")); err == nil && n >= 8 && n <= 256 {
16		return n
17	}
18	return 24
19}
20
21// legacyCursorNames are the X11 names of CSS cursors for themes without
22// the CSS ones.
23var legacyCursorNames = map[string][]string{
24	"default":     {"left_ptr"},
25	"pointer":     {"hand2", "hand1"},
26	"text":        {"xterm"},
27	"wait":        {"watch"},
28	"progress":    {"left_ptr_watch"},
29	"crosshair":   {"cross"},
30	"move":        {"fleur"},
31	"grab":        {"openhand"},
32	"grabbing":    {"closedhand"},
33	"not-allowed": {"crossed_circle"},
34	"help":        {"question_arrow"},
35	"ew-resize":   {"sb_h_double_arrow"},
36	"ns-resize":   {"sb_v_double_arrow"},
37	"col-resize":  {"sb_h_double_arrow"},
38	"row-resize":  {"sb_v_double_arrow"},
39	"e-resize":    {"right_side"},
40	"w-resize":    {"left_side"},
41	"n-resize":    {"top_side"},
42	"s-resize":    {"bottom_side"},
43	"ne-resize":   {"top_right_corner"},
44	"nw-resize":   {"top_left_corner"},
45	"se-resize":   {"bottom_right_corner"},
46	"sw-resize":   {"bottom_left_corner"},
47	"nesw-resize": {"fd_double_arrow"},
48	"nwse-resize": {"bd_double_arrow"},
49	"all-scroll":  {"fleur"},
50	"alias":       {"link"},
51	"cell":        {"plus"},
52	"no-drop":     {"crossed_circle"},
53}
54
55// themeCursors caches loaded theme cursors: clients change shapes on every
56// hover and each output loads them on its render goroutine.
57var themeCursors = struct {
58	sync.Mutex
59	m map[themeKey]ports.CursorImage
60}{m: map[themeKey]ports.CursorImage{}}
61
62type themeKey struct {
63	shape       string
64	size, limit int
65}
66
67// loadCursor returns the cursor a client asked for at an output scale, at
68// most limit physical pixels on a side: a XCURSOR_THEME shape at
69// XCURSOR_SIZE × scale, the client image resized to the output scale, or
70// an empty image when hidden, then rotated by the output transform t (the
71// cursor plane and the headless cursor are drawn in target space). Unknown
72// shapes fall back to the arrow.
73func loadCursor(c ports.CursorChange, scale float64, t ports.BufferTransform, limit int) (ports.CursorImage, error) {
74	img, err := loadCursorUpright(c, scale, limit)
75	if err != nil {
76		return img, err
77	}
78	return rotateCursor(img, t), nil
79}
80
81// rotateCursor returns img as an output with transform t holds it, hotspot
82// included. It never modifies img (theme images are cached).
83func rotateCursor(img ports.CursorImage, t ports.BufferTransform) ports.CursorImage {
84	if t == 0 || img.W <= 0 || img.H <= 0 || len(img.Pixels) < img.W*img.H*4 {
85		return img
86	}
87	dw, dh := t.Size(img.W, img.H)
88	out := make([]byte, dw*dh*4)
89	for y := range img.H {
90		for x := range img.W {
91			bx, by := t.ToBuffer(float64(x)+.5, float64(y)+.5, float64(img.W), float64(img.H))
92			src := (y*img.W + x) * 4
93			copy(out[(int(by)*dw+int(bx))*4:], img.Pixels[src:src+4])
94		}
95	}
96	hx, hy := t.ToBuffer(float64(img.HotX)+.5, float64(img.HotY)+.5, float64(img.W), float64(img.H))
97	return ports.CursorImage{W: dw, H: dh, HotX: int(hx), HotY: int(hy), Pixels: out}
98}
99
100func loadCursorUpright(c ports.CursorChange, scale float64, limit int) (ports.CursorImage, error) {
101	scale = max(scale, 1)
102	if c.Hidden {
103		return ports.CursorImage{}, nil
104	}
105	if c.Image != nil {
106		img := xcursor.Image{W: c.Image.W, H: c.Image.H, HotX: c.Image.HotX, HotY: c.Image.HotY, Pixels: c.Image.Pixels}
107		f := scale / float64(max(c.Scale, 1))
108		w, h := int(math.Round(float64(img.W)*f)), int(math.Round(float64(img.H)*f))
109		if big := max(w, h); big > limit {
110			w, h = w*limit/big, h*limit/big
111		}
112		img = img.Scale(max(w, 1), max(h, 1))
113		return ports.CursorImage{W: img.W, H: img.H, HotX: img.HotX, HotY: img.HotY, Pixels: img.Pixels}, nil
114	}
115	shape := c.Shape
116	if shape == "" {
117		shape = "default"
118	}
119	size := int(math.Round(float64(cursorSize()) * scale))
120	key := themeKey{shape, size, limit}
121	themeCursors.Lock()
122	defer themeCursors.Unlock()
123	if img, ok := themeCursors.m[key]; ok {
124		return img, nil
125	}
126	// The shape is looked up through the whole theme chain before the
127	// arrow: a theme may override only the arrow and inherit the rest.
128	dirs, theme := xcursor.SearchPath(), os.Getenv("XCURSOR_THEME")
129	img, err := xcursor.Load(dirs, theme, append([]string{shape}, legacyCursorNames[shape]...), size)
130	if err != nil && shape != "default" {
131		img, err = xcursor.Load(dirs, theme, []string{"default", "left_ptr"}, size)
132	}
133	if err != nil {
134		return ports.CursorImage{}, err
135	}
136	img = img.ForSize(size, limit)
137	out := ports.CursorImage{W: img.W, H: img.H, HotX: img.HotX, HotY: img.HotY, Pixels: img.Pixels}
138	themeCursors.m[key] = out
139	return out, nil
140}

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 25

Tags 10

↑↓ moveenter openesc close