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