feat/data-dnd e05cb15

inventory.go RawHistoryFolder

1package screencast
2
3import (
4	"reflect"
5	"slices"
6	"strings"
7
8	"github.com/bnema/neferwl/internal/ports"
9	"github.com/bnema/zerobus"
10)
11
12// applyInventory takes the outputs and windows wayland published. The
13// receiver owns the slices. Departures of outputs and windows are signalled
14// to the clients that watch them (the first inventory is not a change).
15func (s *Service) applyInventory(inv ports.CastInventory) {
16	slices.SortFunc(inv.Outputs, func(a, b ports.CastOutput) int { return strings.Compare(a.Info.Name, b.Info.Name) })
17	slices.SortFunc(inv.Windows, func(a, b ports.CastWindow) int {
18		switch {
19		case a.ID < b.ID:
20			return -1
21		case a.ID > b.ID:
22			return 1
23		}
24		return 0
25	})
26	old, had := s.inv, s.haveInv
27	s.inv, s.haveInv = inv, true
28	if !had {
29		return
30	}
31	if s.owns[1] && !reflect.DeepEqual(old.Outputs, inv.Outputs) {
32		s.notifyAllowed(pathDisplayConfig, ifaceDisplayConfig, "MonitorsChanged")
33	}
34	if s.owns[2] && !reflect.DeepEqual(old.Windows, inv.Windows) {
35		s.notifyAllowed(pathIntrospect, ifaceIntrospect, "WindowsChanged")
36	}
37}
38
39// notifyAllowed sends a body-less signal to each peer that passed the caller
40// check and whose executable the allowlist admits now. The signals are
41// directed: the inventory (output names, window titles) is never broadcast,
42// so a peer that was refused, or has not been checked, learns nothing. At
43// most maxCallers peers are served, from the cache, without allocation. The
44// executable is read again through the pidfd, as at the privileged entry
45// points. Peers that left the bus are gone from the cache (onGone).
46func (s *Service) notifyAllowed(path, iface, member string) {
47	for i := range s.callers {
48		c := &s.callers[i]
49		if c.state != callerKnown || !s.allowed(c.exe) || !s.recheck(c) {
50			continue
51		}
52		s.conn.NewSignalTo(c.name, path, iface, member, "")
53		s.send()
54	}
55}
56
57// isBuiltin follows Mutter: the connector of a laptop panel.
58func isBuiltin(connector string) bool {
59	for _, p := range [...]string{"eDP", "LVDS", "DSI"} {
60		if strings.HasPrefix(connector, p) {
61			return true
62		}
63	}
64	return false
65}
66
67// refreshHz is a refresh rate in Hz from millihertz.
68func refreshHz(milli int) float64 { return float64(milli) / 1000 }
69
70// replyState answers DisplayConfig.GetCurrentState:
71// (u, a((ssss)a(siiddada{sv})a{sv}), a(iiduba(ssss)a{sv}), a{sv}).
72// layout-mode 1 is physical: logical sizes are scaled sizes of the modes.
73// Exactly one mode of each monitor is current, as x-d-p-g requires.
74func (s *Service) replyState(req *request) {
75	e := s.replyTo(req, "ua((ssss)a(siiddada{sv})a{sv})a(iiduba(ssss)a{sv})a{sv}")
76	if e == nil {
77		return
78	}
79	e.Uint32(0)
80	monitors := e.BeginArray('(')
81	for i := range s.inv.Outputs {
82		o := &s.inv.Outputs[i]
83		e.Struct()
84		s.encodeNames(e, o)
85		modes := e.BeginArray('(')
86		cur := o.Current
87		if cur < 0 || cur >= len(o.Modes) {
88			cur = 0
89		}
90		for j, m := range o.Modes {
91			e.Struct()
92			e.Str(modeID(m))
93			e.Int32(int32(m.Width))
94			e.Int32(int32(m.Height))
95			e.Float64(refreshHz(m.RefreshMilli))
96			e.Float64(1)
97			sc := e.BeginArray('d')
98			e.Float64(1)
99			e.EndArray(sc)
100			props := e.BeginArray('{')
101			e.Struct()
102			e.Str("is-preferred")
103			e.Variant("b")
104			e.Bool(m.Preferred)
105			if j == cur {
106				e.Struct()
107				e.Str("is-current")
108				e.Variant("b")
109				e.Bool(true)
110			}
111			e.EndArray(props)
112		}
113		e.EndArray(modes)
114		props := e.BeginArray('{')
115		display := strings.TrimSpace(o.Info.Make + " " + o.Info.Model)
116		if display == "" {
117			display = o.Info.Name
118		}
119		e.Struct()
120		e.Str("display-name")
121		e.Variant("s")
122		e.Str(display)
123		e.Struct()
124		e.Str("is-builtin")
125		e.Variant("b")
126		e.Bool(isBuiltin(o.Info.Name))
127		e.EndArray(props)
128	}
129	e.EndArray(monitors)
130	logical := e.BeginArray('(')
131	for i := range s.inv.Outputs {
132		o := &s.inv.Outputs[i]
133		e.Struct()
134		e.Int32(int32(o.X))
135		e.Int32(int32(o.Y))
136		scale := o.Scale
137		if scale <= 0 {
138			scale = 1
139		}
140		e.Float64(scale)
141		e.Uint32(uint32(o.Transform))
142		e.Bool(i == 0)
143		mons := e.BeginArray('(')
144		e.Struct()
145		s.encodeNames(e, o)
146		e.EndArray(mons)
147		props := e.BeginArray('{')
148		e.EndArray(props)
149	}
150	e.EndArray(logical)
151	props := e.BeginArray('{')
152	e.Struct()
153	e.Str("layout-mode")
154	e.Variant("u")
155	e.Uint32(1)
156	e.EndArray(props)
157	s.send()
158}
159
160// encodeNames writes the monitor spec (connector, vendor, product, serial).
161// The serial falls back to the connector, as session restore needs one.
162func (s *Service) encodeNames(e *zerobus.Encoder, o *ports.CastOutput) {
163	e.Struct()
164	serial := o.Info.Serial
165	if serial == "" {
166		serial = o.Info.Name
167	}
168	e.Str(o.Info.Name)
169	e.Str(o.Info.Make)
170	e.Str(o.Info.Model)
171	e.Str(serial)
172}
173
174func modeID(m ports.OutputMode) string {
175	return itoa(m.Width) + "x" + itoa(m.Height) + "@" + ftoa(refreshHz(m.RefreshMilli))
176}
177
178// replyWindows answers Introspect.GetWindows: a{ta{sv}} of app-id and title.
179// The app id is the raw Wayland app id.
180func (s *Service) replyWindows(req *request) {
181	e := s.replyTo(req, "a{ta{sv}}")
182	if e == nil {
183		return
184	}
185	a := e.BeginArray('{')
186	for i := range s.inv.Windows {
187		w := &s.inv.Windows[i]
188		e.Struct()
189		e.Uint64(uint64(w.ID))
190		props := e.BeginArray('{')
191		e.Struct()
192		e.Str("app-id")
193		e.Variant("s")
194		e.Str(w.AppID)
195		e.Struct()
196		e.Str("title")
197		e.Variant("s")
198		e.Str(w.Title)
199		e.EndArray(props)
200	}
201	e.EndArray(a)
202	s.send()
203}

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