main f2d166b

outputoverrides.go RawHistoryFolder

1package core
2
3import (
4	"fmt"
5	"image"
6	"maps"
7	"math"
8	"reflect"
9	"slices"
10
11	"github.com/bnema/neferwl/internal/ports"
12)
13
14// OutputOverrides holds runtime-only output settings applied through output
15// management on top of the file configuration, which is never mutated. Its
16// owner is the backend loop; it is not safe for concurrent use.
17type OutputOverrides struct {
18	file      ports.Config
19	overrides map[string]ports.OutputConfig
20	heads     ports.OutputHeads
21	headless  bool
22}
23
24// NewOutputOverrides starts with no override. Headless outputs keep their
25// mode and enabled state.
26func NewOutputOverrides(file ports.Config, headless bool) *OutputOverrides {
27	return &OutputOverrides{file: file, headless: headless, overrides: map[string]ports.OutputConfig{}}
28}
29
30// Headless reports whether the outputs are headless.
31func (o *OutputOverrides) Headless() bool { return o.headless }
32
33// Heads is the backend inventory requests are validated against.
34func (o *OutputOverrides) Heads() ports.OutputHeads { return o.heads }
35
36// SetHeads records the backend inventory.
37func (o *OutputOverrides) SetHeads(heads ports.OutputHeads) { o.heads = heads }
38
39// Snapshot copies the current overrides for a later Restore.
40func (o *OutputOverrides) Snapshot() map[string]ports.OutputConfig {
41	previous := make(map[string]ports.OutputConfig, len(o.overrides))
42	maps.Copy(previous, o.overrides)
43	return previous
44}
45
46// Restore puts back overrides taken by Snapshot and returns the effective
47// configuration.
48func (o *OutputOverrides) Restore(previous map[string]ports.OutputConfig) ports.Config {
49	o.overrides = previous
50	return o.Effective()
51}
52
53// Effective is the file configuration with the overrides applied.
54func (o *OutputOverrides) Effective() ports.Config {
55	cfg := o.file
56	cfg.Outputs = slices.Clone(o.file.Outputs)
57	// Preserve file order and backend connection order; map iteration would
58	// make automatic placement change unpredictably.
59	for _, head := range o.heads.Heads {
60		entry, ok := o.overrides[head.Info.Name]
61		if !ok {
62			continue
63		}
64		index := slices.IndexFunc(cfg.Outputs, func(v ports.OutputConfig) bool { return v.Name == head.Info.Name })
65		if index < 0 {
66			cfg.Outputs = append(cfg.Outputs, entry)
67		} else {
68			cfg.Outputs[index] = entry
69		}
70	}
71	return cfg
72}
73
74// Reload replaces the file configuration. Runtime overrides are dropped,
75// unless only output scales changed (a saved scale bind): then they stay and
76// take the new file scales.
77func (o *OutputOverrides) Reload(file ports.Config) ports.Config {
78	scales, onlyScales := scaleChanges(o.file, file)
79	o.file = file
80	if !onlyScales {
81		clear(o.overrides)
82		return o.Effective()
83	}
84	for name, scale := range scales {
85		if entry, ok := o.overrides[name]; ok {
86			entry.Scale = scale
87			o.overrides[name] = entry
88		}
89	}
90	return o.Effective()
91}
92
93// scaleChanges lists the output scales that differ from old to cur, and
94// reports whether nothing else differs.
95func scaleChanges(old, cur ports.Config) (map[string]float64, bool) {
96	scaleOf := func(c ports.Config, name string) float64 {
97		for _, o := range c.Outputs {
98			if o.Name == name {
99				return o.Scale
100			}
101		}
102		return 0
103	}
104	// Without scales, an output.<name>.scale-only entry is empty: drop it.
105	strip := func(c ports.Config) ports.Config {
106		c.Outputs = slices.Clone(c.Outputs)
107		for i := range c.Outputs {
108			c.Outputs[i].Scale = 0
109		}
110		c.Outputs = slices.DeleteFunc(c.Outputs, func(o ports.OutputConfig) bool {
111			return o == ports.OutputConfig{Name: o.Name, ScaleOnly: true, SDRBrightness: ports.DefaultSDRBrightness}
112		})
113		return c
114	}
115	if !reflect.DeepEqual(strip(old), strip(cur)) {
116		return nil, false
117	}
118	scales := map[string]float64{}
119	for _, o := range cur.Outputs {
120		if o.Scale != scaleOf(old, o.Name) {
121			scales[o.Name] = o.Scale
122		}
123	}
124	return scales, true
125}
126
127func (o *OutputOverrides) validate(req ports.OutputApply) error {
128	if len(req.Heads) != len(o.heads.Heads) {
129		return fmt.Errorf("incomplete head configuration")
130	}
131	seen := map[string]bool{}
132	for _, change := range req.Heads {
133		if seen[change.Name] {
134			return fmt.Errorf("duplicate head %s", change.Name)
135		}
136		seen[change.Name] = true
137		index := slices.IndexFunc(o.heads.Heads, func(h ports.OutputHead) bool { return h.Info.Name == change.Name })
138		if index < 0 {
139			return fmt.Errorf("unknown head %s", change.Name)
140		}
141		current := o.heads.Heads[index]
142		if change.Scale < 0 || change.Scale > 4 || (change.Scale > 0 && change.Scale < 1) || math.IsNaN(change.Scale) || math.IsInf(change.Scale, 0) {
143			return fmt.Errorf("invalid scale on %s", change.Name)
144		}
145		if change.Transform != nil && *change.Transform > 7 {
146			return fmt.Errorf("invalid transform on %s", change.Name)
147		}
148		if o.headless && (change.Enabled != current.Enabled || change.Mode != nil && (current.Current == nil || change.Mode.Width != current.Current.Width || change.Mode.Height != current.Current.Height || change.Mode.RefreshMilli != current.Current.RefreshMilli)) {
149			return fmt.Errorf("headless mode and enabled state are fixed")
150		}
151		if change.Mode != nil && !slices.ContainsFunc(current.Modes, func(m ports.OutputMode) bool {
152			return m.Width == change.Mode.Width && m.Height == change.Mode.Height && m.RefreshMilli == change.Mode.RefreshMilli
153		}) {
154			return fmt.Errorf("unknown mode on %s", change.Name)
155		}
156	}
157	return nil
158}
159
160// Apply validates a request against the inventory and, unless it is a
161// test, records its changes as overrides. It returns the effective
162// configuration.
163func (o *OutputOverrides) Apply(req ports.OutputApply) (ports.Config, error) {
164	if err := o.validate(req); err != nil {
165		return ports.Config{}, err
166	}
167	if req.Test {
168		return o.Effective(), nil
169	}
170	for _, h := range req.Heads {
171		entry, ok := o.overrides[h.Name]
172		if !ok {
173			index := slices.IndexFunc(o.file.Outputs, func(v ports.OutputConfig) bool { return v.Name == h.Name })
174			if index >= 0 {
175				entry = o.file.Outputs[index]
176			} else {
177				entry.Name = h.Name
178			}
179		}
180		entry.Off = !h.Enabled
181		if h.Scale > 0 {
182			entry.Scale = h.Scale
183		}
184		if h.Transform != nil {
185			entry.Transform = *h.Transform
186		}
187		if h.Pos != nil {
188			entry.Pos = &image.Point{X: h.Pos.X, Y: h.Pos.Y}
189		}
190		if h.Mode != nil {
191			entry.Mode = fmt.Sprintf("%dx%d@%.3f", h.Mode.Width, h.Mode.Height, float64(h.Mode.RefreshMilli)/1000)
192		}
193		o.overrides[h.Name] = entry
194	}
195	return o.Effective(), nil
196}

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 25

Tags 10

↑↓ moveenter openesc close