feat/hints a471fa4

outputs.go RawHistoryFolder

1package wayland
2
3import (
4	"math"
5	"slices"
6	"strings"
7
8	"github.com/bnema/go-wayland-bindings/server/wayland"
9	"github.com/bnema/go-wayland-bindings/server/xdgoutput"
10	"github.com/bnema/neferwl/internal/ports"
11	"github.com/bnema/purego-libwayland/server"
12)
13
14// setOutputHDR records confirmed DRM state independently of the core layout.
15// Until the first modeset report for an output it is treated as SDR.
16func (s *Server) setOutputHDR(f ports.OutputFormats) {
17	if s.hdrOutputs == nil {
18		s.hdrOutputs = make(map[string]ports.OutputHDR)
19	}
20	old, had := s.hdrOutputs[f.Output]
21	if f.HDR == nil {
22		delete(s.hdrOutputs, f.Output)
23	} else {
24		s.hdrOutputs[f.Output] = *f.HDR
25	}
26	if (f.HDR == nil && had) || (f.HDR != nil && (!had || old != *f.HDR)) {
27		s.colorOutputChanged(f.Output)
28	}
29}
30
31// Outputs. Core sends the global layout (ports.SetOutputs); each output is
32// one wl_output global, removed when the display is unplugged. Every size
33// sent to clients (configures, xdg-output, layer configures, pointer
34// coordinates) is logical. Clients that bind wp_fractional_scale_v1 get the
35// exact scale of their output in 1/120 steps and draw at physical size
36// through wp_viewporter; others get wl_output.scale rounded up and the
37// renderer downscales.
38
39// output is one advertised display and the resources clients bound to it.
40type output struct {
41	place      ports.OutputPlacement
42	global     *server.Global
43	resources  []*wayland.Output
44	xdgOutputs []*xdgoutput.ZxdgOutputV1
45}
46
47func (o *output) name() string { return o.place.Info.Name }
48
49// integerScale is wl_output.scale: the fractional scale rounded up.
50func integerScale(scale float64) int32 { return int32(math.Ceil(max(scale, 1) - 1e-9)) }
51
52func (o *output) description() string {
53	i := o.place.Info
54	return strings.Join(strings.Fields(i.Make+" "+i.Model+" "+i.Serial+" ("+i.Name+")"), " ")
55}
56
57// sendAll sends the full wl_output state, then done (v2+).
58func (o *output) sendAll(r *wayland.Output) {
59	i := o.place.Info
60	make, model := i.Make, i.Model
61	if make == "" {
62		make, model = "neferwl", "headless"
63	}
64	refresh := i.RefreshMilli
65	if refresh == 0 {
66		refresh = 60000
67	}
68	r.SendGeometry(int32(o.place.X), int32(o.place.Y), int32(i.PhysicalW), int32(i.PhysicalH), 0, make, model, int32(o.place.Transform))
69	// The mode stays physical; the scale tells clients how to divide it.
70	r.SendMode(3, int32(i.Width), int32(i.Height), int32(refresh))
71	if r.Version() >= 2 {
72		r.SendScale(integerScale(o.place.Scale))
73	}
74	if r.Version() >= 4 {
75		r.SendName(i.Name)
76		r.SendDescription(o.description())
77	}
78	if r.Version() >= 2 {
79		r.SendDone()
80	}
81}
82
83func (o *output) sendXDG(x *xdgoutput.ZxdgOutputV1, withName bool) {
84	x.SendLogicalPosition(int32(o.place.X), int32(o.place.Y))
85	x.SendLogicalSize(int32(o.place.Width), int32(o.place.Height))
86	if withName && x.Version() >= 2 {
87		x.SendName(o.name())
88		x.SendDescription(o.description())
89	}
90}
91
92// outputOf returns the output a wl_output resource belongs to, or nil for a
93// removed one.
94func (s *Server) outputOf(r *wayland.Output) *output {
95	if r == nil {
96		return nil
97	}
98	for _, o := range s.outputs {
99		for _, v := range o.resources {
100			if v.Resource == r.Resource {
101				return o
102			}
103		}
104	}
105	return nil
106}
107
108// outputByName returns the output with that connector, else the focused one,
109// else the first; nil when there is none.
110func (s *Server) outputByName(name string) *output {
111	var first, focused *output
112	for _, o := range s.outputs {
113		if o.name() == name {
114			return o
115		}
116		if first == nil {
117			first = o
118		}
119		if o.name() == s.focusedOutput {
120			focused = o
121		}
122	}
123	if focused != nil {
124		return focused
125	}
126	return first
127}
128
129// addOutput advertises a new wl_output global.
130func (s *Server) addOutput(p ports.OutputPlacement) {
131	o := &output{place: p}
132	g, err := s.display.AddGlobal(wayland.OutputInterface, 4, func(c server.Client, v, id uint32) {
133		r, err := wayland.NewOutput(c, int32(v), id, outputHandler{})
134		if err != nil {
135			return
136		}
137		// A client may bind a global just removed: it gets the last state
138		// and no updates.
139		o.resources = append(o.resources, r)
140		r.OnDestroy = func() { o.resources = removeItem(o.resources, r) }
141		o.sendAll(r)
142		for _, m := range s.workspaceManagers {
143			if m.outputBound(o.name()) {
144				m.res.SendDone()
145			}
146		}
147		s.refreshToplevels()
148	})
149	if err != nil {
150		s.log.Warn().Err(err).Str("output", p.Info.Name).Msg("add wl_output")
151		return
152	}
153	o.global = g
154	s.outputs = append(s.outputs, o)
155	s.log.Info().Str("output", p.Info.Name).Int("x", p.X).Int("y", p.Y).Int("w", p.Width).Int("h", p.Height).Float64("scale", p.Scale).Msg("output added")
156}
157
158// setOutputs applies the global layout from core: new outputs are
159// advertised, gone ones withdrawn (their layer surfaces are closed), and
160// changed ones resent.
161func (s *Server) setOutputs(c ports.SetOutputs) {
162	changed := !slices.Equal(s.outputPlaces, c.Outputs)
163	focusChanged := s.focusedOutput != c.Focused
164	s.outputPlaces = append(ports.Layout(nil), c.Outputs...)
165	defer func() {
166		if changed {
167			s.refreshOutputManagers()
168		}
169		if changed || focusChanged {
170			s.castInventoryChanged()
171		}
172	}()
173	s.focusedOutput = c.Focused
174	want := map[string]ports.OutputPlacement{}
175	for _, p := range c.Outputs {
176		want[p.Info.Name] = p
177	}
178	kept := s.outputs[:0]
179	for _, o := range s.outputs {
180		if _, ok := want[o.name()]; ok {
181			kept = append(kept, o)
182			continue
183		}
184		o.global.Remove()
185		delete(s.hdrOutputs, o.name())
186		delete(s.colorIdentity, o.name())
187		s.outputGone(o)
188		s.log.Info().Str("output", o.name()).Msg("output removed")
189		for _, l := range s.layers {
190			if l.output == o {
191				l.close()
192			}
193		}
194		for _, l := range s.lockSurfaces {
195			if l.output == o {
196				l.unmap()
197			}
198		}
199		for _, surf := range s.surfaces {
200			surf.leave(o)
201		}
202	}
203	s.outputs = kept
204	// Windows of a removed output leave it until core moves them.
205	s.refreshToplevels()
206	for _, p := range c.Outputs {
207		o := s.outputByNameExact(p.Info.Name)
208		if o == nil {
209			s.addOutput(p)
210			continue
211		}
212		if o.place == p {
213			continue
214		}
215		// A 90° to 270° change keeps the logical size: lock surfaces are
216		// logical-sized and need no reconfigure then.
217		logicalChanged := o.place.Width != p.Width || o.place.Height != p.Height
218		// The first layout from core moves an output off the placement it was
219		// added with, so this line carries the real position.
220		s.log.Info().Str("output", p.Info.Name).Int("x", p.X).Int("y", p.Y).Int("w", p.Width).Int("h", p.Height).Float64("scale", p.Scale).Int("transform", int(p.Transform)).Bool("primary", p.Primary).Msg("output changed")
221		o.place = p
222		if logicalChanged {
223			for _, l := range s.lockSurfaces {
224				if l.output == o && !l.closed {
225					l.sendConfigure()
226				}
227			}
228			s.lockSurfaceChanged()
229		}
230		// Buffer sizes follow the mode and the scale (region and workspace
231		// sources too): sessions send new constraints when theirs changed.
232		s.refreshSessions()
233		for _, r := range o.resources {
234			o.sendAll(r)
235		}
236		for _, x := range o.xdgOutputs {
237			o.sendXDG(x, false)
238			if x.Version() < 3 {
239				x.SendDone()
240			}
241		}
242	}
243	// Surfaces follow the scale of their output.
244	for _, surf := range s.surfaces {
245		surf.sendScale()
246	}
247	s.notifyColorFeedback()
248	// Layer surfaces sized from the output follow its new logical size.
249	adopted := false
250	for _, l := range s.layers {
251		if l.output == nil && !l.closed {
252			// Created before any output: it takes the focused one.
253			if l.output = s.outputByName(""); l.output != nil && l.configured {
254				l.sendConfigure()
255				adopted = adopted || l.mapped
256				continue
257			}
258		}
259		// Uses committed state only: pending requests wait for the client's commit.
260		if l.configured && (l.current.width == 0 || l.current.height == 0) {
261			l.sendConfigure()
262		}
263	}
264	if adopted {
265		s.layerChanged()
266	}
267}
268
269func (s *Server) outputByNameExact(name string) *output {
270	for _, o := range s.outputs {
271		if o.name() == name {
272			return o
273		}
274	}
275	return nil
276}
277
278type outputHandler struct{}
279
280func (outputHandler) Release(*wayland.Output) {}
281
282func registerXDGOutput(d *server.Display, s *Server) error {
283	return xdgoutput.NewZxdgOutputManagerV1Global(d, 3, func(c server.Client, v, id uint32) {
284		_, _ = xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgOutputManager{s})
285	})
286}
287
288type xdgOutputManager struct{ server *Server }
289
290func (xdgOutputManager) Destroy(*xdgoutput.ZxdgOutputManagerV1) {}
291func (m xdgOutputManager) GetXdgOutput(r *xdgoutput.ZxdgOutputManagerV1, id uint32, wl *wayland.Output) {
292	x, err := xdgoutput.NewZxdgOutputV1(r.Client(), r.Version(), id, xdgOutput{})
293	if err != nil {
294		return
295	}
296	o := m.server.outputOf(wl)
297	if o == nil {
298		// Removed output: the object stays inert.
299		return
300	}
301	o.xdgOutputs = append(o.xdgOutputs, x)
302	x.OnDestroy = func() { o.xdgOutputs = removeItem(o.xdgOutputs, x) }
303	o.sendXDG(x, true)
304	if r.Version() < 3 || wl.Version() < 2 {
305		x.SendDone()
306	} else {
307		wl.SendDone()
308	}
309}
310
311type xdgOutput struct{}
312
313func (xdgOutput) Destroy(*xdgoutput.ZxdgOutputV1) {}

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