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

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