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