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