v0.1.0 d9218d6

outputs.go RawHistoryFolder

1package app
2
3import (
4	"context"
5	"sync"
6
7	"github.com/bnema/neferwl/internal/ports"
8)
9
10// outputRun drives one output until ctx ends: it renders the scenes and
11// surface contents it receives, with the cursor the client asks for.
12type outputRun func(ctx context.Context, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent, cursor <-chan ports.CursorChange) error
13
14// outputSet owns the running outputs and feeds them. Core sends one scene
15// per output; each goes to its output, latest first. Surface contents go to
16// every output, since windows move between outputs; the latest content of
17// each window is kept so a new output starts with every window drawn.
18// Only the goroutine running loop touches the set.
19type outputSet struct {
20	outs    map[string]*runningOutput
21	latest  map[ports.WindowID]ports.SurfaceContent
22	cursor  ports.CursorChange
23	stopped chan string
24	quit    chan struct{} // closed by wait: nobody reads stopped any more
25}
26
27type runningOutput struct {
28	scenes   chan ports.Scene
29	contents chan ports.SurfaceContent
30	cursor   chan ports.CursorChange
31	ctx      context.Context
32	stop     context.CancelFunc
33	err      error
34	done     chan struct{}
35}
36
37func newOutputSet() *outputSet {
38	return &outputSet{outs: map[string]*runningOutput{}, latest: map[ports.WindowID]ports.SurfaceContent{}, stopped: make(chan string), quit: make(chan struct{})}
39}
40
41// start runs an output in its own goroutine and replays the window contents.
42func (s *outputSet) start(ctx context.Context, name string, run outputRun) {
43	octx, stop := context.WithCancel(ctx)
44	r := &runningOutput{scenes: make(chan ports.Scene, 1), contents: make(chan ports.SurfaceContent, 64), cursor: make(chan ports.CursorChange, 1), ctx: octx, stop: stop, done: make(chan struct{})}
45	s.outs[name] = r
46	go func() {
47		r.err = run(octx, r.scenes, r.contents, r.cursor)
48		close(r.done)
49		select {
50		case s.stopped <- name:
51		case <-s.quit:
52		}
53	}()
54	r.cursor <- s.cursor
55	for _, c := range s.latest {
56		r.send(c)
57	}
58}
59
60// setCursor gives every output the latest cursor, replacing an unread one.
61func (s *outputSet) setCursor(c ports.CursorChange) {
62	s.cursor = c
63	for _, r := range s.outs {
64		select {
65		case <-r.cursor:
66		default:
67		}
68		r.cursor <- c
69	}
70}
71
72// send hands content to the output unless it has stopped.
73func (r *runningOutput) send(c ports.SurfaceContent) {
74	select {
75	case r.contents <- c:
76	case <-r.ctx.Done():
77	case <-r.done:
78	}
79}
80
81// scenes routes each scene to its output, replacing an unread one.
82func (s *outputSet) scenes(set []ports.Scene) {
83	for _, sc := range set {
84		if r := s.outs[sc.Output]; r != nil {
85			select {
86			case <-r.scenes:
87			default:
88			}
89			r.scenes <- sc
90		}
91	}
92}
93
94func (s *outputSet) content(c ports.SurfaceContent) {
95	if c.Empty() {
96		delete(s.latest, c.ID)
97	} else {
98		s.latest[c.ID] = c
99	}
100	for _, r := range s.outs {
101		r.send(c)
102	}
103}
104
105// finish forgets a stopped output and returns its error.
106func (s *outputSet) finish(name string) error {
107	r := s.outs[name]
108	if r == nil {
109		return nil
110	}
111	<-r.done
112	r.stop()
113	delete(s.outs, name)
114	return r.err
115}
116
117// wait stops every output and returns the first error.
118func (s *outputSet) wait() error {
119	var first error
120	close(s.quit)
121	for name, r := range s.outs {
122		r.stop()
123		<-r.done
124		if first == nil && r.err != nil {
125			first = r.err
126		}
127		delete(s.outs, name)
128	}
129	return first
130}
131
132// cursors routes pointer moves from the input goroutine to the cursor of
133// the output under the pointer and hides the others.
134type cursors struct {
135	mu   sync.Mutex
136	all  map[string]cursor
137	last string
138}
139
140// cursor is a hardware or software cursor of one output.
141type cursor interface {
142	Move(x, y float64)
143	Hide()
144}
145
146func newCursors() *cursors { return &cursors{all: map[string]cursor{}} }
147
148// set registers the cursor of an output; nil removes it.
149func (c *cursors) set(output string, cur cursor) {
150	c.mu.Lock()
151	defer c.mu.Unlock()
152	if cur == nil {
153		delete(c.all, output)
154		return
155	}
156	c.all[output] = cur
157	// Only the output under the pointer shows it; before the first move,
158	// none does (a new cursor would show at its top-left corner).
159	if output != c.last {
160		cur.Hide()
161	}
162}
163
164// move places the cursor on output at physical (x, y).
165func (c *cursors) move(output string, x, y float64) {
166	c.mu.Lock()
167	defer c.mu.Unlock()
168	if output != c.last {
169		for name, cur := range c.all {
170			if name != output {
171				cur.Hide()
172			}
173		}
174		c.last = output
175	}
176	if cur := c.all[output]; cur != nil {
177		cur.Move(x, y)
178	}
179}

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 26

Tags 10

↑↓ moveenter openesc close