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