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