main 7a3b92c

input.go RawHistoryFolder

1package bar
2
3import (
4	"math"
5	"slices"
6	"strconv"
7	"time"
8
9	"github.com/bnema/neferclient"
10
11	"github.com/bnema/neferbar/internal/layout"
12	"github.com/bnema/neferbar/internal/module"
13	"github.com/bnema/neferbar/internal/popup"
14)
15
16// Evdev button codes.
17const (
18	btnLeft   = 0x110
19	btnRight  = 0x111
20	btnMiddle = 0x112
21)
22
23// pxPerStep is how far a continuous scroll (touchpad) moves for one step.
24const pxPerStep = 15
25
26// press is the latest button press sent to a module.
27type press struct {
28	m      *module.Module // the module the press was sent to
29	token  uint32         // the number the module echoes to refer to this press
30	serial uint32         // the pointer serial of the press, for popup grabs
31	at     time.Time
32	used   bool // a menu already answered this press
33}
34
35// menuPressTTL is how long after a press a module may still answer it with a
36// menu.
37const menuPressTTL = 5 * time.Second
38
39// takeMenuPress accepts a menu that module m opens in answer to the press
40// numbered token: it must be the latest press, sent to m, not older than
41// menuPressTTL, and not answered yet. The serial of the press is what the
42// popup grabs with. A press answers one menu.
43func (p *pointerState) takeMenuPress(m *module.Module, token uint32, now time.Time) (serial uint32, ok bool) {
44	pr := &p.press
45	if m == nil || pr.m != m || token == 0 || pr.token != token || pr.used || pr.serial == 0 || now.Sub(pr.at) > menuPressTTL {
46		return 0, false
47	}
48	pr.used = true
49	return pr.serial, true
50}
51
52// pointerState turns pointer events over the bar into the lines of the module
53// protocol. It owns no Wayland object: the bar loop feeds it columns.
54type pointerState struct {
55	hover    *module.Module // the interactive module under the pointer
56	hoverOff int            // its cell column
57	hoverGen uint64         // hover.Gen() when the hover line was sent
58	token    uint32         // last click token
59	press    press
60	wheel    wheelAcc
61	buf      [64]byte // the line being built
62}
63
64// send hands a line built in p.buf to m. A full queue drops the line: the
65// script is not reading and pointer events are not worth waiting for.
66func (p *pointerState) send(m *module.Module, line []byte) { m.Send(line) }
67
68// line starts a line in p.buf.
69func (p *pointerState) line(verb string) []byte { return append(p.buf[:0], verb...) }
70
71func appendInt(b []byte, v int) []byte {
72	b = append(b, ' ')
73	return strconv.AppendInt(b, int64(v), 10)
74}
75
76// forgetRestarted drops the hover of a module whose script restarted: the new
77// process was never told about it, so it gets no leave, and the next motion
78// sends it a hover.
79func (p *pointerState) forgetRestarted() {
80	if p.hover != nil && p.hover.Gen() != p.hoverGen {
81		p.hover, p.hoverOff = nil, 0
82	}
83}
84
85// motion handles Enter and Motion at cell column col of the bar row.
86func (p *pointerState) motion(lay *layout.Layout, col int) {
87	p.forgetRestarted()
88	m, off := interactiveAt(lay, col)
89	if m == p.hover && (m == nil || off == p.hoverOff) {
90		return
91	}
92	if p.hover != nil && m != p.hover {
93		p.send(p.hover, p.line("leave"))
94	}
95	p.hover, p.hoverOff = m, off
96	if m != nil {
97		p.hoverGen = m.Gen()
98		p.send(m, appendInt(p.line("hover"), off))
99	}
100}
101
102// leave handles the pointer leaving the bar.
103func (p *pointerState) leave() {
104	p.forgetRestarted()
105	if p.hover != nil {
106		p.send(p.hover, p.line("leave"))
107	}
108	p.hover, p.hoverOff = nil, 0
109	p.wheel.reset()
110}
111
112// button handles a button press at column col. serial is the press serial.
113func (p *pointerState) button(lay *layout.Layout, col int, code uint32, serial uint32) {
114	var name string
115	switch code {
116	case btnLeft:
117		name = "left"
118	case btnMiddle:
119		name = "middle"
120	case btnRight:
121		name = "right"
122	default:
123		return
124	}
125	m, off := interactiveAt(lay, col)
126	if m == nil {
127		return
128	}
129	p.token++
130	p.press = press{m: m, token: p.token, serial: serial, at: time.Now()}
131	b := append(p.line("click "), name...)
132	b = appendInt(b, off)
133	b = append(b, ' ')
134	b = strconv.AppendUint(b, uint64(p.token), 10)
135	p.send(m, b)
136}
137
138// axis handles one scroll event at column col.
139func (p *pointerState) axis(lay *layout.Layout, col int, axis uint32, value120 int32, delta float64) {
140	steps := p.wheel.add(axis, value120, delta)
141	if steps == 0 {
142		return
143	}
144	m, off := interactiveAt(lay, col)
145	if m == nil {
146		return
147	}
148	var dir string
149	switch {
150	case axis == neferclient.AxisVertical && steps > 0:
151		dir = "down"
152	case axis == neferclient.AxisVertical:
153		dir = "up"
154	case steps > 0:
155		dir = "right"
156	default:
157		dir = "left"
158	}
159	if steps < 0 {
160		steps = -steps
161	}
162	b := append(p.line("scroll "), dir...)
163	b = appendInt(b, int(steps))
164	b = appendInt(b, off)
165	p.send(m, b)
166}
167
168// interactiveAt is the interactive module at column col, and the column inside it.
169func interactiveAt(lay *layout.Layout, col int) (*module.Module, int) {
170	m, off, ok := lay.At(col)
171	if !ok || !m.Interactive {
172		return nil, 0
173	}
174	return m, off
175}
176
177// wheelAcc counts scroll steps. A wheel reports notches in 1/120 units; a
178// touchpad reports pixels. A compositor may send both for a wheel, so once a
179// 1/120 value was seen the pixels are ignored until the pointer leaves.
180type wheelAcc struct {
181	wheel bool
182	v120  [2]int32
183	px    [2]float64
184}
185
186func (a *wheelAcc) reset() { *a = wheelAcc{} }
187
188// stop ends a touchpad scroll on axis: the pixels left over do not carry into
189// the next gesture.
190func (a *wheelAcc) stop(axis uint32) {
191	if axis <= 1 {
192		a.px[axis] = 0
193	}
194}
195
196// cellColumn is the cell column under logical x on a surface of the given
197// scale, for cells cellW physical pixels wide.
198func cellColumn(x, scale float64, cellW int) int {
199	return int(math.Floor(x * scale / float64(cellW)))
200}
201
202// add accumulates one event and returns the whole steps it completed:
203// positive for down or right, negative for up or left.
204func (a *wheelAcc) add(axis uint32, value120 int32, delta float64) int32 {
205	if axis > 1 {
206		return 0
207	}
208	if value120 != 0 {
209		a.wheel = true
210		a.v120[axis] += value120
211		steps := a.v120[axis] / 120
212		a.v120[axis] -= steps * 120
213		return steps
214	}
215	if a.wheel {
216		return 0
217	}
218	a.px[axis] += delta
219	steps := int32(a.px[axis] / pxPerStep)
220	a.px[axis] -= float64(steps) * pxPerStep
221	return steps
222}
223
224// inputRect is the logical-pixel rectangle of a span of cells: rounded
225// outward, full surface height.
226func inputRect(start, width, cellW int, scale float64, height int32) neferclient.Rect {
227	return popup.CellRect(start, width, cellW, scale, height)
228}
229
230// inputKey is everything but the spans that the input rectangles depend on.
231type inputKey struct {
232	cellW  int
233	scale  float64
234	height int32
235}
236
237// inputRegion tracks the spans the compositor was last told about.
238type inputRegion struct {
239	spans, last []layout.Span
240	rects       []neferclient.Rect
241	key         inputKey
242}
243
244// reset forgets what the compositor knows: a new surface starts click-through.
245func (r *inputRegion) reset() {
246	r.last = r.last[:0]
247	r.key = inputKey{}
248}
249
250// update collects the spans of the interactive modules and reports whether the
251// input rectangles differ from the last ones; if so rects holds the new ones
252// (non-nil, possibly empty). It allocates only while the slices grow.
253func (r *inputRegion) update(lay *layout.Layout, key inputKey) (rects []neferclient.Rect, changed bool) {
254	r.spans = lay.Spans(r.spans[:0])
255	r.spans = slices.DeleteFunc(r.spans, func(s layout.Span) bool { return !s.M.Interactive })
256	if key == r.key && slices.Equal(r.spans, r.last) {
257		return nil, false
258	}
259	if len(r.spans) == 0 && len(r.last) == 0 {
260		r.key = key
261		return nil, false
262	}
263	r.rects = r.rects[:0]
264	for _, s := range r.spans {
265		r.rects = append(r.rects, inputRect(s.Start, s.Width, key.cellW, key.scale, key.height))
266	}
267	if r.rects == nil {
268		r.rects = []neferclient.Rect{}
269	}
270	r.last = append(r.last[:0], r.spans...)
271	r.key = key
272	return r.rects, true
273}

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