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