v0.10.0 d5d679d

outputmanagement.go RawHistoryFolder

1package app
2
3import (
4	"errors"
5
6	"github.com/bnema/neferwl/internal/core"
7	"github.com/bnema/neferwl/internal/ports"
8	"github.com/bnema/zerowrap"
9)
10
11// errApplySuperseded answers a request whose configuration a reload replaced.
12var errApplySuperseded = errors.New("output apply superseded by reload")
13
14// outputApply owns output management from request to reply: runtime
15// overrides, supersede by reload, and rollback after a failed backend apply.
16// It belongs to the backend owner loop, which starts the configuration next
17// returns, reports each result through finished and drains the outbox (core
18// configuration, Wayland replies and inventory) with sends that never block
19// the loop.
20type outputApply struct {
21	state *core.OutputOverrides
22	log   zerowrap.Logger
23	// running is set while the backend applies a configuration. The next
24	// one waits in start until it finishes: configurations never overlap.
25	running bool
26	start   ports.Config
27	pending bool
28	// req is the request behind the backend work; nil for a reload or after
29	// a reload superseded it.
30	req      *ports.OutputApply
31	previous map[string]ports.OutputConfig
32	rollback bool
33	failure  error
34	// config is the latest effective configuration core has not received.
35	config  ports.ConfigChanged
36	publish bool
37	replies []ports.OutputApplied
38	// headsDue is set while Wayland has not received the latest inventory.
39	headsDue bool
40}
41
42func newOutputApply(state *core.OutputOverrides, log zerowrap.Logger) *outputApply {
43	return &outputApply{state: state, log: log}
44}
45
46// reload replaces the file configuration and supersedes the pending request.
47// Only the latest reload needs backend work.
48func (a *outputApply) reload(file ports.Config) {
49	cfg := a.state.Reload(file)
50	a.setConfig(cfg)
51	if a.req != nil {
52		a.reply(a.req.ID, errApplySuperseded)
53		a.req = nil
54	}
55	if !a.state.Headless() {
56		a.due(cfg)
57	}
58}
59
60// heads records the backend inventory: it validates requests and waits for
61// Wayland in the outbox, latest only.
62func (a *outputApply) heads(heads ports.OutputHeads) {
63	a.state.SetHeads(heads)
64	a.headsDue = true
65}
66
67// request validates a protocol request. Test, invalid and headless requests
68// are answered at once; others wait for the backend.
69func (a *outputApply) request(req ports.OutputApply) {
70	if a.running || a.pending {
71		a.reply(req.ID, errors.New("output apply already pending"))
72		return
73	}
74	previous := a.state.Snapshot()
75	cfg, err := a.state.Apply(req)
76	if err != nil || req.Test {
77		a.reply(req.ID, err)
78		return
79	}
80	a.setConfig(cfg)
81	if a.state.Headless() {
82		a.reply(req.ID, nil)
83		return
84	}
85	a.req, a.previous, a.rollback, a.failure = &req, previous, false, nil
86	a.due(cfg)
87}
88
89// next returns the configuration the backend must start now, once. It
90// returns nothing while the backend applies the previous one.
91func (a *outputApply) next() (ports.Config, bool) {
92	if a.running || !a.pending {
93		return ports.Config{}, false
94	}
95	cfg := a.start
96	a.start, a.pending, a.running = ports.Config{}, false, true
97	return cfg, true
98}
99
100// finished records the backend result of the running configuration. When a
101// reload is waiting, the result belongs to a superseded configuration and is
102// dropped. After a failed request the previous overrides return and the
103// rollback configuration becomes due; the request is answered with the
104// original failure once the rollback is done.
105func (a *outputApply) finished(err error) {
106	if !a.running {
107		return
108	}
109	a.running = false
110	if a.pending {
111		return
112	}
113	req := a.req
114	if req == nil {
115		return
116	}
117	if a.rollback {
118		a.req = nil
119		a.reply(req.ID, a.failure)
120		return
121	}
122	if err != nil {
123		cfg := a.state.Restore(a.previous)
124		a.setConfig(cfg)
125		a.rollback, a.failure = true, err
126		a.due(cfg)
127		return
128	}
129	a.req = nil
130	for _, h := range req.Heads {
131		e := a.log.Info().Str("output", h.Name).Bool("enabled", h.Enabled).Float64("scale", h.Scale)
132		if h.Transform != nil {
133			e = e.Uint8("transform", uint8(*h.Transform))
134		}
135		e.Msg("output configuration applied")
136	}
137	a.reply(req.ID, nil)
138}
139
140// due replaces the configuration waiting to start: only the latest counts.
141func (a *outputApply) due(cfg ports.Config) {
142	a.start, a.pending = cfg, true
143}
144
145func (a *outputApply) setConfig(cfg ports.Config) {
146	a.config = ports.ConfigChanged{Config: cfg}
147	a.publish = true
148}
149
150func (a *outputApply) reply(id uint64, err error) {
151	if err != nil {
152		a.log.Warn().Err(err).Msg("output configuration failed")
153	}
154	a.replies = append(a.replies, ports.OutputApplied{ID: id, Err: err})
155}
156
157// configOut returns the core channel when a configuration waits, else nil so
158// the owner loop's send case stays disabled.
159func (a *outputApply) configOut(ch chan<- ports.ConfigChanged) chan<- ports.ConfigChanged {
160	if !a.publish {
161		return nil
162	}
163	return ch
164}
165
166// configSent records that core received the latest configuration.
167func (a *outputApply) configSent() { a.publish = false }
168
169// replyOut returns the reply channel and the oldest reply, or a nil channel.
170func (a *outputApply) replyOut(ch chan<- ports.OutputApplied) (chan<- ports.OutputApplied, ports.OutputApplied) {
171	if len(a.replies) == 0 {
172		return nil, ports.OutputApplied{}
173	}
174	return ch, a.replies[0]
175}
176
177// headsOut returns the Wayland inventory channel and the latest inventory,
178// or a nil channel when Wayland has it.
179func (a *outputApply) headsOut(ch chan<- ports.OutputHeads) (chan<- ports.OutputHeads, ports.OutputHeads) {
180	if !a.headsDue {
181		return nil, ports.OutputHeads{}
182	}
183	return ch, a.state.Heads()
184}
185
186// headsSent records that Wayland received the latest inventory.
187func (a *outputApply) headsSent() { a.headsDue = false }
188
189// replySent drops the delivered reply.
190func (a *outputApply) replySent() {
191	a.replies[0] = ports.OutputApplied{}
192	a.replies = a.replies[1:]
193}

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