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}