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, false)
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}