contentlink.go RawHistoryFolder

1package wayland
2
3import (
4	"slices"
5	"sync/atomic"
6
7	"github.com/bnema/neferwl/internal/ports"
8)
9
10// batchesPerLink is how many batches circulate between wayland and one
11// output: one wayland fills, one waits in the channel, one the output reads.
12const batchesPerLink = 3
13
14// batchItems is the initial capacity of a batch: windows beyond it grow the
15// batch once, and it keeps the capacity.
16const batchItems = 8
17
18// contentLink is the content path to one running output. The display
19// goroutine owns cur and the sending side of in; the output owns the batch it
20// received until it returns it. free holds the batches nobody uses: it has
21// room for all of them, so Return never blocks.
22//
23// The link is also the ports.ContentReturn handed to the output, and the
24// handle DetachOutput takes: a detach removes this link and no other, even
25// when a later attach of the same name replaced it.
26type contentLink struct {
27	s    *Server
28	name string
29	in   chan *ports.ContentBatch
30	free chan *ports.ContentBatch
31	cur  *ports.ContentBatch
32	// pending: cur holds contents that could not be sent yet (the channel is
33	// full, or every batch is out). Return only wakes the display when it is
34	// set, so a steady stream of frames costs the display no extra wake.
35	pending atomic.Bool
36}
37
38// Return gives a consumed (reset) batch back and, when contents wait for one,
39// has the display goroutine send them. Safe from any goroutine, also after
40// the link was detached: the batch only goes back to its own link.
41func (l *contentLink) Return(b *ports.ContentBatch) {
42	l.free <- b // never full: the link owns batchesPerLink batches
43	// The display stores pending before it looks at free once more, so
44	// either it sees this batch or this load sees pending.
45	if l.pending.Load() && l.s.flushPosted.CompareAndSwap(false, true) {
46		l.s.display.Post(l.s.flushLinksFn)
47	}
48}
49
50// add puts c in the batch being filled, replacing the window's earlier
51// content: the output only needs the newest one (see ports.ContentBatch).
52func (l *contentLink) add(c ports.SurfaceContent) {
53	items := l.cur.Items
54	for i := range items {
55		if items[i].ID == c.ID {
56			items[i] = c
57			return
58		}
59	}
60	l.cur.Items = append(items, c)
61}
62
63// try sends the batch being filled when the channel is empty and a batch is
64// free to fill next. Display goroutine only.
65func (l *contentLink) try() bool {
66	if len(l.in) != 0 {
67		return false
68	}
69	select {
70	case next := <-l.free:
71		l.in <- l.cur // empty, and only this goroutine sends
72		l.cur = next
73		return true
74	default:
75		return false
76	}
77}
78
79// flushLink sends l's batch when the output can take it; otherwise l keeps
80// filling it and asks Return for a wake.
81func (s *Server) flushLink(l *contentLink) {
82	if len(l.cur.Items) == 0 {
83		l.pending.Store(false)
84		return
85	}
86	if l.try() {
87		l.pending.Store(false)
88		return
89	}
90	l.pending.Store(true)
91	if l.try() { // a Return may have slipped in before pending was set
92		l.pending.Store(false)
93	}
94}
95
96// flushLinks retries every link that waits for a free batch. Display
97// goroutine; posted by Return.
98func (s *Server) flushLinks() {
99	s.flushPosted.Store(false)
100	for _, l := range s.links {
101		if l.pending.Load() {
102			s.flushLink(l)
103		}
104	}
105}
106
107// attachOutput starts feeding the output called name; a link of the same name
108// is replaced (dropped from the feed: its handle then detaches nothing). The
109// returned ContentReturn is the link, and the handle for detachOutput. The
110// first batch holds the latest content of every live window,
111// so the output starts with all of them drawn. Display goroutine, or before
112// Run.
113func (s *Server) attachOutput(name string) (<-chan *ports.ContentBatch, ports.ContentReturn) {
114	for i, old := range s.links {
115		if old.name == name {
116			s.links = slices.Delete(s.links, i, i+1)
117			break
118		}
119	}
120	l := &contentLink{
121		s:    s,
122		name: name,
123		in:   make(chan *ports.ContentBatch, 1),
124		free: make(chan *ports.ContentBatch, batchesPerLink),
125	}
126	for range batchesPerLink - 1 {
127		l.free <- &ports.ContentBatch{Items: make([]ports.SurfaceContent, 0, batchItems)}
128	}
129	l.cur = &ports.ContentBatch{Items: make([]ports.SurfaceContent, 0, max(batchItems, len(s.latest)))}
130	for _, c := range s.latest {
131		l.add(c)
132	}
133	s.links = append(s.links, l)
134	s.flushLink(l)
135	return l.in, l
136}
137
138// detachOutput stops feeding the link ret, the handle attachOutput returned.
139// A handle whose link was replaced by a later attach of its name, or that is
140// not a link, detaches nothing: a newer link is never removed. Display
141// goroutine, or before Run. The output may still Return its last batch.
142func (s *Server) detachOutput(ret ports.ContentReturn) {
143	l, ok := ret.(*contentLink)
144	if !ok {
145		return
146	}
147	if i := slices.Index(s.links, l); i >= 0 {
148		s.links = slices.Delete(s.links, i, i+1)
149	}
150}
151
152// AttachOutput starts feeding the output called name with the contents of
153// the windows, as batches on the returned channel, starting with the current
154// content of every window. The output hands each batch back, reset, to the
155// returned ContentReturn, which is also the handle for DetachOutput.
156// Attaching a name that already has a feed replaces that feed. Call it from
157// outside the display goroutine. It returns a nil channel and handle when
158// the server is stopped.
159func (s *Server) AttachOutput(name string) (<-chan *ports.ContentBatch, ports.ContentReturn) {
160	var in <-chan *ports.ContentBatch
161	var ret ports.ContentReturn
162	if !s.display.Do(func() { in, ret = s.attachOutput(name) }) {
163		return nil, nil
164	}
165	return in, ret
166}
167
168// DetachOutput stops the feed ret, returned by AttachOutput. Only that feed
169// stops: if a later AttachOutput of the same name replaced it, the newer
170// feed keeps running. Call it from outside the display goroutine, once the
171// output no longer reads its channel. A nil ret does nothing.
172func (s *Server) DetachOutput(ret ports.ContentReturn) {
173	if ret == nil {
174		return
175	}
176	s.display.Do(func() { s.detachOutput(ret) })
177}

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 23

Tags 10

↑↓ moveenter openesc close