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