feat/data-dnd e05cb15

session.go RawHistoryFolder

1package screencast
2
3import (
4	"strconv"
5
6	"github.com/bnema/neferwl/internal/ports"
7)
8
9// session is one org.gnome.Mutter.ScreenCast.Session. The sessions live in a
10// fixed array of Service, so pointers into it (the streams' back references,
11// the PipeWire handlers) stay valid.
12type session struct {
13	used  bool
14	id    uint32
15	path  string
16	owner string // unique name that called CreateSession
17	// started: Start was called, the streams were opened.
18	started bool
19	// dying: the session is closed to the bus; the slot is freed once every
20	// stream released its PipeWire resources.
21	dying   bool
22	streams [maxStreams]stream
23}
24
25func (s *Service) sessionByID(id uint32) *session {
26	for i := range s.sessions {
27		if sess := &s.sessions[i]; sess.used && !sess.dying && sess.id == id {
28			return sess
29		}
30	}
31	return nil
32}
33
34func (s *Service) streamByID(id uint32) *stream {
35	for i := range s.sessions {
36		sess := &s.sessions[i]
37		if !sess.used || sess.dying {
38			continue
39		}
40		for j := range sess.streams {
41			if st := &sess.streams[j]; st.used && st.id == id {
42				return st
43			}
44		}
45	}
46	return nil
47}
48
49func (s *Service) forEachSession(f func(*session)) {
50	for i := range s.sessions {
51		if s.sessions[i].used {
52			f(&s.sessions[i])
53		}
54	}
55}
56
57func (s *Service) forEachStream(f func(*stream)) {
58	for i := range s.sessions {
59		sess := &s.sessions[i]
60		if !sess.used {
61			continue
62		}
63		for j := range sess.streams {
64			if sess.streams[j].used {
65				f(&sess.streams[j])
66			}
67		}
68	}
69}
70
71// createSession answers CreateSession.
72func (s *Service) createSession(req *request) {
73	if req.remote {
74		s.replyErr(req, errNotSupported, "remote desktop sessions are not supported")
75		return
76	}
77	var sess *session
78	for i := range s.sessions {
79		if !s.sessions[i].used {
80			sess = &s.sessions[i]
81			break
82		}
83	}
84	if sess == nil {
85		s.replyErr(req, errLimits, "too many screen cast sessions")
86		return
87	}
88	s.nextSess++
89	*sess = session{used: true, id: s.nextSess, owner: req.sender}
90	sess.path = prefixSession + strconv.FormatUint(uint64(sess.id), 10)
91	s.log.Info().Str("sender", req.sender).Str("session", sess.path).Msg("screen cast session created")
92	if e := s.replyTo(req, "o"); e != nil {
93		e.ObjectPath(sess.path)
94		s.send()
95	}
96}
97
98// record answers RecordMonitor and RecordWindow: it adds a stream.
99func (s *Service) record(req *request, sess *session) {
100	if sess.started {
101		s.replyErr(req, errFailed, "session already started")
102		return
103	}
104	var st *stream
105	for i := range sess.streams {
106		if !sess.streams[i].used {
107			st = &sess.streams[i]
108			break
109		}
110	}
111	if st == nil {
112		s.replyErr(req, errLimits, "too many streams in the session")
113		return
114	}
115	*st = stream{used: true, sess: sess, cursor: req.cursor != 0, maxFPS: defaultFPS, pwState: -2}
116	if req.method == mRecordWindow {
117		if !s.haveWindow(ports.WindowID(req.window)) {
118			*st = stream{}
119			s.replyErr(req, errInvalidArgs, "no such window")
120			return
121		}
122		st.window = ports.WindowID(req.window)
123		// A window moves and resizes: the stream carries no position.
124		st.px, st.py, st.pw, st.ph = 0, 0, 1, 1
125	} else {
126		o := s.outputByName(req.text)
127		if o == nil {
128			*st = stream{}
129			s.replyErr(req, errInvalidArgs, "no such monitor")
130			return
131		}
132		st.output = o.Info.Name
133		st.px, st.py, st.pw, st.ph = int32(o.X), int32(o.Y), int32(o.Width), int32(o.Height)
134		if o.Current >= 0 && o.Current < len(o.Modes) {
135			if hz := o.Modes[o.Current].RefreshMilli / 1000; hz > 0 {
136				st.maxFPS = min(hz, 240)
137			}
138		}
139	}
140	s.nextStrm++
141	s.nextCast++
142	st.id, st.cast = s.nextStrm, s.nextCast
143	st.path = prefixStream + strconv.FormatUint(uint64(st.id), 10)
144	st.h2 = streamHandler{st: st}
145	if e := s.replyTo(req, "o"); e != nil {
146		e.ObjectPath(st.path)
147		s.send()
148	}
149}
150
151// start answers Start: the streams are opened on the wayland side.
152func (s *Service) start(req *request, sess *session) {
153	if sess.started {
154		s.replyErr(req, errFailed, "session already started")
155		return
156	}
157	n := 0
158	for i := range sess.streams {
159		if sess.streams[i].used {
160			sess.streams[i].needOpen = true
161			n++
162		}
163	}
164	if n == 0 {
165		s.replyErr(req, errFailed, "session has no stream")
166		return
167	}
168	sess.started = true
169	s.reply(req)
170}
171
172// closeSession ends a session: its streams are closed on the wayland side
173// and, with signal set, the owner is told. The slot is freed by reap.
174func (s *Service) closeSession(sess *session, signal bool) {
175	if sess.dying {
176		return
177	}
178	sess.dying = true
179	for i := range sess.streams {
180		if st := &sess.streams[i]; st.used {
181			st.close()
182			if !st.opened && st.src == nil {
183				*st = stream{}
184			}
185		}
186	}
187	if signal {
188		s.emitTo(sess.owner, sess.path, ifaceSession, "Closed")
189		s.send()
190	}
191	s.log.Info().Str("session", sess.path).Msg("screen cast session closed")
192}
193
194// outputByName finds an output of the inventory by connector.
195func (s *Service) outputByName(name string) *ports.CastOutput {
196	for i := range s.inv.Outputs {
197		if s.inv.Outputs[i].Info.Name == name {
198			return &s.inv.Outputs[i]
199		}
200	}
201	return nil
202}
203
204func (s *Service) haveWindow(id ports.WindowID) bool {
205	for i := range s.inv.Windows {
206		if s.inv.Windows[i].ID == id {
207			return true
208		}
209	}
210	return false
211}
212
213// properties answers Properties.Get, GetAll and Set.
214func (s *Service) properties(req *request, sess *session, st *stream) {
215	if req.method == mSet {
216		s.replyErr(req, errPropertyReadOnly, "no writable property")
217		return
218	}
219	iface := ""
220	switch req.obj {
221	case objScreenCast:
222		iface = ifaceScreenCast
223	case objSession:
224		iface = ifaceSession
225	case objStream:
226		iface = ifaceStream
227	case objDisplayConfig:
228		iface = ifaceDisplayConfig
229	case objIntrospect:
230		iface = ifaceIntrospect
231	case objScreenshot:
232		iface = ifaceScreenshot
233	}
234	if req.text != iface && !(req.method == mGetAll && req.text == "") {
235		s.replyErr(req, errUnknownInterface, "no such interface")
236		return
237	}
238	if req.method == mGetAll {
239		e := s.replyTo(req, "a{sv}")
240		if e == nil {
241			return
242		}
243		a := e.BeginArray('{')
244		switch req.obj {
245		case objScreenCast:
246			e.Struct()
247			e.Str("Version")
248			e.Variant("i")
249			e.Int32(screenCastVersion)
250		case objStream:
251			e.Struct()
252			e.Str("Parameters")
253			e.Variant("a{sv}")
254			encodeParameters(e, st)
255		case objIntrospect:
256			e.Struct()
257			e.Str("version")
258			e.Variant("u")
259			e.Uint32(introspectVersion)
260		}
261		e.EndArray(a)
262		s.send()
263		return
264	}
265	switch {
266	case req.obj == objScreenCast && req.text2 == "Version":
267		if e := s.replyTo(req, "v"); e != nil {
268			e.Variant("i")
269			e.Int32(screenCastVersion)
270			s.send()
271		}
272	case req.obj == objStream && req.text2 == "Parameters":
273		if e := s.replyTo(req, "v"); e != nil {
274			e.Variant("a{sv}")
275			encodeParameters(e, st)
276			s.send()
277		}
278	case req.obj == objIntrospect && req.text2 == "version":
279		if e := s.replyTo(req, "v"); e != nil {
280			e.Variant("u")
281			e.Uint32(introspectVersion)
282			s.send()
283		}
284	default:
285		s.replyErr(req, errUnknownProperty, "no such property")
286	}
287}
288
289// Versions served. Introspect version 2 would make x-d-p-g read
290// AnimationsEnabled and ScreenSize, which are not served.
291const (
292	screenCastVersion = 4
293	introspectVersion = 1
294	defaultFPS        = 60
295)
296
297func itoa(n int) string     { return strconv.Itoa(n) }
298func ftoa(f float64) string { return strconv.FormatFloat(f, 'f', 3, 64) }

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