v0.2.0 13c6a90

security.go RawHistoryFolder

1package core
2
3import (
4	"context"
5	"slices"
6
7	"github.com/bnema/neferwl/internal/ports"
8)
9
10// syncSecurity runs only on the owner. The gate, not event selection order,
11// establishes the epoch; surface events can only fill in that exact epoch.
12func (c *Core) syncSecurity() bool {
13	if c.inputActive {
14		return false
15	}
16	if c.ch.Security == nil {
17		return false
18	}
19	state := c.ch.Security.Snapshot()
20	if state == c.security {
21		return false
22	}
23	c.security = state
24	c.lockSurfaces, c.lockFocus, c.lockPinned = nil, 0, false
25	c.pressed, c.buttons, c.inputKeys = map[string]bool{}, map[uint32]bool{}, map[string]bool{}
26	c.pointer, c.grab, c.pointerOutput = 0, 0, ""
27	c.drag, c.swallow, c.mods, c.lastButton = nil, map[uint32]bool{}, 0, 0
28	c.pointerAt, c.motionMsec = [2]float64{}, 0
29	c.keyboard = keyboard{}
30	c.constrained, c.constraint = ports.PointerConstrained{}, ports.PointerConstraint{}
31	if c.ch.Constraints != nil {
32		latest(c.ch.Constraints, c.constraint)
33	}
34	c.dropSwipe()
35	c.swipe = nil
36	for _, sc := range c.screens {
37		sc.mon.stopSwitch()
38		for w := range sc.mon.all() {
39			w.stopSlide()
40		}
41	}
42	c.stopFrame()
43	c.stopPulse()
44	if state.Protected {
45		c.dropCaptureSessions()
46	}
47	return true
48}
49
50// Check spawns without adopting a new epoch midway through processing an
51// event: commands emitted by that event must retain its original owner epoch.
52func (c *Core) inputEpochChanged() bool {
53	return c.inputActive && c.ch.Security != nil && c.ch.Security.Snapshot() != c.security
54}
55
56func (c *Core) protectionRequested() bool {
57	return c.security.Protected || c.ch.Security != nil && c.ch.Security.Snapshot() != c.security
58}
59
60func (c *Core) securityCheckpoint(ctx context.Context) error {
61	if !c.syncSecurity() {
62		return nil
63	}
64	// Clear display focus even if the new epoch has no mapped lock surfaces.
65	if err := c.command(ctx, ports.PointerFocus{}); err != nil {
66		return err
67	}
68	if err := c.command(ctx, ports.FocusWindow{}); err != nil {
69		return err
70	}
71	return c.publish(ctx)
72}
73
74// blockProtected reports whether a protected session drops ev. A dropped
75// fullscreen request is still answered, at the first publish after unlock.
76func (c *Core) blockProtected(ev ports.ClientEvent) bool {
77	if !c.security.Protected || !blockedProtectedEvent(ev) {
78		return false
79	}
80	if v, ok := ev.(ports.WindowFullscreenRequest); ok {
81		c.configures.answer[v.ID] = true
82	}
83	return true
84}
85
86func blockedProtectedEvent(ev ports.ClientEvent) bool {
87	switch ev.(type) {
88	case ports.WindowActivate, ports.WorkspaceActivate, ports.PointerWarp, ports.PointerConstrained,
89		ports.PopupRequest, ports.ShortcutsInhibit, ports.WindowFullscreenRequest,
90		ports.CaptureSessionOpen, ports.CaptureFrameTaken, ports.CaptureExclusionBegin, ports.CaptureExclusionLayer,
91		ports.WindowMoveRequest:
92		return true
93	}
94	return false
95}
96
97func (c *Core) applyLockChanged(v ports.SessionLockChanged) bool {
98	if c.ch.Security == nil || v.State != c.security || v.State != c.ch.Security.Snapshot() {
99		return false
100	}
101	if !c.security.Protected {
102		return true
103	}
104	c.lockSurfaces = slices.Clone(v.Surfaces)
105	c.lockKeyboardFocus()
106	// A destroyed or resized role must not retain a pointer grab.
107	if !c.validLockID(c.pointer) {
108		c.pointer = 0
109	}
110	if !c.validLockID(c.grab) {
111		c.grab = 0
112		clear(c.buttons)
113	}
114	return true
115}
116
117func (c *Core) lockSurface(sc *screen) (ports.LockSurfacePlacement, bool) {
118	o := sc.mon.Output()
119	for _, s := range c.lockSurfaces {
120		if s.ID != 0 && sc.name() != "" && s.Output == sc.name() && s.Width == o.W && s.Height == o.H && o.W > 0 && o.H > 0 {
121			return s, true
122		}
123	}
124	return ports.LockSurfacePlacement{}, false
125}
126
127func (c *Core) validLockID(id WindowID) bool {
128	if id == 0 {
129		return false
130	}
131	for _, sc := range c.screens {
132		if s, ok := c.lockSurface(sc); ok && s.ID == id {
133			return true
134		}
135	}
136	return false
137}
138
139// lockKeyboardFocus prefers the lock surface of the focused output, where the
140// user is looking, then any other. A surface the user clicked or typed into is
141// pinned and keeps focus. Until then, focus moves to the focused output's
142// surface once it maps: lock clients map their surfaces one by one, in any
143// order.
144func (c *Core) lockKeyboardFocus() WindowID {
145	valid := c.validLockID(c.lockFocus)
146	if valid && c.lockPinned {
147		return c.lockFocus
148	}
149	if c.focusScreen >= 0 && c.focusScreen < len(c.screens) {
150		if s, ok := c.lockSurface(c.screens[c.focusScreen]); ok {
151			c.lockFocus, c.lockPinned = s.ID, false
152			return c.lockFocus
153		}
154	}
155	if valid {
156		return c.lockFocus
157	}
158	c.lockFocus, c.lockPinned = 0, false
159	for _, sc := range c.screens {
160		if s, ok := c.lockSurface(sc); ok {
161			c.lockFocus = s.ID
162			break
163		}
164	}
165	return c.lockFocus
166}
167
168func (c *Core) lockHit(x, y float64) (WindowID, float64, float64) {
169	o, ok := c.layout().At(x, y)
170	if !ok {
171		return 0, 0, 0
172	}
173	i := c.screenIndex(o.Info.Name)
174	if i < 0 {
175		return 0, 0, 0
176	}
177	if s, ok := c.lockSurface(c.screens[i]); ok {
178		return s.ID, x - float64(o.X), y - float64(o.Y)
179	}
180	return 0, 0, 0
181}
182
183func (c *Core) publishProtected(ctx context.Context) error {
184	if err := c.syncOutputs(ctx); err != nil {
185		return err
186	}
187	focus := c.lockKeyboardFocus()
188	scenes := make([]ports.Scene, 0, len(c.screens))
189	for _, sc := range c.screens {
190		c.seq++
191		o := sc.mon.Output()
192		scene := ports.Scene{Security: c.security, Output: sc.name(), Seq: c.seq, OutputWidth: o.W, OutputHeight: o.H, Scale: sc.scale, Off: sc.off, Background: "#000000"}
193		if s, ok := c.lockSurface(sc); ok {
194			scene.Windows = []ports.SceneWindow{{ID: s.ID, Rect: Rect{W: o.W, H: o.H}, Fullscreen: true, Focused: s.ID == focus}}
195		}
196		scenes = append(scenes, scene)
197	}
198	if err := c.syncFocus(ctx, focus); err != nil {
199		return err
200	}
201	// Surface updates may invalidate the role under a stationary pointer.
202	if !c.validLockID(c.pointer) {
203		c.pointer = 0
204	}
205	if err := c.command(ctx, ports.PointerFocus{ID: c.pointer, X: c.pointerAt[0], Y: c.pointerAt[1]}); err != nil {
206		return err
207	}
208	latest(c.ch.Scenes, scenes)
209	c.publishState()
210	c.publishWorkspaces()
211	return nil
212}
213
214func (c *Core) admitInput(ev ports.InputEvent) (ports.InputEvent, bool) {
215	if v, ok := ev.(ports.SecurityInput); ok {
216		if v.State != c.security || v.Event == nil {
217			return nil, false
218		}
219		ev = v.Event
220	} else if c.ch.Security != nil || c.security != (ports.SecurityState{}) {
221		return nil, false
222	}
223	// The producer resets native keymap state and quarantines physically held
224	// keys on transitions. Core preserves that keymap-aware modifier state;
225	// it must not infer modifier meaning from evdev codes or XKB bit positions.
226	// No release without a press in this epoch (password keys must not reach
227	// the desktop after unlock). Standalone legacy channel tests remain raw.
228	if c.ch.Security != nil {
229		if b, ok := ev.(ports.PointerButton); ok && !b.Pressed && !c.buttons[b.Button] {
230			return nil, false
231		}
232		if k, ok := ev.(ports.KeyEvent); ok {
233			held := heldKey(k)
234			if k.Pressed {
235				c.inputKeys[held] = true
236			} else {
237				if !c.inputKeys[held] {
238					return nil, false
239				}
240				delete(c.inputKeys, held)
241			}
242		}
243	}
244	return ev, true
245}
246
247func (c *Core) protectedInput(ctx context.Context, ev ports.InputEvent) error {
248	switch v := ev.(type) {
249	case ports.KeyEvent:
250		if id := c.lockKeyboardFocus(); id != 0 {
251			c.lockPinned = true // typing must not move focus mid-secret
252			return c.command(ctx, ports.ForwardKey{ID: id, Key: v})
253		}
254	case ports.PointerMotion:
255		c.motionMsec = v.TimeMsec
256		c.cursorX, c.cursorY = c.layout().Clamp(c.cursorX, c.cursorY, v.X, v.Y)
257		id, x, y := c.lockHit(c.cursorX, c.cursorY)
258		if id != c.pointer {
259			c.pointer = id
260			if err := c.command(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil {
261				return err
262			}
263		}
264		c.pointerAt = [2]float64{x, y}
265		if id != 0 {
266			return c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, DX: v.DX, DY: v.DY, UnaccelDX: v.UnaccelDX, UnaccelDY: v.UnaccelDY, TimeMsec: v.TimeMsec, TimeUsec: v.TimeUsec})
267		}
268	case ports.PointerButton:
269		id := c.pointer
270		if c.grab != 0 {
271			id = c.grab
272		}
273		if !v.Pressed && !c.buttons[v.Button] {
274			return nil
275		}
276		if v.Pressed {
277			if len(c.buttons) == 0 {
278				c.grab = id
279			}
280			c.buttons[v.Button] = true
281		} else {
282			delete(c.buttons, v.Button)
283		}
284		if c.validLockID(id) {
285			if v.Pressed {
286				changed := c.lockFocus != id
287				c.lockFocus, c.lockPinned = id, true
288				if changed {
289					if err := c.publishProtected(ctx); err != nil {
290						return err
291					}
292				}
293			}
294			if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, TimeMsec: v.TimeMsec}); err != nil {
295				return err
296			}
297		}
298		if len(c.buttons) == 0 {
299			c.grab = 0
300		}
301	case ports.PointerAxis:
302		if c.validLockID(c.pointer) {
303			return c.command(ctx, ports.PointerAxisTo{ID: c.pointer, Axis: v})
304		}
305	}
306	return nil
307}

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