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