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