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.applyOutputConfig()
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, Transform: sc.transform, 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 sc.mon.ov.open {
565 // Darken the wallpaper around the previews.
566 scene.Dim = c.cfg.Floating.Dim
567 scene.DimBehind = scene.Dim > 0
568 }
569 if d := c.drag; d != nil && d.target.screen == sc && d.target.kind != dropNone {
570 scene.DropHints = slices.Clone(d.target.hints)
571 }
572 // Only the focused output lights the focused window's lines.
573 scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), frame, i == c.focusScreen)
574 if sc.mon.ov.open {
575 // Frame the selection and separate numbered and named row groups.
576 _, scene.Separators = sc.mon.overviewRows()
577 scene.Separators = append(scene.Separators, overviewOutline(layout, max(c.cfg.Border.Width, 2))...)
578 }
579 for _, p := range layout {
580 // Only the focused output has an activated window.
581 focused := p.Focused && i == c.focusScreen
582 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}
583 if p.Peek {
584 sw.Dim = c.cfg.Stash.Dim
585 }
586 if focused && p.ID == c.pulse.target && !p.Fullscreen && !p.Hidden && p.Preview == 0 && !sc.mon.ov.open {
587 drawable = true
588 if p.ID == c.pulse.id {
589 sw.FocusEffect = pulse
590 }
591 }
592 scene.Windows = append(scene.Windows, sw)
593 t := configureTarget{output: sc.name(), area: frame, focused: focused, captured: capture != nil && capture.window == p.ID}
594 if !p.Hidden && p.Preview == 0 {
595 // Only a sized configure needs the client size.
596 t.client, t.imposed = c.clientRect(p), sc.mon.Current().imposedFloat(p.ID)
597 } else if t.captured && p.Hidden {
598 // A captured hidden window is sized like its capture.
599 t.client = capture.windowSz
600 } else if p.Preview > 0 && !p.Hidden {
601 if rp, ok := real[p.ID]; ok && !rp.Hidden {
602 t.realTiled = !rp.Floating
603 t.client = c.clientRect(rp)
604 }
605 }
606 cp, t := c.captureConfigure(sc, p, t)
607 if v, send := c.configures.nextWithCapture(p, t, cp); send {
608 if err := c.command(ctx, v); err != nil {
609 return err
610 }
611 c.configures.mark(v)
612 }
613 }
614 scene.Windows = append(scene.Windows, c.scenePopups(sc)...)
615 scene.CaptureIndicators = c.captureIndicators(sc)
616 if scene.Capture = c.captureSceneFor(sc, capture); scene.Capture != nil {
617 switch sc {
618 case capture.hiddenScr:
619 scene.CaptureScene = c.captureScene(scene.Seq)
620 case capture.windowScr:
621 scene.CaptureScene = c.captureWindowScene(capture, scene.Seq)
622 }
623 }
624 scenes = append(scenes, scene)
625 }
626 c.configures.prune()
627 // A pulse nothing shows (fullscreen, overview) asks for no frames.
628 if c.pulse.drawable = drawable; !drawable {
629 c.pulse.id = 0
630 }
631 // A workspace switch can hide the window under the pointer; it must not get
632 // clicks. The next motion re-runs hit-testing.
633 if c.pointer != 0 && !c.visible(c.pointer) {
634 c.pointer = 0
635 if c.grab == 0 {
636 if err := c.command(ctx, ports.PointerFocus{}); err != nil {
637 return err
638 }
639 }
640 }
641 if err := c.syncFocus(ctx, focus); err != nil {
642 return err
643 }
644 if err := c.updateInhibit(ctx); err != nil {
645 return err
646 }
647 c.resolveConstraint()
648 if c.inputEpochChanged() {
649 return errSecurityChanged
650 }
651 if c.syncSecurity() {
652 return c.publish(ctx)
653 }
654 latest(c.ch.Scenes, scenes)
655 c.publishState()
656 c.publishWorkspaces()
657 c.armCaptureTimer()
658 // A running slide moves on the next flip, or on the fallback timer.
659 if c.animating() {
660 if c.frameC == nil {
661 c.armFrame()
662 }
663 } else {
664 c.stopFrame()
665 }
666 return nil
667}
668
669// step moves the running slides to now and publishes the frame.
670func (c *Core) step(ctx context.Context) error {
671 c.stopFrame()
672 c.animate(c.now())
673 return c.slid(ctx, false)
674}
675
676// slid publishes a moved view; shown reports another workspace came on
677// screen.
678func (c *Core) slid(ctx context.Context, shown bool) error {
679 if err := c.workspaceVisible(ctx, shown); err != nil {
680 return err
681 }
682 if err := c.publish(ctx); err != nil {
683 return err
684 }
685 return c.rehit(ctx)
686}
687
688// rehit points the pointer at what now lies under a still cursor, as
689// windows slide under it. A held button keeps its grab.
690func (c *Core) rehit(ctx context.Context) error {
691 if c.grab != 0 || c.drag != nil {
692 return nil
693 }
694 id, x, y := c.hit(c.cursorX, c.cursorY)
695 if id != c.pointer {
696 c.pointer, c.pointerAt = id, [2]float64{x, y}
697 return c.command(ctx, ports.PointerFocus{ID: id, X: x, Y: y})
698 }
699 if id == 0 || c.pointerAt == [2]float64{x, y} {
700 return nil
701 }
702 c.pointerAt = [2]float64{x, y}
703 // The same window moved under the cursor: it moves in the window. No
704 // relative motion: the device did not move. The input clock is the
705 // device's; the last motion's time is the closest core knows.
706 return c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, TimeMsec: c.motionMsec})
707}
708
709// updateInhibit makes the shortcuts inhibitor of the keyboard focus the
710// active one and tells wayland when it changes.
711func (c *Core) updateInhibit(ctx context.Context) error {
712 if c.security.Protected {
713 return nil
714 }
715 f := c.keyboardFocus()
716 release, activate, changed := c.keyboard.inhibit(f, c.windows.lookup(f).inhibitShortcuts)
717 if !changed {
718 return nil
719 }
720 if release != 0 {
721 if err := c.command(ctx, ports.ShortcutsInhibitState{Window: release}); err != nil {
722 return err
723 }
724 }
725 if activate != 0 {
726 return c.command(ctx, ports.ShortcutsInhibitState{Window: activate, Active: true})
727 }
728 return nil
729}
730
731// resolveConstraint turns the active constraint into global coordinates
732// and sends it to input when it changed. A hidden window holds nothing.
733func (c *Core) resolveConstraint() {
734 var g ports.PointerConstraint
735 if full, vis, ok := c.shownClient(c.constrained.ID); ok {
736 r := full
737 if w := c.constrained.Rect; w.W > 0 && w.H > 0 {
738 // The region is window-local: placed from the full client origin,
739 // clipped to the window, then to what the viewport shows.
740 x0, y0 := max(full.X, full.X+w.X), max(full.Y, full.Y+w.Y)
741 x1, y1 := min(full.X+full.W, full.X+w.X+w.W), min(full.Y+full.H, full.Y+w.Y+w.H)
742 if x1 > x0 && y1 > y0 {
743 r = Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0}
744 }
745 }
746 r = intersect(r, vis)
747 if r.W > 0 && r.H > 0 {
748 g = ports.PointerConstraint{Mode: c.constrained.Mode, Rect: r}
749 } else {
750 // The region lies outside the viewport: hold the visible client.
751 g = ports.PointerConstraint{Mode: c.constrained.Mode, Rect: vis}
752 }
753 }
754 // The cursor follows core; input resyncs to it only on a change.
755 if g.Mode == c.constraint.Mode && g.Rect == c.constraint.Rect {
756 return
757 }
758 // A layout change must not leave a locked cursor off its window.
759 c.cursorX, c.cursorY = g.Clamp(c.cursorX, c.cursorY)
760 g.X, g.Y = c.cursorX, c.cursorY
761 c.constraint = g
762 if c.ch.Constraints != nil {
763 latest(c.ch.Constraints, g)
764 }
765}
766
767// intersect is the overlap of a and b, empty (zero size) when disjoint.
768func intersect(a, b Rect) Rect {
769 x0, y0 := max(a.X, b.X), max(a.Y, b.Y)
770 x1, y1 := min(a.X+a.W, b.X+b.W), min(a.Y+a.H, b.Y+b.H)
771 if x1 <= x0 || y1 <= y0 {
772 return Rect{}
773 }
774 return Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0}
775}
776
777// shownClient is the global logical client rectangle of a window drawn on
778// its output, whole (window-local coordinates start at its origin), and the
779// part of it inside the workspace viewport, which alone takes the pointer.
780// ok is false when it is hidden, a preview or entirely outside the viewport.
781func (c *Core) shownClient(id WindowID) (full, visible Rect, ok bool) {
782 if id == 0 {
783 return Rect{}, Rect{}, false
784 }
785 s, _ := c.screenOf(id)
786 if s == nil {
787 return Rect{}, Rect{}, false
788 }
789 for _, p := range s.mon.Layout() {
790 if p.ID != id || p.Hidden || p.Preview > 0 {
791 continue
792 }
793 r := c.clientRect(p)
794 full = Rect{X: r.X + s.x, Y: r.Y + s.y, W: r.W, H: r.H}
795 f := s.mon.Frame()
796 visible = intersect(r, f)
797 visible.X, visible.Y = visible.X+s.x, visible.Y+s.y
798 if visible.W <= 0 || visible.H <= 0 {
799 return Rect{}, Rect{}, false
800 }
801 return full, visible, true
802 }
803 return Rect{}, Rect{}, false
804}
805
806// shownClientRect is the visible part of shownClient.
807func (c *Core) shownClientRect(id WindowID) (Rect, bool) {
808 _, vis, ok := c.shownClient(id)
809 return vis, ok
810}
811
812// warpPointer moves the cursor to a window-local point of the window under
813// it (wp_pointer_warp_v1). Input takes the new position, and the window
814// gets the motion; the active constraint still bounds the point.
815func (c *Core) warpPointer(ctx context.Context, v ports.PointerWarp) error {
816 if v.ID == 0 || v.ID != c.pointer || c.grab != 0 && c.grab != v.ID {
817 return nil
818 }
819 full, vis, ok := c.shownClient(v.ID)
820 if !ok || v.X < 0 || v.Y < 0 || v.X >= float64(full.W) || v.Y >= float64(full.H) {
821 return nil
822 }
823 // Local to the whole client, and refused outside the viewport.
824 gx, gy := float64(full.X)+v.X, float64(full.Y)+v.Y
825 if gx < float64(vis.X) || gy < float64(vis.Y) || gx >= float64(vis.X+vis.W) || gy >= float64(vis.Y+vis.H) {
826 return nil
827 }
828 c.cursorX, c.cursorY = c.constraint.Clamp(gx, gy)
829 if c.ch.Constraints != nil {
830 g := c.constraint
831 g.X, g.Y, g.Warp = c.cursorX, c.cursorY, true
832 latest(c.ch.Constraints, g)
833 }
834 // A locked pointer moved: the client knows where it put it.
835 if c.constraint.Mode == ports.ConstraintLock {
836 return nil
837 }
838 return c.rehit(ctx)
839}
840
841// syncOutputs sends the output layout when it changed; input learns the
842// geometry only when the outputs themselves moved.
843func (c *Core) syncOutputs(ctx context.Context) error {
844 v := ports.SetOutputs{Outputs: c.layout(), Focused: c.cur().name(), Off: c.offOutputs()}
845 if sameOutputs(v, c.sentOutputs) {
846 return nil
847 }
848 if err := c.command(ctx, v); err != nil {
849 return err
850 }
851 if c.ch.Layouts != nil && !slices.Equal(v.Outputs, c.sentOutputs.Outputs) {
852 latest(c.ch.Layouts, v.Outputs)
853 }
854 c.sentOutputs = v
855 return nil
856}
857
858// syncFocus tells wayland the keyboard focus when it changed.
859func (c *Core) syncFocus(ctx context.Context, focus WindowID) error {
860 if focus == c.keyboard.sent {
861 return nil
862 }
863 if err := c.command(ctx, ports.FocusWindow{ID: focus}); err != nil {
864 return err
865 }
866 c.keyboard.sent = focus
867 return nil
868}
869
870// latest replaces any unread value: only the owner sends and drains;
871// consumers only receive.
872func latest[T any](ch chan T, v T) {
873 select {
874 case ch <- v:
875 default:
876 select {
877 case <-ch:
878 default:
879 }
880 select {
881 case ch <- v:
882 default:
883 }
884 }
885}
886
887func sameOutputs(a, b ports.SetOutputs) bool {
888 return a.Focused == b.Focused && slices.Equal(a.Outputs, b.Outputs) && slices.Equal(a.Off, b.Off)
889}
890
891// allLayers lists the layer surfaces of every output.
892func (c *Core) allLayers() []ports.LayerSurface {
893 var all []ports.LayerSurface
894 for _, sc := range c.screens {
895 all = append(all, sc.layers...)
896 }
897 return all
898}
899
900func (c *Core) acceptsInput(id WindowID, x, y float64) bool {
901 return c.windows.acceptsInput(id, x, y)
902}
903
904// hit returns the surface under the global logical point and the point in
905// its surface coordinates: popups, then overlay and top layers, then
906// windows (last visible placement is topmost), then bottom and
907// background layers.
908func (c *Core) hit(x, y float64) (WindowID, float64, float64) {
909 if c.security.Protected {
910 return c.lockHit(x, y)
911 }
912 i := c.screenAt(x, y)
913 if i < 0 {
914 return 0, 0, 0
915 }
916 sc := c.screens[i]
917 lx, ly := x-float64(sc.x), y-float64(sc.y)
918 // Layer popups are over everything; window popups are over the windows
919 // only, under the top and overlay layers (as drawn).
920 if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 {
921 return id, px, py
922 }
923 if id, px, py := c.layerAt(sc, lx, ly, true); id != 0 {
924 return id, px, py
925 }
926 if id, px, py := c.popupAt(sc, lx, ly, false); id != 0 {
927 return id, px, py
928 }
929 if sc.mon.ov.open {
930 // Previews and the layers under them take no input: a click
931 // picks a preview (overviewClick).
932 return 0, 0, 0
933 }
934 if !sc.mon.frameHas(lx, ly) {
935 return c.layerAt(sc, lx, ly, false)
936 }
937 var id WindowID
938 var sx, sy float64
939 for _, p := range sc.mon.Layout() {
940 r := c.clientRect(p)
941 // A peek is clickable wherever it shows, border included: it may
942 // be narrower than its border. The point is clamped to its client.
943 box, lx, ly := r, lx, ly
944 if p.Peek {
945 box = p.Rect
946 }
947 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) {
948 if p.Peek {
949 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))
950 }
951 if c.acceptsInput(p.ID, lx-float64(r.X), ly-float64(r.Y)) {
952 // Layout is bottom to top, including fullscreen and floats.
953 id, sx, sy = p.ID, lx-float64(r.X), ly-float64(r.Y)
954 }
955 }
956 }
957 if id == 0 {
958 return c.layerAt(sc, lx, ly, false)
959 }
960 return id, sx, sy
961}
962
963func (c *Core) Run(ctx context.Context) error {
964 c.syncSecurity()
965 defer c.stopFrame()
966 defer c.stopCaptureTimer()
967 defer c.stopPulseTimer()
968 // Startup commands run once per session. Full launcher queues retain a
969 // bounded remainder, selectable alongside owner events without polling.
970 c.startup = make([][]string, len(c.cfg.Startup))
971 for i, argv := range c.cfg.Startup {
972 c.startup[i] = slices.Clone(argv)
973 }
974 c.spawnStartup(ctx)
975 if c.spawnSlots(ctx, false) != nil || c.publishPending(ctx) != nil {
976 return nil
977 }
978 c.publishState()
979 c.publishWorkspaces()
980 for {
981 if c.securityCheckpoint(ctx) != nil {
982 return nil
983 }
984 startupC, startupRequest := c.pendingStartup()
985 select {
986 case <-ctx.Done():
987 return nil
988 case startupC <- startupRequest:
989 // Successful transfer consumes startup once. Only the receiver can
990 // decide execution admission; a later gate read cannot infer it.
991 c.startup = c.startup[1:]
992 continue
993 case ev, ok := <-c.ch.Client:
994 if !ok {
995 c.ch.Client = nil
996 continue
997 }
998 if c.securityCheckpoint(ctx) != nil {
999 return nil
1000 }
1001 if c.blockProtected(ev) {
1002 continue
1003 }
1004 switch v := ev.(type) {
1005 case ports.SessionLockChanged:
1006 if !c.applyLockChanged(v) {
1007 continue
1008 }
1009 case ports.CaptureSessionOpen:
1010 c.captureOpen(v)
1011 case ports.CaptureSessionClose:
1012 c.captureClose(v.ID)
1013 case ports.CaptureFrameTaken:
1014 // Nothing new to show (the target flashes already): no scene.
1015 if !c.captureFrame(v) {
1016 continue
1017 }
1018 case ports.CaptureExclusionBegin:
1019 c.captureExclusionBegin(v)
1020 case ports.CaptureExclusionLayer:
1021 c.captureExclusionLayer(v)
1022 case ports.CaptureExclusionEnd:
1023 c.captureExclusionEnd(v.Session)
1024 case ports.LayerChanged:
1025 c.layerChanged = true
1026 c.setLayers(v.Layers)
1027 case ports.InputRegionChanged:
1028 c.windows.setRegion(v)
1029 case ports.WindowMapped:
1030 c.windows.mapped(v, c.now())
1031 c.placement.place(c, v)
1032 case ports.WindowResized:
1033 if _, w := c.screenOf(v.ID); w != nil {
1034 w.ResizeFloating(v.ID, v.Width, v.Height)
1035 }
1036 case ports.PopupRequest:
1037 if err := c.placePopup(ctx, v); err != nil {
1038 return nil
1039 }
1040 case ports.PopupMapped:
1041 if p := c.popups[v.ID]; p != nil {
1042 p.mapped = true
1043 }
1044 case ports.ShortcutsInhibit:
1045 c.windows.setInhibitShortcuts(v.Window, v.Active)
1046 if err := c.updateInhibit(ctx); err != nil {
1047 return nil
1048 }
1049 continue
1050 case ports.OutputPower:
1051 if i := c.screenIndex(v.Output); i >= 0 && c.screens[i].off == v.On {
1052 c.screens[i].off = !v.On
1053 }
1054 case ports.IdleInhibit:
1055 c.windows.setIdleInhibit(v.Window, v.Active)
1056 c.publishState()
1057 continue
1058 case ports.WindowAppID:
1059 c.windows.setAppID(v.ID, v.AppID)
1060 case ports.WindowParent:
1061 if _, w := c.screenOf(v.ID); w != nil {
1062 w.SetDialogParent(v.ID, v.Parent)
1063 }
1064 case ports.WindowUnmapped:
1065 if c.popups[v.ID] != nil {
1066 if err := c.dropPopup(ctx, v.ID); err != nil {
1067 return nil
1068 }
1069 }
1070 if err := c.closePopupsOf(ctx, v.ID); err != nil {
1071 return nil
1072 }
1073 c.windows.drop(v.ID)
1074 if c.drag != nil && c.drag.id == v.ID {
1075 c.abortDrag()
1076 }
1077 if s, _ := c.screenOf(v.ID); s != nil {
1078 s.mon.RemoveWindow(v.ID)
1079 }
1080 c.releaseSlots()
1081 if c.pointer == v.ID {
1082 c.pointer = 0
1083 if err := c.command(ctx, ports.PointerFocus{}); err != nil {
1084 return nil
1085 }
1086 }
1087 case ports.PointerConstrained:
1088 c.constrained = v
1089 case ports.PointerWarp:
1090 if c.warpPointer(ctx, v) != nil {
1091 return nil
1092 }
1093 case ports.WindowMoveRequest:
1094 if c.clientDrag(ctx, v) != nil {
1095 return nil
1096 }
1097 case ports.WindowFullscreenRequest:
1098 c.configures.answer[v.ID] = true
1099 if v.Fullscreen && !v.External && c.now().Sub(c.windows.lookup(v.ID).mappedAt) < fullscreenGrace {
1100 break
1101 }
1102 // A taskbar request is a user action on that window, as an
1103 // activation: it comes on screen with the focus, leaving
1104 // another window's fullscreen.
1105 if v.External && v.Fullscreen && c.activate(ctx, v.ID) != nil {
1106 return nil
1107 }
1108 if s, _ := c.screenOf(v.ID); s != nil {
1109 s.mon.SetFullscreen(v.ID, v.Fullscreen)
1110 }
1111 case ports.WorkspaceActivate:
1112 before := c.cur().mon.Current()
1113 for _, id := range v.IDs {
1114 for i, sc := range c.screens {
1115 if sc.name() == "" {
1116 continue
1117 }
1118 for w := range sc.mon.all() {
1119 if w.ID == id {
1120 c.focusScreen = i
1121 sc.mon.show(w)
1122 break
1123 }
1124 }
1125 }
1126 }
1127 if c.workspaceVisible(ctx, c.cur().mon.Current() != before) != nil {
1128 return nil
1129 }
1130 case ports.WindowActivate:
1131 if c.activate(ctx, v.ID) != nil {
1132 return nil
1133 }
1134 }
1135 if err := c.publish(ctx); err != nil {
1136 return nil
1137 }
1138 case f, ok := <-c.ch.Frames:
1139 if c.securityCheckpoint(ctx) != nil {
1140 return nil
1141 }
1142 if !ok {
1143 c.ch.Frames = nil
1144 continue
1145 }
1146 if c.sliding(f.Output) && c.step(ctx) != nil {
1147 return nil
1148 }
1149 continue
1150 case <-c.frameC:
1151 if c.securityCheckpoint(ctx) != nil {
1152 return nil
1153 }
1154 c.frameC, c.frameStop = nil, nil
1155 if c.step(ctx) != nil {
1156 return nil
1157 }
1158 continue
1159 case <-c.pulse.timerC:
1160 if c.securityCheckpoint(ctx) != nil {
1161 return nil
1162 }
1163 if c.pulseTick() && c.publish(ctx) != nil {
1164 return nil
1165 }
1166 continue
1167 case <-c.capt.timerC:
1168 c.capt.timerC, c.capt.timerStop = nil, nil
1169 if c.captureFlashTick() && c.publish(ctx) != nil {
1170 return nil
1171 }
1172 continue
1173 case ev, ok := <-c.ch.Output:
1174 if !ok {
1175 c.ch.Output = nil
1176 continue
1177 }
1178 if c.securityCheckpoint(ctx) != nil {
1179 return nil
1180 }
1181 switch v := ev.(type) {
1182 case ports.OutputAdded:
1183 c.addScreen(v.Info)
1184 if c.layerChanged {
1185 c.setLayers(c.allLayers())
1186 }
1187 c.cursorX, c.cursorY = c.clampPointer(c.cursorX, c.cursorY, c.cursorX, c.cursorY)
1188 case ports.OutputRemoved:
1189 // A drop target may be on the output going away.
1190 c.abortDrag()
1191 c.removeScreen(v.Name)
1192 if c.layerChanged {
1193 c.setLayers(c.allLayers())
1194 }
1195 c.cursorX, c.cursorY = c.clampPointer(c.cursorX, c.cursorY, c.cursorX, c.cursorY)
1196 }
1197 if c.workspaceVisible(ctx, false) != nil {
1198 return nil
1199 }
1200 if err := c.publish(ctx); err != nil {
1201 return nil
1202 }
1203 case ev, ok := <-c.ch.Config:
1204 if !ok {
1205 c.ch.Config = nil
1206 continue
1207 }
1208 if c.securityCheckpoint(ctx) != nil {
1209 return nil
1210 }
1211 if err := c.apply(ev.Config); err != nil {
1212 select {
1213 case c.ch.ConfigErrors <- err:
1214 default:
1215 }
1216 continue
1217 }
1218 // A rotation can shrink the layout under a still pointer.
1219 c.cursorX, c.cursorY = c.clampPointer(c.cursorX, c.cursorY, c.cursorX, c.cursorY)
1220 if c.workspaceVisible(ctx, false) != nil {
1221 return nil
1222 }
1223 if err := c.publish(ctx); err != nil {
1224 return nil
1225 }
1226 case ev, ok := <-c.ch.Input:
1227 if !ok {
1228 c.ch.Input = nil
1229 continue
1230 }
1231 if c.securityCheckpoint(ctx) != nil {
1232 return nil
1233 }
1234 // Even stale input is activity, but never becomes client input.
1235 if err := c.userActivity(ctx); err != nil {
1236 return nil
1237 }
1238 // Commands can backpressure; recheck after activity before admits/binds.
1239 if c.securityCheckpoint(ctx) != nil {
1240 return nil
1241 }
1242 var admitted bool
1243 ev, admitted = c.admitInput(ev)
1244 if !admitted {
1245 continue
1246 }
1247 if err := c.handleInput(ctx, ev); err != nil {
1248 if errors.Is(err, errSecurityChanged) {
1249 continue
1250 }
1251 return err
1252 }
1253 }
1254 }
1255}
1256
1257// workspaceVisible settles guests and refills slots on a show. Publication
1258// subsequently decides whether visible empty workspaces need terminals.
1259func (c *Core) workspaceVisible(ctx context.Context, shown bool) error {
1260 c.releaseSlots()
1261 c.settleGuests()
1262 return c.spawnSlots(ctx, shown)
1263}
1264
1265// spawnEmpty requests terminals for visible empty workspaces. publish calls
1266// it after the output layout and before scenes, so a terminal never maps
1267// before wayland knows its output.
1268func (c *Core) spawnEmpty(ctx context.Context) error {
1269 if c.protectionRequested() {
1270 return nil
1271 }
1272 first := c.firstTerminalResolved
1273 terms := c.fillEmpty()
1274 err := c.publishPending(ctx)
1275 for i, req := range terms {
1276 if err != nil || !c.trySpawn(ctx, req) {
1277 for _, suppressed := range terms[i:] {
1278 c.cancelTerminal(suppressed)
1279 }
1280 if i == 0 {
1281 c.firstTerminalResolved = first
1282 }
1283 return err
1284 }
1285 }
1286 return err
1287}
1288
1289// activate brings a window forward for a valid xdg-activation token. The
1290// token proves a user action in the requesting client (a click on a link,
1291// a notification), so unlike other client requests it may move the user
1292// (the exception to the ADR 011 golden rule).
1293func (c *Core) activate(ctx context.Context, id WindowID) error {
1294 s, w := c.screenOf(id)
1295 if s == nil || c.popups[id] != nil {
1296 return nil
1297 }
1298 before := c.cur().mon.Current()
1299 if w != s.mon.Current() {
1300 s.mon.show(w)
1301 }
1302 w.Activate(id)
1303 c.focusScreen = c.screenIndex(s.name())
1304 return c.workspaceVisible(ctx, c.cur().mon.Current() != before)
1305}
1306
1307// heldKey names a key for press tracking: by physical key, since Shift
1308// may be released before the key.
1309func heldKey(key ports.KeyEvent) string {
1310 if key.Keycode != 0 {
1311 return "#" + strconv.FormatUint(uint64(key.Keycode), 10)
1312 }
1313 return keyName(key.Keysym)
1314}