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