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