1package core
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "slices"
8 "strconv"
9 "strings"
10 "time"
11 "unicode"
12
13 "github.com/bnema/neferwl/internal/ports"
14)
15
16var ErrQuit = errors.New("quit requested")
17
18var errSecurityChanged = errors.New("security epoch changed during input")
19
20// Channels connects the owner to adapters. Scenes must have capacity 1: core is
21// its only sender and may drain a stale set; consumers only receive. Each
22// send holds one scene per output.
23type Channels struct {
24 // Security is the defensive session gate; nil preserves standalone operation.
25 Security ports.SessionSecurity
26 Client <-chan ports.ClientEvent
27 Input <-chan ports.InputEvent
28 Output <-chan ports.OutputEvent
29 Config <-chan ports.ConfigChanged
30 Commands chan<- ports.ClientCommand
31 Spawn chan<- ports.SpawnRequest
32 Scenes chan []ports.Scene
33 // Layouts, when set, receives the output layout whenever it changes,
34 // latest first (capacity 1, drained like Scenes). Input and cursors use it.
35 Layouts chan ports.Layout
36 // Constraints, when set, receives the active pointer constraint in
37 // global logical coordinates whenever it changes (capacity 1, drained
38 // like Scenes). Input applies it to the pointer.
39 Constraints chan ports.PointerConstraint
40 ConfigErrors chan<- error
41 // State, when set, receives a snapshot for scripts whenever it changes,
42 // latest first (capacity 1, drained like Scenes).
43 State chan ports.State
44 Workspaces chan ports.Workspaces
45 // Scales, when set, receives each scale a scale bind sets, to persist it.
46 // Core does not wait: a full channel drops the change.
47 Scales chan<- ports.ScaleChanged
48 // Terminal enables automatic terminal opening according to terminal.auto-open.
49 Terminal bool
50 // Clock tells the time for fullscreenGrace and slides; nil is the
51 // system clock.
52 Clock ports.Clock
53 // Frames, when set, reports outputs' page flips: a running slide moves
54 // one step per flip. Without flips it moves on a timer.
55 Frames <-chan ports.OutputFrame
56}
57
58// fullscreenGrace is how long after mapping a window's fullscreen request
59// is ignored. Wine asks for fullscreen whenever a window has the monitor's
60// size, so a launcher that remembered a fullscreen size would open
61// fullscreen and hide the windows it opens next. The user can still
62// fullscreen it (bind or taskbar), and an app can ask again later.
63const fullscreenGrace = time.Second
64
65type binding struct {
66 mods ports.Mods
67 key string
68}
69type Core struct {
70 security ports.SecurityState
71 lockSurfaces []ports.LockSurfacePlacement
72 lockFocus WindowID
73 lockPinned bool // lockFocus was clicked or typed into: it keeps focus
74 inputKeys map[string]bool
75 inputActive bool
76 startup [][]string
77 nextWorkspaceID uint64
78 ch Channels
79 cfg ports.Config
80 screens []*screen
81 focusScreen int
82 binds map[binding]Action
83 pressed map[string]bool
84 configures configures
85 pointer WindowID
86 grab WindowID
87 buttons map[uint32]bool
88 // drag is the pointer drag in progress (drag.go); swallow holds the
89 // buttons whose release no client must see. mods are the modifiers
90 // held, cmdMod the one keyboard.cmd names. lastButton is the latest
91 // press a client saw, the one xdg_toplevel.move's serial names.
92 drag *dragState
93 swallow map[uint32]bool
94 lastButton uint32
95 mods ports.Mods
96 cmdMod ports.Mods
97 cursorX, cursorY float64 // global, logical
98 pointerOutput string // output under the pointer at the last motion
99 // constrained is the constraint wayland activated; constraint is it
100 // resolved to global coordinates, as last sent to input.
101 constrained ports.PointerConstrained
102 constraint ports.PointerConstraint
103 seq uint64
104 // layerChanged is set once layer state arrives from wayland.
105 layerChanged bool
106 sentOutputs ports.SetOutputs
107 specs []NamedWorkspace
108 presets []Width
109 slots map[slotKey]*slotState
110 placement spawnPlacement
111 // toSpawn holds slots to start; Run sends them (apply has no context).
112 toSpawn []slotKey
113 sentPending bool
114 firstTerminalResolved bool
115 windows windowRegistry
116 sentState ports.State
117 sentWorkspaces ports.Workspaces
118 // popups are the placed xdg_popups; popupOrder stacks them, oldest first.
119 popups map[WindowID]*popupState
120 popupOrder []WindowID
121 keyboard keyboard
122 // activity is when core last told wayland about user input
123 // (ports.UserActivity), sent at most once per ActivityInterval.
124 activity time.Time
125 // swipe is the touchpad swipe in progress. frameC fires when no page
126 // flip came in time to move a running slide (frameStop stops it).
127 swipe *swipeGesture
128 frameC <-chan time.Time
129 frameStop func() bool
130 // motionMsec is the time of the last pointer motion; pointerAt is the
131 // last position sent in the pointer's window.
132 motionMsec uint32
133 pointerAt [2]float64
134 // capt is the capture state (capture.go, capindicator.go).
135 capt captureState
136 // pulse marks a window that just got the focus (pulse.go).
137 pulse focusPulse
138}
139
140func keyName(s string) string {
141 if len(s) == 1 && unicode.IsLetter(rune(s[0])) {
142 return strings.ToLower(s)
143 }
144 return s
145}
146func parseBind(s, cmd string) (binding, error) {
147 parts := strings.Split(s, "+")
148 b := binding{key: keyName(parts[len(parts)-1])}
149 if b.key == "" {
150 return b, fmt.Errorf("invalid bind %q", s)
151 }
152 for _, p := range parts[:len(parts)-1] {
153 var m ports.Mods
154 switch p {
155 case "Cmd":
156 switch cmd {
157 case "super":
158 m = ports.ModSuper
159 case "alt":
160 m = ports.ModAlt
161 case "ctrl":
162 m = ports.ModCtrl
163 }
164 case "Shift":
165 m = ports.ModShift
166 case "Ctrl":
167 m = ports.ModCtrl
168 case "Alt":
169 m = ports.ModAlt
170 case "Super":
171 m = ports.ModSuper
172 }
173 if m == 0 || b.mods&m != 0 {
174 return b, fmt.Errorf("invalid bind %q", s)
175 }
176 b.mods |= m
177 }
178 return b, nil
179}
180func (c *Core) apply(cfg ports.Config) error {
181 if cfg.Layout.MaxColumns < 1 {
182 return fmt.Errorf("invalid max columns")
183 }
184 presets := make([]Width, 0, len(cfg.Layout.Presets))
185 for _, s := range cfg.Layout.Presets {
186 v, e := ParseWidth(s)
187 if e != nil {
188 return e
189 }
190 presets = append(presets, v)
191 }
192 if cfg.Layout.Gaps < 0 {
193 return fmt.Errorf("invalid gaps")
194 }
195 if cfg.Keyboard.CmdKey != "super" && cfg.Keyboard.CmdKey != "alt" && cfg.Keyboard.CmdKey != "ctrl" {
196 return fmt.Errorf("invalid cmd key")
197 }
198 binds := map[binding]Action{}
199 for combo, a := range cfg.Binds {
200 b, e := parseBind(combo, cfg.Keyboard.CmdKey)
201 if e != nil {
202 return e
203 }
204 if _, ok := binds[b]; ok {
205 return fmt.Errorf("duplicate bind %q", combo)
206 }
207 if _, ok := SpawnArgv(Action(a)); ok {
208 binds[b] = Action(a)
209 continue
210 }
211 if Action(a) == ActionScaleUp || Action(a) == ActionScaleDown {
212 binds[b] = Action(a)
213 continue
214 }
215 if _, _, ok := WorkspaceArg(Action(a)); ok {
216 binds[b] = Action(a)
217 continue
218 }
219 if _, _, ok := ResizeArg(Action(a)); ok {
220 binds[b] = Action(a)
221 continue
222 }
223 if _, ok := NamedArg(Action(a)); ok {
224 binds[b] = Action(a)
225 continue
226 }
227 switch Action(a) {
228 case "none", ActionSpawnTerminal, ActionFocusColumnLeft, ActionFocusColumnRight, ActionFocusWindowUp, ActionFocusWindowDown, ActionMoveColumnLeft, ActionMoveColumnRight, ActionCycleColumnWidth, ActionMaximizeColumn, ActionToggleFullscreen, ActionToggleWindowStash, ActionToggleStashVisible, ActionToggleOverview, ActionCloseWindow, ActionQuit, ActionFocusWorkspaceUp, ActionFocusWorkspaceDown, ActionMoveColumnToWorkspaceUp, ActionMoveColumnToWorkspaceDown, ActionMoveWindowToWorkspaceUp, ActionMoveWindowToWorkspaceDown, ActionFocusMonitorLeft, ActionFocusMonitorRight, ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight, ActionConsumeOrExpelLeft, ActionConsumeOrExpelRight,
229 ActionMoveWindowUp, ActionMoveWindowDown, ActionMoveWorkspaceUp, ActionMoveWorkspaceDown, ActionToggleFloating:
230 default:
231 return fmt.Errorf("invalid action %q", a)
232 }
233 binds[b] = Action(a)
234 }
235 overflow := func(v string) (Overflow, error) {
236 switch o := Overflow(v); o {
237 case "", OverflowScroll, OverflowFixed:
238 return o, nil
239 }
240 return "", fmt.Errorf("invalid overflow %q", v)
241 }
242 if _, err := overflow(cfg.Layout.Overflow); err != nil {
243 return err
244 }
245 if cfg.Layout.Overflow == "" {
246 cfg.Layout.Overflow = string(OverflowScroll)
247 }
248 for _, o := range cfg.Layout.Outputs {
249 if _, err := overflow(o.Overflow); err != nil {
250 return err
251 }
252 if o.Output == "" || o.MaxColumns < 0 {
253 return fmt.Errorf("invalid layout for output %q", o.Output)
254 }
255 }
256 named := make([]NamedWorkspace, 0, len(cfg.Workspaces))
257 seen := map[string]bool{}
258 for _, ws := range cfg.Workspaces {
259 o, err := overflow(ws.Overflow)
260 if err != nil {
261 return err
262 }
263 if ws.Name == "" || seen[ws.Name] || ws.MaxColumns < 0 || ws.Size[0] < 0 || ws.Size[1] < 0 {
264 return fmt.Errorf("invalid workspace %q", ws.Name)
265 }
266 seen[ws.Name] = true
267 named = append(named, NamedWorkspace{Name: ws.Name, Monitor: ws.Monitor, MaxColumns: ws.MaxColumns, Overflow: o, Size: ws.Size})
268 }
269 specs, err := parseSlots(cfg.Workspaces)
270 if err != nil {
271 return err
272 }
273 c.cfg = cfg
274 c.binds = binds
275 c.cmdMod = map[string]ports.Mods{"super": ports.ModSuper, "alt": ports.ModAlt, "ctrl": ports.ModCtrl}[cfg.Keyboard.CmdKey]
276 c.specs, c.presets = named, presets
277 c.toSpawn = append(c.toSpawn, c.updateSlots(specs)...)
278 c.named()
279 for _, s := range c.screens {
280 c.settings(s.mon)
281 }
282 c.applyConfigScales()
283 return nil
284}
285
286// publishPending tells wayland at once when slots start waiting, before
287// their windows can map.
288func (c *Core) publishPending(ctx context.Context) error {
289 if p := c.placement.anyPending(); p != c.sentPending {
290 if err := c.command(ctx, ports.SlotsPending{Pending: p}); err != nil {
291 return err
292 }
293 c.sentPending = p
294 }
295 return nil
296}
297
298// spawnSlots starts the slots queued by config. With shown set (the user
299// just switched to another workspace), it also refills the empty slots of
300// the workspace now on screen.
301func (c *Core) spawnSlots(ctx context.Context, shown bool) error {
302 if c.protectionRequested() {
303 return nil
304 }
305 keys := c.toSpawn
306 c.toSpawn = nil
307 if shown {
308 keys = append(keys, c.refill()...)
309 }
310 started := map[slotKey]bool{}
311 for i, key := range keys {
312 if c.protectionRequested() {
313 c.toSpawn = append(c.toSpawn, keys[i:]...)
314 return nil
315 }
316 st, ok := c.slots[key]
317 // A key can be queued twice (new slot on the workspace on screen).
318 if !ok || started[key] || st.window != 0 {
319 continue
320 }
321 started[key] = true
322 req := c.spawnSlot(key)
323 // Wayland must read slot tokens before this window can map.
324 pendingErr := c.publishPending(ctx)
325 if pendingErr != nil || c.protectionRequested() {
326 c.placement.dropSlot(key)
327 st.stale = false
328 c.toSpawn = append(c.toSpawn, keys[i:]...)
329 return pendingErr
330 }
331 if !c.trySpawn(ctx, req) {
332 c.placement.dropSlot(key)
333 st.stale = false
334 c.toSpawn = append(c.toSpawn, keys[i:]...)
335 return ctx.Err()
336 }
337 }
338 return nil
339}
340func New(cfg ports.Config, ch Channels) (*Core, error) {
341 if cap(ch.Scenes) != 1 || (ch.Layouts != nil && cap(ch.Layouts) != 1) || (ch.Constraints != nil && cap(ch.Constraints) != 1) || (ch.State != nil && cap(ch.State) != 1) || (ch.Workspaces != nil && cap(ch.Workspaces) != 1) {
342 return nil, fmt.Errorf("scenes, layouts, constraints, state and workspaces must have capacity 1")
343 }
344 // A placeholder screen holds windows until the first output arrives.
345 c := &Core{inputKeys: map[string]bool{}, slots: map[slotKey]*slotState{}, placement: newSpawnPlacement(), windows: newWindowRegistry(), popups: map[WindowID]*popupState{}, ch: ch, pressed: map[string]bool{}, buttons: map[uint32]bool{}, swallow: map[uint32]bool{}, configures: newConfigures()}
346 c.screens = []*screen{{mon: newMonitorWithIDs("", "", &c.nextWorkspaceID), scale: 1, cfgScale: 1}}
347 if err := c.apply(cfg); err != nil {
348 return nil, err
349 }
350 return c, nil
351}
352
353// newTimer makes a timer on clock, or the system timer when clock is nil.
354func newTimer(clock ports.Clock, d time.Duration) (<-chan time.Time, func() bool) {
355 if clock != nil {
356 t := clock.NewTimer(d)
357 return t.C(), t.Stop
358 }
359 t := time.NewTimer(d)
360 return t.C, t.Stop
361}
362
363func (c *Core) now() time.Time {
364 if c.ch.Clock != nil {
365 return c.ch.Clock.Now()
366 }
367 return time.Now()
368}
369
370func (c *Core) command(ctx context.Context, v ports.ClientCommand) error {
371 if c.inputEpochChanged() {
372 return errSecurityChanged
373 }
374 if c.ch.Security != nil {
375 v = ports.SecurityCommand{State: c.security, Command: v}
376 }
377 select {
378 case <-ctx.Done():
379 return ctx.Err()
380 case c.ch.Commands <- v:
381 if c.inputEpochChanged() {
382 return errSecurityChanged
383 }
384 return nil
385 }
386}
387
388// clientRect is the placement minus the border on its inset sides.
389// The renderer applies the same inset (ports.Border).
390func (c *Core) clientRect(p Placement) Rect {
391 if p.Fullscreen {
392 return p.Rect
393 }
394 return p.Rect.Inset(p.Inset, c.cfg.Border.Width)
395}
396
397// onScreen reports whether a placement is drawn on its output o: not
398// hidden and not scrolled off.
399func onScreen(p Placement, o Rect) bool {
400 return !p.Hidden && p.Rect.Overlaps(o)
401}
402
403// floatDim is the veil opacity of a layout: dim only when a float is
404// drawn above the tiles, never for a demoted covering float alone nor for
405// an overview preview.
406func floatDim(layout []Placement, o Rect, dim float64) float64 {
407 shown := false
408 for _, p := range layout {
409 if p.Floating && !p.Below && p.Preview == 0 && onScreen(p, o) {
410 if p.Fullscreen {
411 return 0
412 }
413 shown = true
414 }
415 }
416 if !shown {
417 return 0
418 }
419 return dim
420}
421
422// visible reports whether the window, layer surface or popup is on screen
423// on any output.
424func (c *Core) visible(id WindowID) bool {
425 _, _, ok := c.windowRect(id)
426 return ok
427}
428
429// windowFocus is the focused window of each output.
430func (c *Core) windowFocus() map[*screen]WindowID {
431 m := make(map[*screen]WindowID, len(c.screens))
432 for _, sc := range c.screens {
433 m[sc], _ = sc.mon.Focused()
434 }
435 return m
436}
437
438// keyboardFocus is the surface holding the keyboard (see keyboard.focus).
439func (c *Core) keyboardFocus() WindowID {
440 if c.security.Protected {
441 return c.lockKeyboardFocus()
442 }
443 var exclusive WindowID
444 for _, sc := range c.screens {
445 for _, l := range sc.layers {
446 if l.Keyboard == 1 && (l.Layer == ports.LayerTop || l.Layer == ports.LayerOverlay) && l.ID > exclusive {
447 exclusive = l.ID
448 }
449 }
450 }
451 window, _ := c.cur().mon.Focused()
452 return c.keyboard.focus(keyboardCandidates{
453 exclusive: exclusive,
454 grab: c.grabFocus(),
455 window: window,
456 layerShown: func(id WindowID) bool {
457 return c.onDemand(id) && c.visible(id)
458 },
459 windows: c.windowFocus,
460 })
461}
462
463// setLayers splits the mapped layer surfaces by output. A surface without an
464// output, or on an unknown one, goes to the focused output.
465func (c *Core) setLayers(all []ports.LayerSurface) {
466 previous := make(map[WindowID]bool)
467 for _, sc := range c.screens {
468 for _, l := range sc.layers {
469 previous[l.ID] = true
470 }
471 sc.layers = nil
472 }
473 for _, l := range all {
474 delete(previous, l.ID)
475 sc := c.cur()
476 if i := c.screenIndex(l.Output); i >= 0 {
477 sc = c.screens[i]
478 }
479 sc.layers = append(sc.layers, l)
480 }
481 for id := range previous {
482 c.windows.layerGone(id)
483 }
484 c.pruneRetained()
485 c.clampCaptureKeyboard()
486 for _, sc := range c.screens {
487 sc.arrange()
488 }
489}
490
491func (c *Core) publish(ctx context.Context) error {
492 if c.inputEpochChanged() {
493 return errSecurityChanged
494 }
495 c.syncSecurity()
496 if c.security.Protected {
497 return c.publishProtected(ctx)
498 }
499 for _, sc := range c.screens {
500 // The overview's selection is provisional: it focuses nothing yet.
501 if !sc.mon.ov.open {
502 sc.mon.Current().settleLeft()
503 }
504 }
505 c.captureExpire()
506 capture, err := c.captureEvaluate(ctx)
507 if err != nil {
508 return err
509 }
510 // The workspace a session renders off screen keeps its real layout for
511 // the configures, popups and CaptureScene of this publish.
512 c.captureTrack(capture)
513 if err := c.closeHiddenPopups(ctx); err != nil {
514 return err
515 }
516 // Clients learn outputs and scales before the configures sized for them.
517 if err := c.syncOutputs(ctx); err != nil {
518 return err
519 }
520 // Output layout must be known before an automatic terminal can map.
521 if err := c.spawnEmpty(ctx); err != nil {
522 return err
523 }
524 if err := c.publishPending(ctx); err != nil {
525 return err
526 }
527 focus := c.keyboardFocus()
528 window, _ := c.cur().mon.Focused()
529 c.pulseFocus(window, focus == window)
530 var pulse float64
531 if c.pulse.id != 0 {
532 pulse = c.advancePulse(c.now())
533 }
534 // drawable: the scene can show a pulse on the focused window.
535 drawable := false
536 scenes := make([]ports.Scene, 0, len(c.screens))
537 // Before the first output (and after the last is unplugged) the
538 // placeholder's scene has no output name: no renderer draws it.
539 for i, sc := range c.screens {
540 c.seq++
541 o := sc.mon.Output()
542 // frame is the viewport of the workspace on screen: the whole output
543 // unless it has a size override (never in the overview).
544 frame := sc.mon.Frame()
545 var clip Rect
546 if frame != (Rect{W: o.W, H: o.H}) {
547 clip = frame
548 }
549 scene := ports.Scene{Security: c.security, Output: sc.name(), Seq: c.seq, OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0), Layers: shownLayers(sc)}
550 layout := sc.mon.Layout()
551 var real map[WindowID]Placement
552 if sc.mon.ov.open {
553 real = make(map[WindowID]Placement)
554 for w := range sc.mon.all() {
555 for _, p := range w.Layout() {
556 real[p.ID] = p
557 }
558 }
559 }
560 scene.Dim = floatDim(layout, frame, c.cfg.Floating.Dim)
561 if d := c.drag; d != nil && d.target.screen == sc && d.target.kind != dropNone {
562 scene.DropHints = slices.Clone(d.target.hints)
563 }
564 // Only the focused output lights the focused window's lines.
565 scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), frame, i == c.focusScreen)
566 if sc.mon.ov.open {
567 // Frame the selection and separate numbered and named row groups.
568 _, scene.Separators = sc.mon.overviewRows()
569 scene.Separators = append(scene.Separators, overviewOutline(layout, max(c.cfg.Border.Width, 2))...)
570 }
571 for _, p := range layout {
572 // Only the focused output has an activated window.
573 focused := p.Focused && i == c.focusScreen
574 sw := ports.SceneWindow{ID: p.ID, Rect: p.Rect, Focused: focused, Fullscreen: p.Fullscreen, Hidden: p.Hidden, Floating: p.Floating, Below: p.Below, Inset: p.Inset, Preview: p.Preview}
575 if p.Peek {
576 sw.Dim = c.cfg.Stash.Dim
577 }
578 if focused && p.ID == c.pulse.target && !p.Fullscreen && !p.Hidden && p.Preview == 0 && !sc.mon.ov.open {
579 drawable = true
580 if p.ID == c.pulse.id {
581 sw.FocusEffect = pulse
582 }
583 }
584 scene.Windows = append(scene.Windows, sw)
585 t := configureTarget{output: sc.name(), area: frame, focused: focused, captured: capture != nil && capture.window == p.ID}
586 if !p.Hidden && p.Preview == 0 {
587 // Only a sized configure needs the client size.
588 t.client, t.imposed = c.clientRect(p), sc.mon.Current().imposedFloat(p.ID)
589 } else if t.captured && p.Hidden {
590 // A captured hidden window is sized like its capture.
591 t.client = capture.windowSz
592 } else if p.Preview > 0 && !p.Hidden {
593 if rp, ok := real[p.ID]; ok && !rp.Hidden {
594 t.realTiled = !rp.Floating
595 t.client = c.clientRect(rp)
596 }
597 }
598 cp, t := c.captureConfigure(sc, p, t)
599 if v, send := c.configures.nextWithCapture(p, t, cp); send {
600 if err := c.command(ctx, v); err != nil {
601 return err
602 }
603 c.configures.mark(v)
604 }
605 }
606 scene.Windows = append(scene.Windows, c.scenePopups(sc)...)
607 scene.CaptureIndicators = c.captureIndicators(sc)
608 if scene.Capture = c.captureSceneFor(sc, capture); scene.Capture != nil {
609 switch sc {
610 case capture.hiddenScr:
611 scene.CaptureScene = c.captureScene(scene.Seq)
612 case capture.windowScr:
613 scene.CaptureScene = c.captureWindowScene(capture, scene.Seq)
614 }
615 }
616 scenes = append(scenes, scene)
617 }
618 c.configures.prune()
619 // A pulse nothing shows (fullscreen, overview) asks for no frames.
620 if c.pulse.drawable = drawable; !drawable {
621 c.pulse.id = 0
622 }
623 // A workspace switch can hide the window under the pointer; it must not get
624 // clicks. The next motion re-runs hit-testing.
625 if c.pointer != 0 && !c.visible(c.pointer) {
626 c.pointer = 0
627 if c.grab == 0 {
628 if err := c.command(ctx, ports.PointerFocus{}); err != nil {
629 return err
630 }
631 }
632 }
633 if err := c.syncFocus(ctx, focus); err != nil {
634 return err
635 }
636 if err := c.updateInhibit(ctx); err != nil {
637 return err
638 }
639 c.resolveConstraint()
640 if c.inputEpochChanged() {
641 return errSecurityChanged
642 }
643 if c.syncSecurity() {
644 return c.publish(ctx)
645 }
646 latest(c.ch.Scenes, scenes)
647 c.publishState()
648 c.publishWorkspaces()
649 c.armCaptureTimer()
650 // A running slide moves on the next flip, or on the fallback timer.
651 if c.animating() {
652 if c.frameC == nil {
653 c.armFrame()
654 }
655 } else {
656 c.stopFrame()
657 }
658 return nil
659}
660
661// step moves the running slides to now and publishes the frame.
662func (c *Core) step(ctx context.Context) error {
663 c.stopFrame()
664 c.animate(c.now())
665 return c.slid(ctx, false)
666}
667
668// slid publishes a moved view; shown reports another workspace came on
669// screen.
670func (c *Core) slid(ctx context.Context, shown bool) error {
671 if err := c.workspaceVisible(ctx, shown); err != nil {
672 return err
673 }
674 if err := c.publish(ctx); err != nil {
675 return err
676 }
677 return c.rehit(ctx)
678}
679
680// rehit points the pointer at what now lies under a still cursor, as
681// windows slide under it. A held button keeps its grab.
682func (c *Core) rehit(ctx context.Context) error {
683 if c.grab != 0 || c.drag != nil {
684 return nil
685 }
686 id, x, y := c.hit(c.cursorX, c.cursorY)
687 if id != c.pointer {
688 c.pointer, c.pointerAt = id, [2]float64{x, y}
689 return c.command(ctx, ports.PointerFocus{ID: id, X: x, Y: y})
690 }
691 if id == 0 || c.pointerAt == [2]float64{x, y} {
692 return nil
693 }
694 c.pointerAt = [2]float64{x, y}
695 // The same window moved under the cursor: it moves in the window. No
696 // relative motion: the device did not move. The input clock is the
697 // device's; the last motion's time is the closest core knows.
698 return c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, TimeMsec: c.motionMsec})
699}
700
701// updateInhibit makes the shortcuts inhibitor of the keyboard focus the
702// active one and tells wayland when it changes.
703func (c *Core) updateInhibit(ctx context.Context) error {
704 if c.security.Protected {
705 return nil
706 }
707 f := c.keyboardFocus()
708 release, activate, changed := c.keyboard.inhibit(f, c.windows.lookup(f).inhibitShortcuts)
709 if !changed {
710 return nil
711 }
712 if release != 0 {
713 if err := c.command(ctx, ports.ShortcutsInhibitState{Window: release}); err != nil {
714 return err
715 }
716 }
717 if activate != 0 {
718 return c.command(ctx, ports.ShortcutsInhibitState{Window: activate, Active: true})
719 }
720 return nil
721}
722
723// resolveConstraint turns the active constraint into global coordinates
724// and sends it to input when it changed. A hidden window holds nothing.
725func (c *Core) resolveConstraint() {
726 var g ports.PointerConstraint
727 if full, vis, ok := c.shownClient(c.constrained.ID); ok {
728 r := full
729 if w := c.constrained.Rect; w.W > 0 && w.H > 0 {
730 // The region is window-local: placed from the full client origin,
731 // clipped to the window, then to what the viewport shows.
732 x0, y0 := max(full.X, full.X+w.X), max(full.Y, full.Y+w.Y)
733 x1, y1 := min(full.X+full.W, full.X+w.X+w.W), min(full.Y+full.H, full.Y+w.Y+w.H)
734 if x1 > x0 && y1 > y0 {
735 r = Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0}
736 }
737 }
738 r = intersect(r, vis)
739 if r.W > 0 && r.H > 0 {
740 g = ports.PointerConstraint{Mode: c.constrained.Mode, Rect: r}
741 } else {
742 // The region lies outside the viewport: hold the visible client.
743 g = ports.PointerConstraint{Mode: c.constrained.Mode, Rect: vis}
744 }
745 }
746 // The cursor follows core; input resyncs to it only on a change.
747 if g.Mode == c.constraint.Mode && g.Rect == c.constraint.Rect {
748 return
749 }
750 // A layout change must not leave a locked cursor off its window.
751 c.cursorX, c.cursorY = g.Clamp(c.cursorX, c.cursorY)
752 g.X, g.Y = c.cursorX, c.cursorY
753 c.constraint = g
754 if c.ch.Constraints != nil {
755 latest(c.ch.Constraints, g)
756 }
757}
758
759// intersect is the overlap of a and b, empty (zero size) when disjoint.
760func intersect(a, b Rect) Rect {
761 x0, y0 := max(a.X, b.X), max(a.Y, b.Y)
762 x1, y1 := min(a.X+a.W, b.X+b.W), min(a.Y+a.H, b.Y+b.H)
763 if x1 <= x0 || y1 <= y0 {
764 return Rect{}
765 }
766 return Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0}
767}
768
769// shownClient is the global logical client rectangle of a window drawn on
770// its output, whole (window-local coordinates start at its origin), and the
771// part of it inside the workspace viewport, which alone takes the pointer.
772// ok is false when it is hidden, a preview or entirely outside the viewport.
773func (c *Core) shownClient(id WindowID) (full, visible Rect, ok bool) {
774 if id == 0 {
775 return Rect{}, Rect{}, false
776 }
777 s, _ := c.screenOf(id)
778 if s == nil {
779 return Rect{}, Rect{}, false
780 }
781 for _, p := range s.mon.Layout() {
782 if p.ID != id || p.Hidden || p.Preview > 0 {
783 continue
784 }
785 r := c.clientRect(p)
786 full = Rect{X: r.X + s.x, Y: r.Y + s.y, W: r.W, H: r.H}
787 f := s.mon.Frame()
788 visible = intersect(r, f)
789 visible.X, visible.Y = visible.X+s.x, visible.Y+s.y
790 if visible.W <= 0 || visible.H <= 0 {
791 return Rect{}, Rect{}, false
792 }
793 return full, visible, true
794 }
795 return Rect{}, Rect{}, false
796}
797
798// shownClientRect is the visible part of shownClient.
799func (c *Core) shownClientRect(id WindowID) (Rect, bool) {
800 _, vis, ok := c.shownClient(id)
801 return vis, ok
802}
803
804// warpPointer moves the cursor to a window-local point of the window under
805// it (wp_pointer_warp_v1). Input takes the new position, and the window
806// gets the motion; the active constraint still bounds the point.
807func (c *Core) warpPointer(ctx context.Context, v ports.PointerWarp) error {
808 if v.ID == 0 || v.ID != c.pointer || c.grab != 0 && c.grab != v.ID {
809 return nil
810 }
811 full, vis, ok := c.shownClient(v.ID)
812 if !ok || v.X < 0 || v.Y < 0 || v.X >= float64(full.W) || v.Y >= float64(full.H) {
813 return nil
814 }
815 // Local to the whole client, and refused outside the viewport.
816 gx, gy := float64(full.X)+v.X, float64(full.Y)+v.Y
817 if gx < float64(vis.X) || gy < float64(vis.Y) || gx >= float64(vis.X+vis.W) || gy >= float64(vis.Y+vis.H) {
818 return nil
819 }
820 c.cursorX, c.cursorY = c.constraint.Clamp(gx, gy)
821 if c.ch.Constraints != nil {
822 g := c.constraint
823 g.X, g.Y, g.Warp = c.cursorX, c.cursorY, true
824 latest(c.ch.Constraints, g)
825 }
826 // A locked pointer moved: the client knows where it put it.
827 if c.constraint.Mode == ports.ConstraintLock {
828 return nil
829 }
830 return c.rehit(ctx)
831}
832
833// syncOutputs sends the output layout when it changed; input learns the
834// geometry only when the outputs themselves moved.
835func (c *Core) syncOutputs(ctx context.Context) error {
836 v := ports.SetOutputs{Outputs: c.layout(), Focused: c.cur().name(), Off: c.offOutputs()}
837 if sameOutputs(v, c.sentOutputs) {
838 return nil
839 }
840 if err := c.command(ctx, v); err != nil {
841 return err
842 }
843 if c.ch.Layouts != nil && !slices.Equal(v.Outputs, c.sentOutputs.Outputs) {
844 latest(c.ch.Layouts, v.Outputs)
845 }
846 c.sentOutputs = v
847 return nil
848}
849
850// syncFocus tells wayland the keyboard focus when it changed.
851func (c *Core) syncFocus(ctx context.Context, focus WindowID) error {
852 if focus == c.keyboard.sent {
853 return nil
854 }
855 if err := c.command(ctx, ports.FocusWindow{ID: focus}); err != nil {
856 return err
857 }
858 c.keyboard.sent = focus
859 return nil
860}
861
862// latest replaces any unread value: only the owner sends and drains;
863// consumers only receive.
864func latest[T any](ch chan T, v T) {
865 select {
866 case ch <- v:
867 default:
868 select {
869 case <-ch:
870 default:
871 }
872 select {
873 case ch <- v:
874 default:
875 }
876 }
877}
878
879func sameOutputs(a, b ports.SetOutputs) bool {
880 return a.Focused == b.Focused && slices.Equal(a.Outputs, b.Outputs) && slices.Equal(a.Off, b.Off)
881}
882
883// allLayers lists the layer surfaces of every output.
884func (c *Core) allLayers() []ports.LayerSurface {
885 var all []ports.LayerSurface
886 for _, sc := range c.screens {
887 all = append(all, sc.layers...)
888 }
889 return all
890}
891
892func (c *Core) acceptsInput(id WindowID, x, y float64) bool {
893 return c.windows.acceptsInput(id, x, y)
894}
895
896// hit returns the surface under the global logical point and the point in
897// its surface coordinates: popups, then overlay and top layers, then
898// windows (last visible placement is topmost), then bottom and
899// background layers.
900func (c *Core) hit(x, y float64) (WindowID, float64, float64) {
901 if c.security.Protected {
902 return c.lockHit(x, y)
903 }
904 o, ok := c.layout().At(x, y)
905 if !ok {
906 return 0, 0, 0
907 }
908 lx, ly := x-float64(o.X), y-float64(o.Y)
909 sc := c.screens[c.screenIndex(o.Info.Name)]
910 // Layer popups are over everything; window popups are over the windows
911 // only, under the top and overlay layers (as drawn).
912 if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 {
913 return id, px, py
914 }
915 if id, px, py := c.layerAt(sc, lx, ly, true); id != 0 {
916 return id, px, py
917 }
918 if id, px, py := c.popupAt(sc, lx, ly, false); id != 0 {
919 return id, px, py
920 }
921 if sc.mon.ov.open {
922 // Previews and the layers under them take no input: a click
923 // picks a preview (overviewClick).
924 return 0, 0, 0
925 }
926 if !sc.mon.frameHas(lx, ly) {
927 return c.layerAt(sc, lx, ly, false)
928 }
929 var id WindowID
930 var sx, sy float64
931 for _, p := range sc.mon.Layout() {
932 r := c.clientRect(p)
933 // A peek is clickable wherever it shows, border included: it may
934 // be narrower than its border. The point is clamped to its client.
935 box, lx, ly := r, lx, ly
936 if p.Peek {
937 box = p.Rect
938 }
939 if !p.Hidden && r.W > 0 && r.H > 0 && lx >= float64(box.X) && lx < float64(box.X+box.W) && ly >= float64(box.Y) && ly < float64(box.Y+box.H) {
940 if p.Peek {
941 lx, ly = min(max(lx, float64(r.X)), float64(r.X+r.W-1)), min(max(ly, float64(r.Y)), float64(r.Y+r.H-1))
942 }
943 if c.acceptsInput(p.ID, lx-float64(r.X), ly-float64(r.Y)) {
944 // Layout is bottom to top, including fullscreen and floats.
945 id, sx, sy = p.ID, lx-float64(r.X), ly-float64(r.Y)
946 }
947 }
948 }
949 if id == 0 {
950 return c.layerAt(sc, lx, ly, false)
951 }
952 return id, sx, sy
953}
954
955func (c *Core) Run(ctx context.Context) error {
956 c.syncSecurity()
957 defer c.stopFrame()
958 defer c.stopCaptureTimer()
959 defer c.stopPulseTimer()
960 // Startup commands run once per session. Full launcher queues retain a
961 // bounded remainder, selectable alongside owner events without polling.
962 c.startup = make([][]string, len(c.cfg.Startup))
963 for i, argv := range c.cfg.Startup {
964 c.startup[i] = slices.Clone(argv)
965 }
966 c.spawnStartup(ctx)
967 if c.spawnSlots(ctx, false) != nil || c.publishPending(ctx) != nil {
968 return nil
969 }
970 c.publishState()
971 c.publishWorkspaces()
972 for {
973 if c.securityCheckpoint(ctx) != nil {
974 return nil
975 }
976 startupC, startupRequest := c.pendingStartup()
977 select {
978 case <-ctx.Done():
979 return nil
980 case startupC <- startupRequest:
981 // Successful transfer consumes startup once. Only the receiver can
982 // decide execution admission; a later gate read cannot infer it.
983 c.startup = c.startup[1:]
984 continue
985 case ev, ok := <-c.ch.Client:
986 if !ok {
987 c.ch.Client = nil
988 continue
989 }
990 if c.securityCheckpoint(ctx) != nil {
991 return nil
992 }
993 if c.blockProtected(ev) {
994 continue
995 }
996 switch v := ev.(type) {
997 case ports.SessionLockChanged:
998 if !c.applyLockChanged(v) {
999 continue
1000 }
1001 case ports.CaptureSessionOpen:
1002 c.captureOpen(v)
1003 case ports.CaptureSessionClose:
1004 c.captureClose(v.ID)
1005 case ports.CaptureFrameTaken:
1006 // Nothing new to show (the target flashes already): no scene.
1007 if !c.captureFrame(v) {
1008 continue
1009 }
1010 case ports.CaptureExclusionBegin:
1011 c.captureExclusionBegin(v)
1012 case ports.CaptureExclusionLayer:
1013 c.captureExclusionLayer(v)
1014 case ports.CaptureExclusionEnd:
1015 c.captureExclusionEnd(v.Session)
1016 case ports.LayerChanged:
1017 c.layerChanged = true
1018 c.setLayers(v.Layers)
1019 case ports.InputRegionChanged:
1020 c.windows.setRegion(v)
1021 case ports.WindowMapped:
1022 c.windows.mapped(v, c.now())
1023 c.placement.place(c, v)
1024 case ports.WindowResized:
1025 if _, w := c.screenOf(v.ID); w != nil {
1026 w.ResizeFloating(v.ID, v.Width, v.Height)
1027 }
1028 case ports.PopupRequest:
1029 if err := c.placePopup(ctx, v); err != nil {
1030 return nil
1031 }
1032 case ports.PopupMapped:
1033 if p := c.popups[v.ID]; p != nil {
1034 p.mapped = true
1035 }
1036 case ports.ShortcutsInhibit:
1037 c.windows.setInhibitShortcuts(v.Window, v.Active)
1038 if err := c.updateInhibit(ctx); err != nil {
1039 return nil
1040 }
1041 continue
1042 case ports.OutputPower:
1043 if i := c.screenIndex(v.Output); i >= 0 && c.screens[i].off == v.On {
1044 c.screens[i].off = !v.On
1045 }
1046 case ports.IdleInhibit:
1047 c.windows.setIdleInhibit(v.Window, v.Active)
1048 c.publishState()
1049 continue
1050 case ports.WindowAppID:
1051 c.windows.setAppID(v.ID, v.AppID)
1052 case ports.WindowParent:
1053 if _, w := c.screenOf(v.ID); w != nil {
1054 w.SetDialogParent(v.ID, v.Parent)
1055 }
1056 case ports.WindowUnmapped:
1057 if c.popups[v.ID] != nil {
1058 if err := c.dropPopup(ctx, v.ID); err != nil {
1059 return nil
1060 }
1061 }
1062 if err := c.closePopupsOf(ctx, v.ID); err != nil {
1063 return nil
1064 }
1065 c.windows.drop(v.ID)
1066 if c.drag != nil && c.drag.id == v.ID {
1067 c.abortDrag()
1068 }
1069 if s, _ := c.screenOf(v.ID); s != nil {
1070 s.mon.RemoveWindow(v.ID)
1071 }
1072 c.releaseSlots()
1073 if c.pointer == v.ID {
1074 c.pointer = 0
1075 if err := c.command(ctx, ports.PointerFocus{}); err != nil {
1076 return nil
1077 }
1078 }
1079 case ports.PointerConstrained:
1080 c.constrained = v
1081 case ports.PointerWarp:
1082 if c.warpPointer(ctx, v) != nil {
1083 return nil
1084 }
1085 case ports.WindowMoveRequest:
1086 if c.clientDrag(ctx, v) != nil {
1087 return nil
1088 }
1089 case ports.WindowFullscreenRequest:
1090 c.configures.answer[v.ID] = true
1091 if v.Fullscreen && !v.External && c.now().Sub(c.windows.lookup(v.ID).mappedAt) < fullscreenGrace {
1092 break
1093 }
1094 // A taskbar request is a user action on that window, as an
1095 // activation: it comes on screen with the focus, leaving
1096 // another window's fullscreen.
1097 if v.External && v.Fullscreen && c.activate(ctx, v.ID) != nil {
1098 return nil
1099 }
1100 if s, _ := c.screenOf(v.ID); s != nil {
1101 s.mon.SetFullscreen(v.ID, v.Fullscreen)
1102 }
1103 case ports.WorkspaceActivate:
1104 before := c.cur().mon.Current()
1105 for _, id := range v.IDs {
1106 for i, sc := range c.screens {
1107 if sc.name() == "" {
1108 continue
1109 }
1110 for w := range sc.mon.all() {
1111 if w.ID == id {
1112 c.focusScreen = i
1113 sc.mon.show(w)
1114 break
1115 }
1116 }
1117 }
1118 }
1119 if c.workspaceVisible(ctx, c.cur().mon.Current() != before) != nil {
1120 return nil
1121 }
1122 case ports.WindowActivate:
1123 if c.activate(ctx, v.ID) != nil {
1124 return nil
1125 }
1126 }
1127 if err := c.publish(ctx); err != nil {
1128 return nil
1129 }
1130 case f, ok := <-c.ch.Frames:
1131 if c.securityCheckpoint(ctx) != nil {
1132 return nil
1133 }
1134 if !ok {
1135 c.ch.Frames = nil
1136 continue
1137 }
1138 if c.sliding(f.Output) && c.step(ctx) != nil {
1139 return nil
1140 }
1141 continue
1142 case <-c.frameC:
1143 if c.securityCheckpoint(ctx) != nil {
1144 return nil
1145 }
1146 c.frameC, c.frameStop = nil, nil
1147 if c.step(ctx) != nil {
1148 return nil
1149 }
1150 continue
1151 case <-c.pulse.timerC:
1152 if c.securityCheckpoint(ctx) != nil {
1153 return nil
1154 }
1155 if c.pulseTick() && c.publish(ctx) != nil {
1156 return nil
1157 }
1158 continue
1159 case <-c.capt.timerC:
1160 c.capt.timerC, c.capt.timerStop = nil, nil
1161 if c.captureFlashTick() && c.publish(ctx) != nil {
1162 return nil
1163 }
1164 continue
1165 case ev, ok := <-c.ch.Output:
1166 if !ok {
1167 c.ch.Output = nil
1168 continue
1169 }
1170 if c.securityCheckpoint(ctx) != nil {
1171 return nil
1172 }
1173 switch v := ev.(type) {
1174 case ports.OutputAdded:
1175 c.addScreen(v.Info)
1176 if c.layerChanged {
1177 c.setLayers(c.allLayers())
1178 }
1179 case ports.OutputRemoved:
1180 // A drop target may be on the output going away.
1181 c.abortDrag()
1182 c.removeScreen(v.Name)
1183 if c.layerChanged {
1184 c.setLayers(c.allLayers())
1185 }
1186 c.cursorX, c.cursorY = c.layout().Clamp(c.cursorX, c.cursorY, c.cursorX, c.cursorY)
1187 }
1188 if c.workspaceVisible(ctx, false) != nil {
1189 return nil
1190 }
1191 if err := c.publish(ctx); err != nil {
1192 return nil
1193 }
1194 case ev, ok := <-c.ch.Config:
1195 if !ok {
1196 c.ch.Config = nil
1197 continue
1198 }
1199 if c.securityCheckpoint(ctx) != nil {
1200 return nil
1201 }
1202 if err := c.apply(ev.Config); err != nil {
1203 select {
1204 case c.ch.ConfigErrors <- err:
1205 default:
1206 }
1207 continue
1208 }
1209 if c.workspaceVisible(ctx, false) != nil {
1210 return nil
1211 }
1212 if err := c.publish(ctx); err != nil {
1213 return nil
1214 }
1215 case ev, ok := <-c.ch.Input:
1216 if !ok {
1217 c.ch.Input = nil
1218 continue
1219 }
1220 if c.securityCheckpoint(ctx) != nil {
1221 return nil
1222 }
1223 // Even stale input is activity, but never becomes client input.
1224 if err := c.userActivity(ctx); err != nil {
1225 return nil
1226 }
1227 // Commands can backpressure; recheck after activity before admits/binds.
1228 if c.securityCheckpoint(ctx) != nil {
1229 return nil
1230 }
1231 var admitted bool
1232 ev, admitted = c.admitInput(ev)
1233 if !admitted {
1234 continue
1235 }
1236 if err := c.handleInput(ctx, ev); err != nil {
1237 if errors.Is(err, errSecurityChanged) {
1238 continue
1239 }
1240 return err
1241 }
1242 }
1243 }
1244}
1245
1246// workspaceVisible settles guests and refills slots on a show. Publication
1247// subsequently decides whether visible empty workspaces need terminals.
1248func (c *Core) workspaceVisible(ctx context.Context, shown bool) error {
1249 c.releaseSlots()
1250 c.settleGuests()
1251 return c.spawnSlots(ctx, shown)
1252}
1253
1254// spawnEmpty requests terminals for visible empty workspaces. publish calls
1255// it after the output layout and before scenes, so a terminal never maps
1256// before wayland knows its output.
1257func (c *Core) spawnEmpty(ctx context.Context) error {
1258 if c.protectionRequested() {
1259 return nil
1260 }
1261 first := c.firstTerminalResolved
1262 terms := c.fillEmpty()
1263 err := c.publishPending(ctx)
1264 for i, req := range terms {
1265 if err != nil || !c.trySpawn(ctx, req) {
1266 for _, suppressed := range terms[i:] {
1267 c.cancelTerminal(suppressed)
1268 }
1269 if i == 0 {
1270 c.firstTerminalResolved = first
1271 }
1272 return err
1273 }
1274 }
1275 return err
1276}
1277
1278// activate brings a window forward for a valid xdg-activation token. The
1279// token proves a user action in the requesting client (a click on a link,
1280// a notification), so unlike other client requests it may move the user
1281// (the exception to the ADR 011 golden rule).
1282func (c *Core) activate(ctx context.Context, id WindowID) error {
1283 s, w := c.screenOf(id)
1284 if s == nil || c.popups[id] != nil {
1285 return nil
1286 }
1287 before := c.cur().mon.Current()
1288 if w != s.mon.Current() {
1289 s.mon.show(w)
1290 }
1291 w.Activate(id)
1292 c.focusScreen = c.screenIndex(s.name())
1293 return c.workspaceVisible(ctx, c.cur().mon.Current() != before)
1294}
1295
1296// heldKey names a key for press tracking: by physical key, since Shift
1297// may be released before the key.
1298func heldKey(key ports.KeyEvent) string {
1299 if key.Keycode != 0 {
1300 return "#" + strconv.FormatUint(uint64(key.Keycode), 10)
1301 }
1302 return keyName(key.Keysym)
1303}