main 5d6fa26

security.go RawHistoryFolder

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

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