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