v0.2.0 13c6a90

headless.go RawHistoryFolder

1package app
2
3import (
4	"context"
5	"fmt"
6	"os"
7	"path/filepath"
8
9	"github.com/bnema/neferwl/internal/adapters/headless"
10	"github.com/bnema/neferwl/internal/ports"
11	"github.com/bnema/zerowrap"
12)
13
14// runHeadless drives one virtual output per size, named HEADLESS-1, -2, ...
15// With several outputs, screenshots go to a subdirectory per output. Modes
16// are fixed, so apply answers every configuration without backend work.
17func runHeadless(ctx context.Context, sizes [][2]int, shots string, hdr bool, apply *outputApply, ch outputChannels, curs *cursors, newRenderer func(w, h int) (ports.Renderer, error), log zerowrap.Logger) error {
18	set := newOutputSet(ctx, ch.captured)
19	set.wireSecurity(ch)
20	inventory := ports.OutputHeads{}
21	for i, size := range sizes {
22		name := fmt.Sprintf("HEADLESS-%d", i+1)
23		mode := ports.OutputMode{Width: size[0], Height: size[1], RefreshMilli: 60000, Preferred: true}
24		inventory.Heads = append(inventory.Heads, ports.OutputHead{Info: ports.OutputInfo{Name: name, Width: size[0], Height: size[1], RefreshMilli: 60000}, Modes: []ports.OutputMode{mode}, Current: &mode, Enabled: true})
25		dir := shots
26		if dir != "" && len(sizes) > 1 {
27			dir = filepath.Join(shots, name)
28			if err := os.MkdirAll(dir, 0o755); err != nil {
29				// Outputs already started stop and release their captures.
30				return joinErr(err, set.wait())
31			}
32		}
33		cur := &headless.Cursor{}
34		curs.set(name, cur)
35		opts := headless.Options{Cursor: cur, LoadCursor: loadCursor, Width: size[0], Height: size[1], ScreenshotDir: dir, HDR: hdr, Formats: ch.formats, Log: log, NewRenderer: newRenderer, NewCaptureRenderer: newRenderer, Name: name, Presented: ch.presented, Captured: ch.captured}
36		started := set.start(ctx, name, func(octx context.Context, sc <-chan ports.Scene, cc <-chan ports.SurfaceContent, cu <-chan ports.CursorChange, cap <-chan ports.CaptureRequest, secure <-chan ports.SecurityState, instance ports.OutputInstance) error {
37			opts.Security, opts.SecurityChanges, opts.Instance = ch.security, secure, instance
38			return headless.Run(octx, opts, sc, cc, cu, cap)
39		}, &opts.SecurityEvents)
40		if !started {
41			return set.wait()
42		}
43		select {
44		case ch.events <- ports.OutputAdded{Info: ports.OutputInfo{Name: name, Width: size[0], Height: size[1]}}:
45		case <-ctx.Done():
46			return set.wait()
47		}
48	}
49	// All virtual lifetimes have registered before this no-lease barrier.
50	if set.state.Protected {
51		set.securityEvent(ports.SecurityBackendBarrier{Generation: set.state.Generation})
52	}
53	apply.heads(inventory)
54	// The pointer starts centred on the first output, like libinput's.
55	curs.move("HEADLESS-1", float64(sizes[0][0])/2, float64(sizes[0][1])/2)
56	for {
57		configs, configNext := apply.configOut(ch.configured), apply.config
58		replies, replyNext := apply.replyOut(ch.replies)
59		heads, headsNext := apply.headsOut(ch.heads)
60		contents, contentOut, contentNext, contentOwner := set.contentOut(ch.contents)
61		select {
62		case <-ctx.Done():
63			return set.wait()
64		case state, ok := <-ch.securityChanges:
65			if !ok {
66				ch.securityChanges = nil
67				continue
68			}
69			if set.setSecurity(state) && set.state.Protected {
70				set.securityEvent(ports.SecurityBackendBarrier{Generation: set.state.Generation})
71			}
72		case ev := <-ch.reloads:
73			apply.reload(ev.Config)
74		case req := <-ch.requests:
75			apply.request(req)
76		case configs <- configNext:
77			apply.configSent()
78		case replies <- replyNext:
79			apply.replySent()
80		case heads <- headsNext:
81			apply.headsSent()
82		case s := <-ch.scenes:
83			set.scenes(s)
84		case c := <-contents:
85			set.content(c)
86		case contentOut <- contentNext:
87			contentOwner.contentSent()
88		case q := <-ch.captures:
89			set.routeCapture(q)
90		case c := <-ch.cursorChanges:
91			set.setCursor(c)
92		case stopped := <-set.stopped:
93			if !set.stoppedCurrent(stopped) {
94				continue
95			}
96			// An owner error is not protection or removal evidence.
97			err := set.finish(stopped.name)
98			return joinErr(err, set.wait())
99		}
100	}
101}
102
103func joinErr(a, b error) error {
104	if a != nil {
105		return a
106	}
107	return b
108}

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