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