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