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}