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 c.cfg = cfg
357 c.binds = binds
358 c.cmdMod = map[string]ports.Mods{"super": ports.ModSuper, "alt": ports.ModAlt, "ctrl": ports.ModCtrl}[cfg.Keyboard.CmdKey]
359 c.specs, c.rules = named, rules
360 c.toSpawn = append(c.toSpawn, c.updateSlots(specs)...)
361 c.named()
362 for _, s := range c.screens {
363 c.settings(s.mon)
364 }
365 c.applyOutputConfig()
366 for _, s := range c.screens {
367 old, now := axes[s.mon], s.mon.Current().policy()
368 if old.content != now.content || old.workspace != now.workspace {
369 s.mon.ov.dropScroll()
370 }
371 }
372 return nil
373}
374
375// spawnSlots starts the slots queued by config. With shown set (the user
376// just switched to another workspace), it also refills the empty slots of
377// the workspace now on screen.
378func (c *Core) spawnSlots(ctx context.Context, shown bool) error {
379 if c.protectionRequested() {
380 return nil
381 }
382 keys := c.toSpawn
383 c.toSpawn = nil
384 if shown {
385 keys = append(keys, c.refill()...)
386 }
387 started := map[slotKey]bool{}
388 for i, key := range keys {
389 if c.protectionRequested() {
390 c.toSpawn = append(c.toSpawn, keys[i:]...)
391 return nil
392 }
393 st, ok := c.slots[key]
394 // A key can be queued twice (new slot on the workspace on screen).
395 if !ok || started[key] || st.window != 0 {
396 continue
397 }
398 started[key] = true
399 req := c.spawnSlot(key)
400 if err := ctx.Err(); err != nil || c.protectionRequested() {
401 c.placement.dropSlot(key)
402 st.stale = false
403 c.toSpawn = append(c.toSpawn, keys[i:]...)
404 return err
405 }
406 if !c.trySpawn(ctx, req) {
407 c.placement.dropSlot(key)
408 st.stale = false
409 c.toSpawn = append(c.toSpawn, keys[i:]...)
410 return ctx.Err()
411 }
412 }
413 return nil
414}
415func New(cfg ports.Config, ch Channels, opts Options) (*Core, error) {
416 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) {
417 return nil, fmt.Errorf("scenes, layouts, constraints, state and workspaces must have capacity 1")
418 }
419 // A placeholder screen holds windows until the first output arrives.
420 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()}
421 c.screens = []*screen{{mon: newMonitorWithIDs("", "", &c.nextWorkspaceID), scale: 1, cfgScale: 1}}
422 if err := c.apply(cfg); err != nil {
423 return nil, err
424 }
425 return c, nil
426}
427
428// newTimer makes a timer on clock, or the system timer when clock is nil.
429func newTimer(clock ports.Clock, d time.Duration) (<-chan time.Time, func() bool) {
430 t := newPortTimer(clock, d)
431 return t.C(), t.Stop
432}
433
434// newPortTimer makes a ports.Timer on clock, or on the system clock when
435// clock is nil.
436func newPortTimer(clock ports.Clock, d time.Duration) ports.Timer {
437 if clock != nil {
438 return clock.NewTimer(d)
439 }
440 return sysTimer{time.NewTimer(d)}
441}
442
443// sysTimer is a time.Timer as a ports.Timer.
444type sysTimer struct{ t *time.Timer }
445
446func (t sysTimer) C() <-chan time.Time { return t.t.C }
447func (t sysTimer) Stop() bool { return t.t.Stop() }
448func (t sysTimer) Reset(d time.Duration) bool { return t.t.Reset(d) }
449
450func (c *Core) now() time.Time {
451 if c.opts.Clock != nil {
452 return c.opts.Clock.Now()
453 }
454 return time.Now()
455}
456
457// command queues v for the wayland goroutine and never blocks: a busy
458// display must not stall keyboard, scenes or frame wakes. The sender
459// goroutine of Run delivers the queue in order.
460func (c *Core) command(ctx context.Context, v ports.ClientCommand) error {
461 _, err := c.queue(ctx, v)
462 return err
463}
464
465// queue pushes v to the outbox and returns its sequence number (0: not
466// queued).
467func (c *Core) queue(ctx context.Context, v ports.ClientCommand) (uint64, error) {
468 if err := ctx.Err(); err != nil {
469 return 0, err
470 }
471 if c.inputEpochChanged() {
472 return 0, errSecurityChanged
473 }
474 if c.ch.Commands == nil {
475 // Nobody receives: queueing would only grow without bound.
476 return 0, nil
477 }
478 seq := c.out.push(c.ch.Commands, v, c.security, c.opts.Security != nil)
479 if c.out.backlog() > outboxLimit {
480 if err := c.out.flush(ctx, c.ch.Commands, seq-outboxLimit); err != nil {
481 return seq, err
482 }
483 }
484 // The queued command keeps the epoch it was made under, so a display
485 // that has moved on drops it. Report the change all the same: the rest
486 // of the event was decided under the old epoch and must not go on.
487 if c.inputEpochChanged() {
488 return seq, errSecurityChanged
489 }
490 return seq, nil
491}
492
493// clientRect is the placement minus the border on its inset sides.
494// The renderer applies the same inset (ports.Border).
495func (c *Core) clientRect(p Placement) Rect {
496 if p.Fullscreen {
497 return p.Rect
498 }
499 return p.Rect.Inset(p.Inset, c.cfg.Border.Width)
500}
501
502// viewport is where a workspace is seen: frame is the whole viewport,
503// usable the part exclusive layers (panels) leave to the tiles.
504type viewport struct{ frame, usable Rect }
505
506// whole is a viewport with no panel: all of r is usable.
507func whole(r Rect) viewport { return viewport{frame: r, usable: r} }
508
509// area is where p can be seen, drawn and pointed at. A tile lives in the
510// usable area and is cut at its edge (Scene.TileInset), so a cascade band
511// or a column scrolled under a panel is not seen; a float, a fullscreen
512// window and an overview preview are placed against the frame.
513func (v viewport) area(p Placement) Rect {
514 if p.Floating || p.Fullscreen || p.Preview > 0 {
515 return v.frame
516 }
517 return v.usable
518}
519
520// shows reports whether the user sees p: not hidden, and overlapping its
521// area.
522func (v viewport) shows(p Placement) bool {
523 return !p.Hidden && p.Rect.Overlaps(v.area(p))
524}
525
526// floatDim is the veil opacity of a layout: dim only when a float is
527// drawn above the tiles, never for a demoted covering float alone nor for
528// an overview preview. A leaving float (fading out after it closed or hid)
529// drops the veil at once: a veil that fades changes Scene.Dim every frame,
530// which redraws the whole output for the length of the fade, where the
531// float's own fade redraws only its rect.
532func floatDim(layout []Placement, v viewport, dim float64) float64 {
533 for _, p := range layout {
534 if p.Floating && !p.Below && p.Preview == 0 && v.shows(p) {
535 if p.Fullscreen {
536 return 0
537 }
538 return dim
539 }
540 }
541 return 0
542}
543
544// visible reports whether the window, layer surface or popup is on screen
545// on any output.
546func (c *Core) visible(id WindowID) bool {
547 _, _, ok := c.windowRect(id)
548 return ok
549}
550
551// windowFocus is the focused window of each output.
552func (c *Core) windowFocus() map[*screen]WindowID {
553 m := make(map[*screen]WindowID, len(c.screens))
554 for _, sc := range c.screens {
555 m[sc], _ = sc.mon.Focused()
556 }
557 return m
558}
559
560// keyboardFocus is the surface holding the keyboard (see keyboard.focus).
561func (c *Core) keyboardFocus() WindowID {
562 if c.security.Protected {
563 return c.lockKeyboardFocus()
564 }
565 var exclusive WindowID
566 for _, sc := range c.screens {
567 for _, l := range sc.layers {
568 if l.Keyboard == 1 && (l.Layer == ports.LayerTop || l.Layer == ports.LayerOverlay) && l.ID > exclusive {
569 exclusive = l.ID
570 }
571 }
572 }
573 window, _ := c.cur().mon.Focused()
574 return c.keyboard.focus(keyboardCandidates{
575 exclusive: exclusive,
576 grab: c.grabFocus(),
577 window: window,
578 layerShown: func(id WindowID) bool {
579 return c.onDemand(id) && c.visible(id)
580 },
581 windows: c.windowFocus,
582 })
583}
584
585// setLayers splits the mapped layer surfaces by output. A surface without an
586// output, or on an unknown one, goes to the focused output.
587func (c *Core) setLayers(all []ports.LayerSurface) {
588 previous := make(map[WindowID]bool)
589 for _, sc := range c.screens {
590 for _, l := range sc.layers {
591 previous[l.ID] = true
592 }
593 sc.layers = nil
594 }
595 for _, l := range all {
596 delete(previous, l.ID)
597 sc := c.cur()
598 if i := c.screenIndex(l.Output); i >= 0 {
599 sc = c.screens[i]
600 }
601 sc.layers = append(sc.layers, l)
602 }
603 for id := range previous {
604 c.windows.layerGone(id)
605 }
606 c.pruneRetained()
607 c.clampCaptureKeyboard()
608 for _, sc := range c.screens {
609 sc.arrange()
610 }
611}
612
613func (c *Core) publish(ctx context.Context) error { return c.publishFrame(ctx, nil) }
614
615// publishFrame publishes the scenes. only is the screen whose page flip
616// moved the springs (nil: any, or none moved): only a flip's own screen gets
617// a fresh Seq for its animation, so another output keeps its Seq while it draws the same.
618func (c *Core) publishFrame(ctx context.Context, only *screen) error {
619 if c.inputEpochChanged() {
620 return errSecurityChanged
621 }
622 c.syncSecurity()
623 c.updatePositionHints(only)
624 c.hintValidate()
625 if c.security.Protected {
626 return c.publishProtected(ctx)
627 }
628 if c.switching() && c.switcherStale() {
629 // Another workspace came on screen under the switcher: it is over.
630 c.cancelSwitcher()
631 }
632 for _, sc := range c.screens {
633 // The overview's selection is provisional: it focuses nothing yet.
634 if !sc.mon.ov.open {
635 sc.mon.Current().settleLeft()
636 }
637 // A switcher's selection is provisional too.
638 if !sc.mon.previewing() {
639 sc.mon.Current().noteFocus()
640 }
641 }
642 if c.pgrab != nil && !c.pgrab.valid() {
643 if err := c.abortPointerGrab(ctx); err != nil {
644 return err
645 }
646 }
647 c.refreshShown()
648 c.captureExpire()
649 capture, err := c.captureEvaluate(ctx)
650 if err != nil {
651 return err
652 }
653 // The workspace a session renders off screen keeps its real layout for
654 // the configures, popups and CaptureScene of this publish.
655 c.captureTrack(capture)
656 if err := c.closeHiddenPopups(ctx); err != nil {
657 return err
658 }
659 // Clients learn outputs and scales before the configures sized for them.
660 if err := c.syncOutputs(ctx); err != nil {
661 return err
662 }
663 // Output layout must be known before an automatic terminal can map.
664 if err := c.spawnEmpty(ctx); err != nil {
665 return err
666 }
667 focus := c.keyboardFocus()
668 c.advanceFlash(only)
669 scenes := make([]ports.Scene, 0, len(c.screens))
670 // Before the first output (and after the last is unplugged) the
671 // placeholder's scene has no output name: no renderer draws it.
672 for i, sc := range c.screens {
673 scene, err := c.sceneFor(ctx, i, sc, only, capture)
674 if err != nil {
675 return err
676 }
677 scenes = append(scenes, scene)
678 }
679 c.configures.prune()
680 for i, sc := range c.screens {
681 sc.last = scenes[i]
682 }
683 // A workspace switch can hide the window under the pointer; it must not get
684 // clicks. The next motion re-runs hit-testing.
685 if c.pointer != 0 && !c.visible(c.pointer) {
686 c.pointer = 0
687 if c.grab == 0 {
688 if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil {
689 return err
690 }
691 }
692 }
693 if err := c.syncFocus(ctx, focus); err != nil {
694 return err
695 }
696 if err := c.updateInhibit(ctx); err != nil {
697 return err
698 }
699 c.resolveConstraint()
700 if c.inputEpochChanged() {
701 return errSecurityChanged
702 }
703 if c.syncSecurity() {
704 return c.publishFrame(ctx, only)
705 }
706 latest(c.ch.Scenes, scenes)
707 c.publishState()
708 c.publishWorkspaces()
709 c.armCaptureTimer()
710 // A running slide moves on the next flip, or on the fallback timer.
711 if c.animating() {
712 if c.frameC == nil {
713 c.armFrame()
714 }
715 } else {
716 c.stopFrame()
717 }
718 return nil
719}
720
721// step moves the running slides of only (nil: every screen) to now and
722// publishes the frame.
723func (c *Core) step(ctx context.Context, only *screen) error {
724 // The fallback timer guards every animating output: a flip of one keeps
725 // it running while another still animates, or that one could starve.
726 if only == nil || !c.animatingOther(only) {
727 c.stopFrame()
728 }
729 c.animate(c.now(), only)
730 return c.slidFrame(ctx, false, only)
731}
732
733// animatingOther reports whether a spring runs on a screen
734// other than only.
735func (c *Core) animatingOther(only *screen) bool {
736 for _, sc := range c.screens {
737 if sc != only && (sc.springing() || c.flashing(sc) || c.positionHintFading(sc)) {
738 return true
739 }
740 }
741 return false
742}
743
744// slid publishes a moved view; shown reports another workspace came on
745// screen.
746func (c *Core) slid(ctx context.Context, shown bool) error {
747 return c.slidFrame(ctx, shown, nil)
748}
749
750// slidFrame is slid for the page flip of only (nil: any screen).
751func (c *Core) slidFrame(ctx context.Context, shown bool, only *screen) error {
752 if err := c.workspaceVisible(ctx, shown); err != nil {
753 return err
754 }
755 if err := c.publishFrame(ctx, only); err != nil {
756 return err
757 }
758 return c.rehit(ctx)
759}
760
761// rehit points the pointer at what now lies under a still cursor, as
762// windows slide under it. A held button keeps its grab.
763func (c *Core) rehit(ctx context.Context) error {
764 if c.dataDrag != nil {
765 // The drag target follows the layout too.
766 return c.dataDragRetarget(ctx, c.motionTime)
767 }
768 if c.grab != 0 || c.pointerBusy() {
769 return nil
770 }
771 id, x, y := c.hit(c.cursorX, c.cursorY)
772 if id != c.pointer {
773 c.pointer, c.pointerAt = id, [2]float64{x, y}
774 return c.pointerFocus(ctx, ports.PointerFocus{ID: id, X: x, Y: y})
775 }
776 if id == 0 || c.pointerAt == [2]float64{x, y} {
777 return nil
778 }
779 c.pointerAt = [2]float64{x, y}
780 // The same window moved under the cursor: it moves in the window. No
781 // relative motion: the device did not move. The input clock is the
782 // device's; the last motion's time is the closest core knows.
783 return c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, Time: c.motionTime})
784}
785
786// updateInhibit makes the shortcuts inhibitor of the keyboard focus the
787// active one and tells wayland when it changes.
788func (c *Core) updateInhibit(ctx context.Context) error {
789 if c.security.Protected {
790 return nil
791 }
792 f := c.keyboardFocus()
793 release, activate, changed := c.keyboard.inhibit(f, c.windows.lookup(f).inhibitShortcuts)
794 if !changed {
795 return nil
796 }
797 if release != 0 {
798 if err := c.command(ctx, ports.ShortcutsInhibitState{Window: release}); err != nil {
799 return err
800 }
801 }
802 if activate != 0 {
803 return c.command(ctx, ports.ShortcutsInhibitState{Window: activate, Active: true})
804 }
805 return nil
806}
807
808// resolveConstraint turns the active constraint into global coordinates
809// and sends it to input when it changed. A hidden window holds nothing.
810func (c *Core) resolveConstraint() {
811 var g ports.PointerConstraint
812 if full, vis, ok := c.shownClient(c.constrained.ID); ok {
813 r := full
814 if w := c.constrained.Rect; w.W > 0 && w.H > 0 {
815 // The region is window-local: placed from the full client origin,
816 // clipped to the window, then to what the viewport shows.
817 x0, y0 := max(full.X, full.X+w.X), max(full.Y, full.Y+w.Y)
818 x1, y1 := min(full.X+full.W, full.X+w.X+w.W), min(full.Y+full.H, full.Y+w.Y+w.H)
819 if x1 > x0 && y1 > y0 {
820 r = Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0}
821 }
822 }
823 r = intersect(r, vis)
824 if r.W > 0 && r.H > 0 {
825 g = ports.PointerConstraint{Mode: c.constrained.Mode, Rect: r}
826 } else {
827 // The region lies outside the viewport: hold the visible client.
828 g = ports.PointerConstraint{Mode: c.constrained.Mode, Rect: vis}
829 }
830 }
831 // The cursor follows core; input resyncs to it only on a change.
832 if g.Mode == c.constraint.Mode && g.Rect == c.constraint.Rect {
833 return
834 }
835 // A layout change must not leave a locked cursor off its window.
836 c.cursorX, c.cursorY = g.Clamp(c.cursorX, c.cursorY)
837 g.X, g.Y = c.cursorX, c.cursorY
838 c.constraint = g
839 if c.ch.Constraints != nil {
840 latest(c.ch.Constraints, g)
841 }
842}
843
844// intersect is the overlap of a and b, empty (zero size) when disjoint.
845func intersect(a, b Rect) Rect {
846 x0, y0 := max(a.X, b.X), max(a.Y, b.Y)
847 x1, y1 := min(a.X+a.W, b.X+b.W), min(a.Y+a.H, b.Y+b.H)
848 if x1 <= x0 || y1 <= y0 {
849 return Rect{}
850 }
851 return Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0}
852}
853
854// shownClient is the global logical client rectangle of a window drawn on
855// its output, whole (window-local coordinates start at its origin), and the
856// part of it inside the workspace viewport, which alone takes the pointer.
857// ok is false when it is hidden, a preview or entirely outside the viewport.
858func (c *Core) shownClient(id WindowID) (full, visible Rect, ok bool) {
859 if id == 0 {
860 return Rect{}, Rect{}, false
861 }
862 s, _ := c.screenOf(id)
863 if s == nil {
864 return Rect{}, Rect{}, false
865 }
866 for _, p := range s.shownLayout() {
867 if p.ID != id || p.Hidden || p.Preview > 0 {
868 continue
869 }
870 r := c.clientRect(p)
871 full = Rect{X: r.X + s.x, Y: r.Y + s.y, W: r.W, H: r.H}
872 visible = intersect(r, s.mon.viewport().area(p))
873 visible.X, visible.Y = visible.X+s.x, visible.Y+s.y
874 if visible.W <= 0 || visible.H <= 0 {
875 return Rect{}, Rect{}, false
876 }
877 return full, visible, true
878 }
879 return Rect{}, Rect{}, false
880}
881
882// shownClientRect is the visible part of shownClient.
883func (c *Core) shownClientRect(id WindowID) (Rect, bool) {
884 _, vis, ok := c.shownClient(id)
885 return vis, ok
886}
887
888// warpPointer moves the cursor to a window-local point of the window under
889// it (wp_pointer_warp_v1). Input takes the new position, and the window
890// gets the motion; the active constraint still bounds the point.
891func (c *Core) warpPointer(ctx context.Context, v ports.PointerWarp) error {
892 if v.ID == 0 || v.ID != c.pointer || c.grab != 0 && c.grab != v.ID {
893 return nil
894 }
895 full, vis, ok := c.shownClient(v.ID)
896 if !ok || v.X < 0 || v.Y < 0 || v.X >= float64(full.W) || v.Y >= float64(full.H) {
897 return nil
898 }
899 // Local to the whole client, and refused outside the viewport.
900 gx, gy := float64(full.X)+v.X, float64(full.Y)+v.Y
901 if gx < float64(vis.X) || gy < float64(vis.Y) || gx >= float64(vis.X+vis.W) || gy >= float64(vis.Y+vis.H) {
902 return nil
903 }
904 c.cursorX, c.cursorY = c.constraint.Clamp(gx, gy)
905 if c.ch.Constraints != nil {
906 g := c.constraint
907 g.X, g.Y, g.Warp = c.cursorX, c.cursorY, true
908 latest(c.ch.Constraints, g)
909 }
910 // A locked pointer moved: the client knows where it put it.
911 if c.constraint.Mode == ports.ConstraintLock {
912 return nil
913 }
914 return c.rehit(ctx)
915}
916
917// syncOutputs sends the output layout when it changed; input learns the
918// geometry only when the outputs themselves moved.
919func (c *Core) syncOutputs(ctx context.Context) error {
920 // Built in the scratch every publish, copied only when sent.
921 c.scratch.layout = c.layoutInto(c.scratch.layout)
922 v := ports.SetOutputs{Outputs: c.scratch.layout, Focused: c.cur().name(), Off: c.offOutputs()}
923 if sameOutputs(v, c.sent.outputs) {
924 return nil
925 }
926 v.Outputs = append(make(ports.Layout, 0, len(v.Outputs)), v.Outputs...)
927 if err := c.command(ctx, v); err != nil {
928 return err
929 }
930 if c.ch.Layouts != nil && !slices.Equal(v.Outputs, c.sent.outputs.Outputs) {
931 latest(c.ch.Layouts, v.Outputs)
932 }
933 c.sent.outputs = v
934 return nil
935}
936
937// syncFocus tells wayland the keyboard focus when it changed.
938func (c *Core) syncFocus(ctx context.Context, focus WindowID) error {
939 if focus == c.keyboard.sent {
940 return nil
941 }
942 if err := c.command(ctx, ports.FocusWindow{ID: focus}); err != nil {
943 return err
944 }
945 c.keyboard.sent = focus
946 return nil
947}
948
949// latest replaces any unread value: only the owner sends and drains;
950// consumers only receive.
951func latest[T any](ch chan T, v T) {
952 select {
953 case ch <- v:
954 default:
955 select {
956 case <-ch:
957 default:
958 }
959 select {
960 case ch <- v:
961 default:
962 }
963 }
964}
965
966func sameOutputs(a, b ports.SetOutputs) bool {
967 return a.Focused == b.Focused && slices.Equal(a.Outputs, b.Outputs) && slices.Equal(a.Off, b.Off)
968}
969
970// allLayers lists the layer surfaces of every output.
971func (c *Core) allLayers() []ports.LayerSurface {
972 var all []ports.LayerSurface
973 for _, sc := range c.screens {
974 all = append(all, sc.layers...)
975 }
976 return all
977}
978
979func (c *Core) acceptsInput(id WindowID, x, y float64) bool {
980 return c.windows.acceptsInput(id, x, y)
981}
982
983// hit returns the surface under the global logical point and the point in
984// its surface coordinates: popups, then overlay and top layers, then
985// windows (last visible placement is topmost), then bottom and
986// background layers.
987func (c *Core) hit(x, y float64) (WindowID, float64, float64) {
988 if c.security.Protected {
989 return c.lockHit(x, y)
990 }
991 i := c.screenAt(x, y)
992 if i < 0 {
993 return 0, 0, 0
994 }
995 sc := c.screens[i]
996 lx, ly := x-float64(sc.x), y-float64(sc.y)
997 // Layer popups are over everything; window popups are over the windows
998 // only, under the top and overlay layers (as drawn).
999 if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 {
1000 return id, px, py
1001 }
1002 if id, px, py := c.layerAt(sc, lx, ly, true); id != 0 {
1003 return id, px, py
1004 }
1005 if id, px, py := c.popupAt(sc, lx, ly, false); id != 0 {
1006 return id, px, py
1007 }
1008 if sc.mon.previewing() {
1009 // Previews and the layers under them take no input: a click
1010 // picks a preview (overviewClick, switcherClick).
1011 return 0, 0, 0
1012 }
1013 if !sc.mon.frameHas(lx, ly) {
1014 return c.layerAt(sc, lx, ly, false)
1015 }
1016 var id WindowID
1017 var sx, sy float64
1018 view := sc.mon.viewport()
1019 for _, p := range sc.shownLayout() {
1020 r := c.clientRect(p)
1021 // A peek is clickable wherever it shows, border included: it may
1022 // be narrower than its border. The point is clamped to its client.
1023 box, lx, ly := r, lx, ly
1024 if p.Peek {
1025 box = p.Rect
1026 }
1027 // Only what is drawn takes the pointer: a tile not over a panel.
1028 box = intersect(box, view.area(p))
1029 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) {
1030 if p.Peek {
1031 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))
1032 }
1033 if c.acceptsInput(p.ID, lx-float64(r.X), ly-float64(r.Y)) {
1034 // Layout is bottom to top, including fullscreen and floats.
1035 id, sx, sy = p.ID, lx-float64(r.X), ly-float64(r.Y)
1036 }
1037 }
1038 }
1039 if id == 0 {
1040 return c.layerAt(sc, lx, ly, false)
1041 }
1042 return id, sx, sy
1043}
1044
1045// Ping returns once Run handled a probe, or once ctx ends: the watchdog
1046// uses it to detect a stuck owner loop.
1047func (c *Core) Ping(ctx context.Context) {
1048 ack := make(chan struct{})
1049 select {
1050 case c.ping <- ack:
1051 case <-ctx.Done():
1052 return
1053 }
1054 select {
1055 case <-ack:
1056 case <-ctx.Done():
1057 }
1058}
1059
1060// CommandStats returns the command outbox counters since the last call.
1061// Any goroutine may call it.
1062func (c *Core) CommandStats() CommandStats { return c.out.takeStats() }
1063
1064func (c *Core) Run(ctx context.Context) error {
1065 // The sender stops with Run, not only with ctx: Run also returns on
1066 // quit and on errors. Run waits for it so nothing sends after Run.
1067 sendCtx, stopSender := context.WithCancel(ctx)
1068 senderDone := make(chan struct{})
1069 if c.ch.Commands != nil {
1070 c.out.start()
1071 }
1072 go func() {
1073 defer close(senderDone)
1074 if c.ch.Commands != nil {
1075 c.out.run(sendCtx, c.ch.Commands)
1076 }
1077 }()
1078 defer func() {
1079 stopSender()
1080 <-senderDone
1081 }()
1082 c.syncSecurity()
1083 defer c.stopFrame()
1084 defer c.stopCaptureTimer()
1085 defer c.stopFlashTimer()
1086 defer c.stopPositionHintTimer()
1087 defer c.cancelSwitcher()
1088 defer c.stopRepeat()
1089 // Startup commands run once per session. Full launcher queues retain a
1090 // bounded remainder, selectable alongside owner events without polling.
1091 c.startup = make([][]string, len(c.cfg.Startup))
1092 for i, argv := range c.cfg.Startup {
1093 c.startup[i] = slices.Clone(argv)
1094 }
1095 c.spawnStartup(ctx)
1096 if c.spawnSlots(ctx, false) != nil {
1097 return nil
1098 }
1099 c.publishState()
1100 c.publishWorkspaces()
1101 for {
1102 if c.securityCheckpoint(ctx) != nil {
1103 return nil
1104 }
1105 startupC, startupRequest := c.pendingStartup()
1106 select {
1107 case <-ctx.Done():
1108 return nil
1109 case startupC <- startupRequest:
1110 // Successful transfer consumes startup once. Only the receiver can
1111 // decide execution admission; a later gate read cannot infer it.
1112 c.startup = c.startup[1:]
1113 continue
1114 case ev, ok := <-c.ch.Client:
1115 if !ok {
1116 c.ch.Client = nil
1117 continue
1118 }
1119 if c.securityCheckpoint(ctx) != nil {
1120 return nil
1121 }
1122 if c.blockProtected(ev) {
1123 continue
1124 }
1125 publish, stop := c.clientEvent(ctx, ev)
1126 if stop {
1127 return nil
1128 }
1129 if publish {
1130 if c.publish(ctx) != nil {
1131 return nil
1132 }
1133 if c.dataDrag != nil && dataDragHitChanged(ev) && c.dataDragRetarget(ctx, c.motionTime) != nil {
1134 return nil
1135 }
1136 }
1137 case f, ok := <-c.ch.Frames:
1138 if c.securityCheckpoint(ctx) != nil {
1139 return nil
1140 }
1141 if !ok {
1142 c.ch.Frames = nil
1143 continue
1144 }
1145 if i := c.screenIndex(f.Output); i >= 0 && c.sliding(f.Output) && c.step(ctx, c.screens[i]) != nil {
1146 return nil
1147 }
1148 continue
1149 case ack := <-c.ping:
1150 close(ack)
1151 continue
1152 case <-c.frameC:
1153 if c.securityCheckpoint(ctx) != nil {
1154 return nil
1155 }
1156 c.frameC = nil
1157 if c.step(ctx, nil) != nil {
1158 return nil
1159 }
1160 continue
1161 case <-c.positionHintTimer.timerC:
1162 c.positionHintTimer.timerC = nil
1163 if c.securityCheckpoint(ctx) != nil || c.publish(ctx) != nil {
1164 return nil
1165 }
1166 continue
1167 case <-c.flash.timerC:
1168 if c.securityCheckpoint(ctx) != nil {
1169 return nil
1170 }
1171 if c.flashTick() && c.publish(ctx) != nil {
1172 return nil
1173 }
1174 continue
1175 case <-c.switcher.timerC:
1176 if c.securityCheckpoint(ctx) != nil {
1177 return nil
1178 }
1179 if c.switcherTick() && (c.publish(ctx) != nil || c.rehit(ctx) != nil) {
1180 return nil
1181 }
1182 continue
1183 case <-c.repeatC:
1184 if c.securityCheckpoint(ctx) != nil {
1185 return nil
1186 }
1187 if !c.repeatLive() {
1188 continue
1189 }
1190 // A held key is activity, as its press was: idle timers
1191 // must not fire while it repeats.
1192 if err := c.userActivity(ctx); err != nil {
1193 return nil
1194 }
1195 if c.securityCheckpoint(ctx) != nil {
1196 return nil
1197 }
1198 if err := c.repeatTick(ctx); err != nil {
1199 if errors.Is(err, errSecurityChanged) {
1200 continue
1201 }
1202 return err
1203 }
1204 continue
1205 case <-c.capt.timerC:
1206 c.capt.timerC, c.capt.timerStop = nil, nil
1207 if c.captureFlashTick() && c.publish(ctx) != nil {
1208 return nil
1209 }
1210 continue
1211 case res, ok := <-c.ch.Hints:
1212 if !ok {
1213 c.ch.Hints = nil
1214 continue
1215 }
1216 if c.securityCheckpoint(ctx) != nil {
1217 return nil
1218 }
1219 if c.hintResult(res) && c.publish(ctx) != nil {
1220 return nil
1221 }
1222 case ev, ok := <-c.ch.Output:
1223 if !ok {
1224 c.ch.Output = nil
1225 continue
1226 }
1227 if c.securityCheckpoint(ctx) != nil {
1228 return nil
1229 }
1230 switch v := ev.(type) {
1231 case ports.OutputAdded:
1232 c.addScreen(v.Info)
1233 if c.layerChanged {
1234 c.setLayers(c.allLayers())
1235 }
1236 c.cursorX, c.cursorY = c.clampPointer(c.cursorX, c.cursorY, c.cursorX, c.cursorY)
1237 case ports.OutputRemoved:
1238 // A drop target may be on the output going away.
1239 c.abortDrag()
1240 c.removeScreen(v.Name)
1241 if c.layerChanged {
1242 c.setLayers(c.allLayers())
1243 }
1244 c.cursorX, c.cursorY = c.clampPointer(c.cursorX, c.cursorY, c.cursorX, c.cursorY)
1245 }
1246 if c.workspaceVisible(ctx, false) != nil {
1247 return nil
1248 }
1249 if err := c.publish(ctx); err != nil {
1250 return nil
1251 }
1252 case ev, ok := <-c.ch.Config:
1253 if !ok {
1254 c.ch.Config = nil
1255 continue
1256 }
1257 if c.securityCheckpoint(ctx) != nil {
1258 return nil
1259 }
1260 // A new keymap quarantines held keys: their release never
1261 // comes, so no repeat may outlive a reload.
1262 c.stopRepeat()
1263 if err := c.apply(ev.Config); err != nil {
1264 select {
1265 case c.ch.ConfigErrors <- err:
1266 default:
1267 }
1268 continue
1269 }
1270 if !c.animOn() {
1271 c.stopAnimations()
1272 }
1273 // A rotation can shrink the layout under a still pointer.
1274 c.cursorX, c.cursorY = c.clampPointer(c.cursorX, c.cursorY, c.cursorX, c.cursorY)
1275 if c.workspaceVisible(ctx, false) != nil {
1276 return nil
1277 }
1278 if err := c.publish(ctx); err != nil {
1279 return nil
1280 }
1281 case ev, ok := <-c.ch.Input:
1282 if !ok {
1283 c.ch.Input = nil
1284 continue
1285 }
1286 if c.securityCheckpoint(ctx) != nil {
1287 return nil
1288 }
1289 // Even stale input is activity, but never becomes client input.
1290 if err := c.userActivity(ctx); err != nil {
1291 return nil
1292 }
1293 // Commands can backpressure; recheck after activity before admits/binds.
1294 if c.securityCheckpoint(ctx) != nil {
1295 return nil
1296 }
1297 var admitted bool
1298 ev, admitted = c.admitInput(ev)
1299 if !admitted {
1300 continue
1301 }
1302 if err := c.handleInput(ctx, ev); err != nil {
1303 if errors.Is(err, errSecurityChanged) {
1304 continue
1305 }
1306 return err
1307 }
1308 }
1309 }
1310}
1311
1312// workspaceVisible settles guests and refills slots on a show. Publication
1313// subsequently decides whether visible empty workspaces need terminals.
1314func (c *Core) workspaceVisible(ctx context.Context, shown bool) error {
1315 c.releaseSlots()
1316 c.settleGuests()
1317 return c.spawnSlots(ctx, shown)
1318}
1319
1320// spawnEmpty requests terminals for visible empty workspaces. publish calls
1321// it after the output layout and before scenes, so a terminal never maps
1322// before wayland knows its output.
1323func (c *Core) spawnEmpty(ctx context.Context) error {
1324 if c.protectionRequested() {
1325 return nil
1326 }
1327 first := c.firstTerminalResolved
1328 terms := c.fillEmpty()
1329 issued, err := c.dispatch(ctx, terms)
1330 if len(terms) > 0 && issued == 0 {
1331 c.firstTerminalResolved = first
1332 }
1333 return err
1334}
1335
1336// dispatch sends reqs to the launcher and reports how many it issued.
1337// From the first request that is not issued (context ended, or the gate
1338// suppressed the send), the rest are not sent and their claims are cancelled.
1339func (c *Core) dispatch(ctx context.Context, reqs []ports.SpawnRequest) (issued int, err error) {
1340 for i, req := range reqs {
1341 err = ctx.Err()
1342 if err != nil || !c.trySpawn(ctx, req) {
1343 for _, suppressed := range reqs[i:] {
1344 c.cancelClaim(suppressed)
1345 }
1346 return i, err
1347 }
1348 }
1349 return len(reqs), err
1350}
1351
1352// activate brings a window forward for a valid xdg-activation token. The
1353// token proves a user action in the requesting client (a click on a link,
1354// a notification), so unlike other client requests it may move the user
1355// (the exception to the ADR 011 golden rule).
1356func (c *Core) activate(ctx context.Context, id WindowID) error {
1357 s, w := c.screenOf(id)
1358 if s == nil || c.popups[id] != nil {
1359 return nil
1360 }
1361 before := c.cur().mon.Current()
1362 if w != s.mon.Current() {
1363 s.mon.show(w)
1364 }
1365 w.Activate(id)
1366 c.focusScreen = c.screenIndex(s.name())
1367 return c.workspaceVisible(ctx, c.cur().mon.Current() != before)
1368}
1369
1370// heldKey names a key for press tracking: by physical key, since Shift
1371// may be released before the key.
1372func heldKey(key ports.KeyEvent) string {
1373 if key.Keycode != 0 {
1374 return "#" + strconv.FormatUint(uint64(key.Keycode), 10)
1375 }
1376 return keyName(key.Keysym)
1377}