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