v0.3.0 cf370a0

workspaces.go RawHistoryFolder

1package wayland
2
3import (
4	"context"
5	"encoding/binary"
6
7	ext "github.com/bnema/go-wayland-bindings/server/extworkspace"
8	"github.com/bnema/go-wayland-bindings/server/wayland"
9	"github.com/bnema/neferwl/internal/adapters/wayland/imagecapture"
10	"github.com/bnema/neferwl/internal/ports"
11	"github.com/bnema/purego-libwayland/server"
12)
13
14type workspaceManager struct {
15	s       *Server
16	res     *ext.ExtWorkspaceManagerV1
17	groups  map[string]*workspaceGroup
18	handles map[uint64]*workspaceHandle
19	pending []uint64
20}
21type workspaceGroup struct {
22	res     *ext.ExtWorkspaceGroupHandleV1
23	outputs map[*server.Resource]bool // output_enter sent for each live wl_output
24}
25type workspaceHandle struct {
26	res   *ext.ExtWorkspaceHandleV1
27	info  ports.WorkspaceInfo
28	group string
29}
30
31func registerWorkspaces(d *server.Display, s *Server) error {
32	return ext.NewExtWorkspaceManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
33		m := &workspaceManager{s: s, groups: map[string]*workspaceGroup{}, handles: map[uint64]*workspaceHandle{}}
34		r, err := ext.NewExtWorkspaceManagerV1(c, int32(v), id, m)
35		if err != nil {
36			return
37		}
38		m.res = r
39		s.workspaceManagers = append(s.workspaceManagers, m)
40		r.OnDestroy = func() { s.workspaceManagers = removeItem(s.workspaceManagers, m) }
41		m.update(s.workspaceSnapshot)
42	})
43}
44func (m *workspaceManager) Commit(*ext.ExtWorkspaceManagerV1) {
45	if m.s.protected() {
46		m.pending = nil
47		return
48	}
49	ids := make([]uint64, 0, len(m.pending))
50	for _, id := range m.pending {
51		if h := m.handles[id]; h != nil && h.res.Alive() {
52			ids = append(ids, id)
53		}
54	}
55	m.pending = nil
56	if len(ids) > 0 {
57		m.s.emit(ports.WorkspaceActivate{IDs: ids})
58	}
59}
60func (m *workspaceManager) Stop(*ext.ExtWorkspaceManagerV1) {
61	m.res.SendFinished()
62	m.res.Destroy()
63}
64func (*workspaceManager) CreateWorkspace(*ext.ExtWorkspaceGroupHandleV1, string) {}
65func (*workspaceManager) Destroy(*ext.ExtWorkspaceGroupHandleV1)                 {}
66func (m *workspaceManager) Activate(r *ext.ExtWorkspaceHandleV1) {
67	if m.s.protected() {
68		return
69	}
70	for id, h := range m.handles {
71		if h.res.Resource == r.Resource {
72			m.pending = append(m.pending, id)
73			return
74		}
75	}
76}
77
78// workspaceRequests adapts the workspace destroy request separately from group destroy.
79type workspaceRequests struct{ m *workspaceManager }
80
81func (*workspaceRequests) Destroy(*ext.ExtWorkspaceHandleV1)                                {}
82func (w *workspaceRequests) Activate(r *ext.ExtWorkspaceHandleV1)                           { w.m.Activate(r) }
83func (*workspaceRequests) Deactivate(*ext.ExtWorkspaceHandleV1)                             {}
84func (*workspaceRequests) Assign(*ext.ExtWorkspaceHandleV1, *ext.ExtWorkspaceGroupHandleV1) {}
85func (*workspaceRequests) Remove(*ext.ExtWorkspaceHandleV1)                                 {}
86
87// outputBound announces every wl_output resource bound by this client for an
88// assigned output. A client may bind the same global more than once.
89func (m *workspaceManager) outputBound(name string) bool {
90	if m.s.protected() {
91		return false
92	}
93	g := m.groups[name]
94	o := m.s.outputByNameExact(name)
95	if g == nil || !g.res.Alive() || o == nil {
96		return false
97	}
98	if g.outputs == nil {
99		g.outputs = make(map[*server.Resource]bool)
100	}
101	changed := false
102	for _, r := range o.resources {
103		if r.Client() != m.res.Client() || !r.Alive() || g.outputs[r.Resource] {
104			continue
105		}
106		g.outputs[r.Resource] = true
107		g.res.SendOutputEnter(r)
108		changed = true
109	}
110	return changed
111}
112
113func (m *workspaceManager) leaveOutputs(g *workspaceGroup) {
114	if !g.res.Alive() {
115		return
116	}
117	for res := range g.outputs {
118		if res.Alive() {
119			g.res.SendOutputLeave(wayland.WrapOutput(res))
120		}
121	}
122}
123func (s *Server) forwardWorkspaces(ctx context.Context) {
124	for {
125		select {
126		case <-ctx.Done():
127			return
128		case <-s.display.Stopped():
129			return
130		case snapshot, ok := <-s.channels.Workspaces:
131			if !ok {
132				return
133			}
134			if !s.display.Do(func() { s.updateWorkspaces(snapshot) }) {
135				return
136			}
137		}
138	}
139}
140func (s *Server) updateWorkspaces(snapshot ports.Workspaces) {
141	s.workspaceSnapshot = snapshot
142	s.updateWorkspaceManagers(snapshot)
143}
144
145// updateWorkspaceManagers also refreshes inventory on protection transitions.
146// Keep the last sent state while protected: old client knowledge cannot be
147// revoked, but an unlock refresh must compare against it, not suppressed updates.
148func (s *Server) updateWorkspaceManagers(snapshot ports.Workspaces) {
149	for _, m := range s.workspaceManagers {
150		m.update(snapshot)
151	}
152	s.updateWorkspaceFrames()
153	s.refreshSessions()
154}
155
156// workspaceFrame is a neferwl_workspace_frame_v1: the frame of one workspace,
157// output-local logical, sent when it is created and when it changes. Nothing
158// is sent for a workspace that is gone, nor while the session is protected.
159type workspaceFrame struct {
160	res    *imagecapture.NeferwlWorkspaceFrameV1
161	handle *server.Resource
162	id     uint64
163	sent   bool
164	last   ports.Rect
165}
166
167func (*workspaceFrame) Destroy(*imagecapture.NeferwlWorkspaceFrameV1) {}
168
169func (s *Server) newWorkspaceFrame(c server.Client, id uint32, ws *ext.ExtWorkspaceHandleV1) {
170	f := &workspaceFrame{}
171	known := false
172	if ws != nil {
173		f.handle = ws.Resource
174		f.id, known = s.workspaceIDOf(ws.Resource)
175	}
176	r, err := imagecapture.NewNeferwlWorkspaceFrameV1(c, 1, id, f)
177	if err != nil {
178		return
179	}
180	f.res = r
181	if !known {
182		return
183	}
184	s.workspaceFrames = append(s.workspaceFrames, f)
185	r.OnDestroy = func() { s.workspaceFrames = removeItem(s.workspaceFrames, f) }
186	s.updateWorkspaceFrames()
187}
188
189// updateWorkspaceFrames sends the frame of each workspace whose last sent one
190// differs. A removed workspace gets nothing and its entry is dropped.
191func (s *Server) updateWorkspaceFrames() {
192	if s.protected() {
193		return
194	}
195	kept := s.workspaceFrames[:0]
196	for _, f := range s.workspaceFrames {
197		// The handle must still exist: a removed or renamed handle never
198		// returns, so its frame object is dropped.
199		if id, ok := s.workspaceIDOf(f.handle); !ok || id != f.id {
200			continue
201		}
202		kept = append(kept, f)
203		if !f.res.Alive() {
204			continue
205		}
206		_, w, ok := s.workspaceInfo(f.id)
207		if !ok || (f.sent && f.last == w.Frame) {
208			continue
209		}
210		f.sent, f.last = true, w.Frame
211		f.res.SendFrame(int32(w.Frame.X), int32(w.Frame.Y), int32(w.Frame.W), int32(w.Frame.H))
212	}
213	clear(s.workspaceFrames[len(kept):])
214	s.workspaceFrames = kept
215}
216
217func workspaceState(w ports.WorkspaceInfo) uint32 {
218	var state uint32
219	if w.Active {
220		state |= uint32(ext.ExtWorkspaceHandleV1StateActive)
221	}
222	if w.Hidden {
223		state |= uint32(ext.ExtWorkspaceHandleV1StateHidden)
224	}
225	return state
226}
227func (m *workspaceManager) sendCoordinates(r *ext.ExtWorkspaceHandleV1, index int) {
228	var b [4]byte
229	binary.NativeEndian.PutUint32(b[:], uint32(index))
230	r.SendCoordinates(b[:])
231}
232func (m *workspaceManager) update(snapshot ports.Workspaces) {
233	if m.s.protected() {
234		m.pending = nil
235		return
236	}
237	if !m.res.Alive() {
238		return
239	}
240	wanted := map[string]bool{}
241	infos := map[uint64]ports.WorkspaceInfo{}
242	for _, out := range snapshot.Outputs {
243		wanted[out.Name] = true
244		for _, w := range out.Workspaces {
245			infos[w.ID] = w
246		}
247	}
248	// Move existing workspaces before retiring their old output group.
249	for id, h := range m.handles {
250		// A handle's id is fixed: a workspace that became configured or
251		// unconfigured (a rename in the config) is a removed handle and a
252		// new one.
253		if info, exists := infos[id]; !exists || info.Configured != h.info.Configured {
254			if g := m.groups[h.group]; g != nil && g.res.Alive() && h.res.Alive() {
255				g.res.SendWorkspaceLeave(h.res)
256			}
257			if h.res.Alive() {
258				h.res.SendRemoved()
259			}
260			delete(m.handles, id)
261		}
262	}
263	for _, out := range snapshot.Outputs {
264		g := m.groups[out.Name]
265		if g == nil {
266			r, err := ext.NewExtWorkspaceGroupHandleV1(m.res.Client(), 1, 0, m)
267			if err != nil {
268				continue
269			}
270			g = &workspaceGroup{res: r, outputs: map[*server.Resource]bool{}}
271			m.groups[out.Name] = g
272			m.res.SendWorkspaceGroup(r)
273			r.SendCapabilities(0)
274		}
275		m.outputBound(out.Name)
276		for _, w := range out.Workspaces {
277			if !g.res.Alive() {
278				continue
279			}
280			h := m.handles[w.ID]
281			if h == nil {
282				r, err := ext.NewExtWorkspaceHandleV1(m.res.Client(), 1, 0, &workspaceRequests{m})
283				if err != nil {
284					continue
285				}
286				h = &workspaceHandle{res: r}
287				m.handles[w.ID] = h
288				m.res.SendWorkspace(r)
289				r.SendId(m.s.workspaceIDs.ID(w.ID, w.Configured))
290				r.SendName(w.Name)
291				m.sendCoordinates(r, w.Index)
292				r.SendState(workspaceState(w))
293				r.SendCapabilities(uint32(ext.ExtWorkspaceHandleV1WorkspaceCapabilitiesActivate))
294				h.info = w
295			}
296			if !h.res.Alive() {
297				continue
298			}
299			if h.info.ID != w.ID {
300				continue
301			}
302			if h.info != w {
303				if h.info.Name != w.Name {
304					h.res.SendName(w.Name)
305				}
306				if h.info.Index != w.Index {
307					m.sendCoordinates(h.res, w.Index)
308				}
309				if h.info.Active != w.Active || h.info.Hidden != w.Hidden {
310					h.res.SendState(workspaceState(w))
311				}
312				h.info = w
313			}
314			if h.group != out.Name {
315				if old := m.groups[h.group]; old != nil && old.res.Alive() && h.res.Alive() {
316					old.res.SendWorkspaceLeave(h.res)
317				}
318				g.res.SendWorkspaceEnter(h.res)
319				h.group = out.Name
320			}
321		}
322	}
323	for name, g := range m.groups {
324		if !wanted[name] {
325			m.leaveOutputs(g)
326			if g.res.Alive() {
327				g.res.SendRemoved()
328			}
329			delete(m.groups, name)
330		}
331	}
332	m.res.SendDone()
333}

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