1package core
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "github.com/bnema/neferwl/internal/ports"
8 "maps"
9 "slices"
10 "strconv"
11 "strings"
12 "unicode"
13)
14
15var ErrQuit = errors.New("quit requested")
16
17// Channels connects the owner to adapters. Scenes must have capacity 1: core is
18// its only sender and may drain a stale set; consumers only receive. Each
19// send holds one scene per output.
20type Channels struct {
21 Client <-chan ports.ClientEvent
22 Input <-chan ports.InputEvent
23 Output <-chan ports.OutputEvent
24 Config <-chan ports.ConfigChanged
25 Commands chan<- ports.ClientCommand
26 Spawn chan<- ports.SpawnRequest
27 Scenes chan []ports.Scene
28 // Layouts, when set, receives the output layout whenever it changes,
29 // latest first (capacity 1, drained like Scenes). Input and cursors use it.
30 Layouts chan ports.Layout
31 // Constraints, when set, receives the active pointer constraint in
32 // global logical coordinates whenever it changes (capacity 1, drained
33 // like Scenes). Input applies it to the pointer.
34 Constraints chan ports.PointerConstraint
35 ConfigErrors chan<- error
36 // State, when set, receives a snapshot for scripts whenever it changes,
37 // latest first (capacity 1, drained like Scenes).
38 State chan ports.State
39 // Terminal, when set, keeps a window on every workspace on screen: an
40 // empty one gets the configured terminal (the terminal is the desktop).
41 Terminal bool
42}
43type binding struct {
44 mods ports.Mods
45 key string
46}
47type Core struct {
48 ch Channels
49 cfg ports.Config
50 screens []*screen
51 focusScreen int
52 binds map[binding]Action
53 pressed map[string]bool
54 sent map[WindowID]ports.ConfigureWindow
55 focus WindowID
56 pointer WindowID
57 grab WindowID
58 buttons map[uint32]bool
59 cursorX, cursorY float64 // global, logical
60 pointerOutput string // output under the pointer at the last motion
61 // constrained is the constraint wayland activated; constraint is it
62 // resolved to global coordinates, as last sent to input.
63 constrained ports.PointerConstrained
64 constraint ports.PointerConstraint
65 seq uint64
66 // layerChanged is set once layer state arrives from wayland.
67 layerChanged bool
68 sentOutputs ports.SetOutputs
69 specs []NamedWorkspace
70 presets []Width
71 overflow Overflow
72 slots map[slotKey]*slotState
73 // toSpawn holds slots to start; Run sends them (apply has no context).
74 toSpawn []slotKey
75 sentPending bool
76 // terms are the terminals spawned for empty workspaces, by SlotEnv
77 // token, until their window maps.
78 terms map[string]*termSpawn
79 // clients holds the app ID and PID of mapped windows, for State.
80 clients map[WindowID]ports.WindowMapped
81 sentState ports.State
82 // popups are the placed xdg_popups; popupOrder stacks them, oldest first.
83 popups map[WindowID]*popupState
84 popupOrder []WindowID
85 // layerFocus is the on-demand layer surface the user clicked; it keeps
86 // the keyboard while it stays mapped on-demand and no window focus
87 // changes on any output (layerOver, taken at the click): any click
88 // elsewhere, bind, activation or new window takes the keyboard back. The
89 // pointer moving to another output does not.
90 layerFocus WindowID
91 layerOver map[*screen]WindowID
92 // inhibitors are the windows asking to keep compositor binds while
93 // focused, inhibiting the one active now (0: none); idle are those
94 // keeping the session awake.
95 inhibitors map[WindowID]bool
96 inhibiting WindowID
97 idle map[WindowID]bool
98}
99
100func keyName(s string) string {
101 if len(s) == 1 && unicode.IsLetter(rune(s[0])) {
102 return strings.ToLower(s)
103 }
104 return s
105}
106func parseBind(s, cmd string) (binding, error) {
107 parts := strings.Split(s, "+")
108 b := binding{key: keyName(parts[len(parts)-1])}
109 if b.key == "" {
110 return b, fmt.Errorf("invalid bind %q", s)
111 }
112 for _, p := range parts[:len(parts)-1] {
113 var m ports.Mods
114 switch p {
115 case "Cmd":
116 switch cmd {
117 case "super":
118 m = ports.ModSuper
119 case "alt":
120 m = ports.ModAlt
121 case "ctrl":
122 m = ports.ModCtrl
123 }
124 case "Shift":
125 m = ports.ModShift
126 case "Ctrl":
127 m = ports.ModCtrl
128 case "Alt":
129 m = ports.ModAlt
130 case "Super":
131 m = ports.ModSuper
132 }
133 if m == 0 || b.mods&m != 0 {
134 return b, fmt.Errorf("invalid bind %q", s)
135 }
136 b.mods |= m
137 }
138 return b, nil
139}
140func (c *Core) apply(cfg ports.Config) error {
141 if cfg.Layout.MaxColumns < 1 {
142 return fmt.Errorf("invalid max columns")
143 }
144 presets := make([]Width, 0, len(cfg.Layout.Presets))
145 for _, s := range cfg.Layout.Presets {
146 v, e := ParseWidth(s)
147 if e != nil {
148 return e
149 }
150 presets = append(presets, v)
151 }
152 if cfg.Layout.Gaps < 0 {
153 return fmt.Errorf("invalid gaps")
154 }
155 if cfg.Keyboard.CmdKey != "super" && cfg.Keyboard.CmdKey != "alt" && cfg.Keyboard.CmdKey != "ctrl" {
156 return fmt.Errorf("invalid cmd key")
157 }
158 binds := map[binding]Action{}
159 for combo, a := range cfg.Binds {
160 b, e := parseBind(combo, cfg.Keyboard.CmdKey)
161 if e != nil {
162 return e
163 }
164 if _, ok := binds[b]; ok {
165 return fmt.Errorf("duplicate bind %q", combo)
166 }
167 if _, ok := SpawnArgv(Action(a)); ok {
168 binds[b] = Action(a)
169 continue
170 }
171 if Action(a) == ActionScaleUp || Action(a) == ActionScaleDown {
172 binds[b] = Action(a)
173 continue
174 }
175 if _, _, ok := WorkspaceArg(Action(a)); ok {
176 binds[b] = Action(a)
177 continue
178 }
179 if _, ok := NamedArg(Action(a)); ok {
180 binds[b] = Action(a)
181 continue
182 }
183 switch Action(a) {
184 case "none", ActionSpawnTerminal, ActionFocusColumnLeft, ActionFocusColumnRight, ActionFocusWindowUp, ActionFocusWindowDown, ActionMoveColumnLeft, ActionMoveColumnRight, ActionCycleColumnWidth, ActionToggleFullscreen, ActionCloseWindow, ActionQuit, ActionFocusWorkspaceUp, ActionFocusWorkspaceDown, ActionMoveColumnToWorkspaceUp, ActionMoveColumnToWorkspaceDown, ActionMoveWindowToWorkspaceUp, ActionMoveWindowToWorkspaceDown, ActionFocusMonitorLeft, ActionFocusMonitorRight, ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight, ActionConsumeOrExpelLeft, ActionConsumeOrExpelRight:
185 default:
186 return fmt.Errorf("invalid action %q", a)
187 }
188 binds[b] = Action(a)
189 }
190 overflow := func(v string) (Overflow, error) {
191 switch o := Overflow(v); o {
192 case "", OverflowScroll, OverflowFixed:
193 return o, nil
194 }
195 return "", fmt.Errorf("invalid overflow %q", v)
196 }
197 defOverflow, err := overflow(cfg.Layout.Overflow)
198 if err != nil {
199 return err
200 }
201 if defOverflow == "" {
202 defOverflow = OverflowScroll
203 }
204 named := make([]NamedWorkspace, 0, len(cfg.Workspaces))
205 seen := map[string]bool{}
206 for _, ws := range cfg.Workspaces {
207 o, err := overflow(ws.Overflow)
208 if err != nil {
209 return err
210 }
211 if ws.Name == "" || seen[ws.Name] || ws.MaxColumns < 0 {
212 return fmt.Errorf("invalid workspace %q", ws.Name)
213 }
214 seen[ws.Name] = true
215 named = append(named, NamedWorkspace{Name: ws.Name, Monitor: ws.Monitor, Hidden: ws.Hidden, MaxColumns: ws.MaxColumns, Overflow: o})
216 }
217 specs, err := parseSlots(cfg.Workspaces)
218 if err != nil {
219 return err
220 }
221 c.cfg = cfg
222 c.binds = binds
223 c.specs, c.presets, c.overflow = named, presets, defOverflow
224 c.toSpawn = append(c.toSpawn, c.updateSlots(specs)...)
225 c.named()
226 for _, s := range c.screens {
227 c.settings(s.mon)
228 }
229 c.applyConfigScales()
230 return nil
231}
232
233// publishPending tells wayland at once when slots start waiting, before
234// their windows can map.
235func (c *Core) publishPending(ctx context.Context) error {
236 if p := c.anyPending(); p != c.sentPending {
237 if err := c.command(ctx, ports.SlotsPending{Pending: p}); err != nil {
238 return err
239 }
240 c.sentPending = p
241 }
242 return nil
243}
244
245// spawnSlots starts the slots queued by config. With shown set (the user
246// just switched to another workspace), it also refills the empty slots of
247// the workspace now on screen.
248func (c *Core) spawnSlots(ctx context.Context, shown bool) error {
249 keys := c.toSpawn
250 c.toSpawn = nil
251 if shown {
252 keys = append(keys, c.refill()...)
253 }
254 started := map[slotKey]bool{}
255 for _, key := range keys {
256 st, ok := c.slots[key]
257 // A key can be queued twice (new slot on the workspace on screen).
258 if !ok || started[key] || st.window != 0 {
259 continue
260 }
261 started[key] = true
262 req := c.spawnSlot(key)
263 // Wayland must read slot tokens before this window can map.
264 if err := c.publishPending(ctx); err != nil {
265 return err
266 }
267 select {
268 case <-ctx.Done():
269 return ctx.Err()
270 case c.ch.Spawn <- req:
271 }
272 }
273 return nil
274}
275func New(cfg ports.Config, ch Channels) (*Core, error) {
276 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) {
277 return nil, fmt.Errorf("scenes, layouts, constraints and state must have capacity 1")
278 }
279 // A placeholder screen holds windows until the first output arrives.
280 c := &Core{screens: []*screen{{mon: NewMonitor(), scale: 1, cfgScale: 1}}, slots: map[slotKey]*slotState{}, terms: map[string]*termSpawn{}, clients: map[WindowID]ports.WindowMapped{}, popups: map[WindowID]*popupState{}, ch: ch, pressed: map[string]bool{}, buttons: map[uint32]bool{}, sent: map[WindowID]ports.ConfigureWindow{}, inhibitors: map[WindowID]bool{}, idle: map[WindowID]bool{}}
281 if err := c.apply(cfg); err != nil {
282 return nil, err
283 }
284 return c, nil
285}
286func (c *Core) command(ctx context.Context, v ports.ClientCommand) error {
287 select {
288 case <-ctx.Done():
289 return ctx.Err()
290 case c.ch.Commands <- v:
291 return nil
292 }
293}
294
295// clientRect is the placement minus the border on its inset sides.
296// The renderer applies the same inset (ports.Border).
297func (c *Core) clientRect(p Placement) Rect {
298 if p.Fullscreen {
299 return p.Rect
300 }
301 return p.Rect.Inset(p.Inset, c.cfg.Border.Width)
302}
303
304// visible reports whether the window, layer surface or popup is on screen
305// on any output.
306func (c *Core) visible(id WindowID) bool {
307 _, _, ok := c.windowRect(id)
308 return ok
309}
310
311// windowFocus is the focused window of each output.
312func (c *Core) windowFocus() map[*screen]WindowID {
313 m := make(map[*screen]WindowID, len(c.screens))
314 for _, sc := range c.screens {
315 m[sc], _ = sc.mon.Focused()
316 }
317 return m
318}
319
320// keyboardFocus is the mapped top/overlay layer with exclusive keyboard
321// interactivity and the highest ID on any output, else a grabbing popup,
322// else a clicked on-demand layer (see layerFocus), else the focused window
323// of the focused output.
324func (c *Core) keyboardFocus() WindowID {
325 var layer WindowID
326 for _, sc := range c.screens {
327 for _, l := range sc.layers {
328 if l.Keyboard == 1 && (l.Layer == ports.LayerTop || l.Layer == ports.LayerOverlay) && l.ID > layer {
329 layer = l.ID
330 }
331 }
332 }
333 if layer != 0 {
334 return layer
335 }
336 // A menu with a grab takes the keyboard until it closes.
337 if g := c.grabFocus(); g != 0 {
338 return g
339 }
340 if c.layerFocus != 0 && (!c.onDemand(c.layerFocus) || !maps.Equal(c.layerOver, c.windowFocus())) {
341 c.layerFocus, c.layerOver = 0, nil
342 }
343 if c.layerFocus != 0 {
344 return c.layerFocus
345 }
346 id, _ := c.cur().mon.Focused()
347 return id
348}
349
350// setLayers splits the mapped layer surfaces by output. A surface without an
351// output, or on an unknown one, goes to the focused output.
352func (c *Core) setLayers(all []ports.LayerSurface) {
353 for _, sc := range c.screens {
354 sc.layers = nil
355 }
356 for _, l := range all {
357 sc := c.cur()
358 if i := c.screenIndex(l.Output); i >= 0 {
359 sc = c.screens[i]
360 }
361 sc.layers = append(sc.layers, l)
362 }
363 for _, sc := range c.screens {
364 sc.arrange()
365 }
366}
367
368func (c *Core) publish(ctx context.Context) error {
369 if err := c.closeHiddenPopups(ctx); err != nil {
370 return err
371 }
372 // Clients learn outputs and scales before the configures sized for them.
373 if v := (ports.SetOutputs{Outputs: c.layout(), Focused: c.cur().name()}); !sameOutputs(v, c.sentOutputs) {
374 if err := c.command(ctx, v); err != nil {
375 return err
376 }
377 if !slices.Equal(v.Outputs, c.sentOutputs.Outputs) && c.ch.Layouts != nil {
378 latest(c.ch.Layouts, v.Outputs)
379 }
380 c.sentOutputs = v
381 }
382 terms := c.fillEmpty()
383 if err := c.publishPending(ctx); err != nil {
384 return err
385 }
386 for _, req := range terms {
387 select {
388 case <-ctx.Done():
389 return ctx.Err()
390 case c.ch.Spawn <- req:
391 }
392 }
393 alive := map[WindowID]bool{}
394 focus := c.keyboardFocus()
395 scenes := make([]ports.Scene, 0, len(c.screens))
396 // Before the first output (and after the last is unplugged) the
397 // placeholder's scene has no output name: no renderer draws it.
398 for i, sc := range c.screens {
399 c.seq++
400 o := sc.mon.Output()
401 scene := ports.Scene{Output: sc.name(), Seq: c.seq, OutputWidth: o.W, OutputHeight: o.H, Scale: sc.scale, 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: append([]ports.SceneLayer(nil), sc.placed...)}
402 layout := sc.mon.Layout()
403 // Only the focused output lights the focused window's lines.
404 scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), Rect{W: o.W, H: o.H}, i == c.focusScreen)
405 for _, p := range layout {
406 alive[p.ID] = true
407 // Only the focused output has an activated window.
408 focused := p.Focused && i == c.focusScreen
409 scene.Windows = append(scene.Windows, ports.SceneWindow{ID: p.ID, Rect: p.Rect, Focused: focused, Fullscreen: p.Fullscreen, Hidden: p.Hidden, Floating: p.Floating, Inset: p.Inset})
410 floating := p.Floating
411 if p.Hidden {
412 if old, ok := c.sent[p.ID]; ok && (old.Activated || old.Output != "") {
413 old.Activated, old.Output = false, ""
414 if err := c.command(ctx, old); err != nil {
415 return err
416 }
417 c.sent[p.ID] = old
418 }
419 continue
420 }
421 r := c.clientRect(p)
422 v := ports.ConfigureWindow{ID: p.ID, Width: r.W, Height: r.H, Fullscreen: p.Fullscreen, Activated: focused, Floating: floating && !p.Fullscreen, Output: sc.name()}
423 if v.Floating {
424 // A floating window picks its own size.
425 v.Width, v.Height = 0, 0
426 }
427 if old, ok := c.sent[p.ID]; !ok || old != v {
428 if err := c.command(ctx, v); err != nil {
429 return err
430 }
431 c.sent[p.ID] = v
432 }
433 }
434 scene.Windows = append(scene.Windows, c.scenePopups(sc)...)
435 scenes = append(scenes, scene)
436 }
437 for id := range c.sent {
438 if !alive[id] {
439 delete(c.sent, id)
440 }
441 }
442 // A workspace switch can hide the window under the pointer; it must not get
443 // clicks. The next motion re-runs hit-testing.
444 if c.pointer != 0 && !c.visible(c.pointer) {
445 c.pointer = 0
446 if c.grab == 0 {
447 if err := c.command(ctx, ports.PointerFocus{}); err != nil {
448 return err
449 }
450 }
451 }
452 if focus != c.focus {
453 if err := c.command(ctx, ports.FocusWindow{ID: focus}); err != nil {
454 return err
455 }
456 c.focus = focus
457 }
458 if err := c.updateInhibit(ctx); err != nil {
459 return err
460 }
461 c.resolveConstraint()
462 latest(c.ch.Scenes, scenes)
463 c.publishState()
464 return nil
465}
466
467// updateInhibit makes the shortcuts inhibitor of the keyboard focus the
468// active one and tells wayland when it changes.
469func (c *Core) updateInhibit(ctx context.Context) error {
470 want := WindowID(0)
471 if f := c.keyboardFocus(); f != 0 && c.inhibitors[f] {
472 want = f
473 }
474 if want == c.inhibiting {
475 return nil
476 }
477 if c.inhibiting != 0 {
478 if err := c.command(ctx, ports.ShortcutsInhibitState{Window: c.inhibiting}); err != nil {
479 return err
480 }
481 }
482 c.inhibiting = want
483 if want != 0 {
484 return c.command(ctx, ports.ShortcutsInhibitState{Window: want, Active: true})
485 }
486 return nil
487}
488
489// resolveConstraint turns the active constraint into global coordinates
490// and sends it to input when it changed. A hidden window holds nothing.
491func (c *Core) resolveConstraint() {
492 var g ports.PointerConstraint
493 if id := c.constrained.ID; id != 0 {
494 if s, _ := c.screenOf(id); s != nil {
495 for _, p := range s.mon.Layout() {
496 if p.ID != id || p.Hidden {
497 continue
498 }
499 r := c.clientRect(p)
500 r.X += s.x
501 if w := c.constrained.Rect; w.W > 0 && w.H > 0 {
502 // The region is clipped to the window.
503 x0, y0 := max(r.X, r.X+w.X), max(r.Y, r.Y+w.Y)
504 x1, y1 := min(r.X+r.W, r.X+w.X+w.W), min(r.Y+r.H, r.Y+w.Y+w.H)
505 if x1 > x0 && y1 > y0 {
506 r = Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0}
507 }
508 }
509 g = ports.PointerConstraint{Mode: c.constrained.Mode, Rect: r}
510 }
511 }
512 }
513 // The cursor follows core; input resyncs to it only on a change.
514 if g.Mode == c.constraint.Mode && g.Rect == c.constraint.Rect {
515 return
516 }
517 // A layout change must not leave a locked cursor off its window.
518 c.cursorX, c.cursorY = g.Clamp(c.cursorX, c.cursorY)
519 g.X, g.Y = c.cursorX, c.cursorY
520 c.constraint = g
521 if c.ch.Constraints != nil {
522 latest(c.ch.Constraints, g)
523 }
524}
525
526// latest replaces any unread value: only the owner sends and drains;
527// consumers only receive.
528func latest[T any](ch chan T, v T) {
529 select {
530 case ch <- v:
531 default:
532 select {
533 case <-ch:
534 default:
535 }
536 select {
537 case ch <- v:
538 default:
539 }
540 }
541}
542
543func sameOutputs(a, b ports.SetOutputs) bool {
544 return a.Focused == b.Focused && slices.Equal(a.Outputs, b.Outputs)
545}
546
547// allLayers lists the layer surfaces of every output.
548func (c *Core) allLayers() []ports.LayerSurface {
549 var all []ports.LayerSurface
550 for _, sc := range c.screens {
551 all = append(all, sc.layers...)
552 }
553 return all
554}
555
556// hit returns the surface under the global logical point and the point in
557// its surface coordinates: popups, then overlay and top layers, then
558// windows (fullscreen wins; otherwise the last visible placement is
559// topmost), then bottom and background layers.
560func (c *Core) hit(x, y float64) (WindowID, float64, float64) {
561 o, ok := c.layout().At(x, y)
562 if !ok {
563 return 0, 0, 0
564 }
565 lx, ly := x-float64(o.X), y-float64(o.Y)
566 sc := c.screens[c.screenIndex(o.Info.Name)]
567 // Layer popups are over everything; window popups are over the windows
568 // only, under the top and overlay layers (as drawn).
569 if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 {
570 return id, px, py
571 }
572 if id, px, py := layerAt(sc, lx, ly, true); id != 0 {
573 return id, px, py
574 }
575 if id, px, py := c.popupAt(sc, lx, ly, false); id != 0 {
576 return id, px, py
577 }
578 var id WindowID
579 var sx, sy float64
580 full := false
581 for _, p := range sc.mon.Layout() {
582 r := c.clientRect(p)
583 if !p.Hidden && r.W > 0 && r.H > 0 && lx >= float64(r.X) && lx < float64(r.X+r.W) && ly >= float64(r.Y) && ly < float64(r.Y+r.H) {
584 // Floating windows come last in the layout and are on top,
585 // even of a fullscreen window; nothing else is.
586 if full && !p.Floating {
587 continue
588 }
589 if id == 0 || p.Fullscreen || p.Floating {
590 id, sx, sy = p.ID, lx-float64(r.X), ly-float64(r.Y)
591 }
592 full = full || p.Fullscreen
593 }
594 }
595 if id == 0 {
596 return layerAt(sc, lx, ly, false)
597 }
598 return id, sx, sy
599}
600
601func (c *Core) Run(ctx context.Context) error {
602 // Startup commands run once per session; a config reload does not
603 // run them again.
604 for _, argv := range c.cfg.Startup {
605 select {
606 case <-ctx.Done():
607 return nil
608 case c.ch.Spawn <- ports.SpawnRequest{Argv: slices.Clone(argv)}:
609 }
610 }
611 if c.spawnSlots(ctx, false) != nil || c.publishPending(ctx) != nil {
612 return nil
613 }
614 c.publishState()
615 for {
616 select {
617 case <-ctx.Done():
618 return nil
619 case ev, ok := <-c.ch.Client:
620 if !ok {
621 c.ch.Client = nil
622 continue
623 }
624 switch v := ev.(type) {
625 case ports.LayerChanged:
626 c.layerChanged = true
627 c.setLayers(v.Layers)
628 case ports.WindowMapped:
629 c.clients[v.ID] = v
630 if v.Floating {
631 if s, _ := c.screenOf(v.ID); s == nil {
632 c.cur().mon.AddFloating(v.ID, v.Width, v.Height)
633 }
634 } else if v.Slot == "" || (!c.placeSlotWindow(v.ID, v.Slot) && !c.placeTerminal(v.ID, v.Slot)) {
635 if s, _ := c.screenOf(v.ID); s == nil {
636 c.cur().mon.AddWindow(v.ID)
637 }
638 }
639 case ports.WindowResized:
640 if _, w := c.screenOf(v.ID); w != nil {
641 w.ResizeFloating(v.ID, v.Width, v.Height)
642 }
643 case ports.PopupRequest:
644 if err := c.placePopup(ctx, v); err != nil {
645 return nil
646 }
647 case ports.PopupMapped:
648 if p := c.popups[v.ID]; p != nil {
649 p.mapped = true
650 }
651 case ports.ShortcutsInhibit:
652 if v.Active {
653 c.inhibitors[v.Window] = true
654 } else {
655 delete(c.inhibitors, v.Window)
656 }
657 if err := c.updateInhibit(ctx); err != nil {
658 return nil
659 }
660 continue
661 case ports.IdleInhibit:
662 if v.Active {
663 c.idle[v.Window] = true
664 } else {
665 delete(c.idle, v.Window)
666 }
667 c.publishState()
668 continue
669 case ports.WindowAppID:
670 if info, ok := c.clients[v.ID]; ok {
671 info.AppID = v.AppID
672 c.clients[v.ID] = info
673 }
674 case ports.WindowUnmapped:
675 if c.popups[v.ID] != nil {
676 if err := c.dropPopup(ctx, v.ID); err != nil {
677 return nil
678 }
679 }
680 if err := c.closePopupsOf(ctx, v.ID); err != nil {
681 return nil
682 }
683 delete(c.clients, v.ID)
684 delete(c.inhibitors, v.ID)
685 delete(c.idle, v.ID)
686 if s, _ := c.screenOf(v.ID); s != nil {
687 s.mon.RemoveWindow(v.ID)
688 }
689 c.releaseSlots()
690 if c.pointer == v.ID {
691 c.pointer = 0
692 if err := c.command(ctx, ports.PointerFocus{}); err != nil {
693 return nil
694 }
695 }
696 case ports.PointerConstrained:
697 c.constrained = v
698 case ports.WindowFullscreenRequest:
699 if s, _ := c.screenOf(v.ID); s != nil {
700 s.mon.SetFullscreen(v.ID, v.Fullscreen)
701 }
702 case ports.WindowActivate:
703 if c.activate(ctx, v.ID) != nil {
704 return nil
705 }
706 }
707 if err := c.publish(ctx); err != nil {
708 return nil
709 }
710 case ev, ok := <-c.ch.Output:
711 if !ok {
712 c.ch.Output = nil
713 continue
714 }
715 switch v := ev.(type) {
716 case ports.OutputAdded:
717 c.addScreen(v.Info)
718 if c.layerChanged {
719 c.setLayers(c.allLayers())
720 }
721 case ports.OutputRemoved:
722 c.removeScreen(v.Name)
723 if c.layerChanged {
724 c.setLayers(c.allLayers())
725 }
726 c.cursorX, c.cursorY = c.layout().Clamp(c.cursorX, c.cursorY, c.cursorX, c.cursorY)
727 }
728 if err := c.publish(ctx); err != nil {
729 return nil
730 }
731 case ev, ok := <-c.ch.Config:
732 if !ok {
733 c.ch.Config = nil
734 continue
735 }
736 if err := c.apply(ev.Config); err != nil {
737 select {
738 case c.ch.ConfigErrors <- err:
739 default:
740 }
741 continue
742 }
743 if c.spawnSlots(ctx, false) != nil {
744 return nil
745 }
746 if err := c.publish(ctx); err != nil {
747 return nil
748 }
749 case ev, ok := <-c.ch.Input:
750 if !ok {
751 c.ch.Input = nil
752 continue
753 }
754 switch v := ev.(type) {
755 case ports.PointerMotion:
756 // A locked pointer stays still; relative motion still flows.
757 if c.constraint.Mode != ports.ConstraintLock {
758 c.cursorX, c.cursorY = c.constraint.Clamp(c.layout().Clamp(c.cursorX, c.cursorY, v.X, v.Y))
759 }
760 // The focused screen follows the pointer, so new windows
761 // and launchers open where the user is.
762 // Only a pointer entering another output switches: keyboard
763 // moves to another screen stick until then. Not mid-drag.
764 if o, ok := c.layout().At(c.cursorX, c.cursorY); ok && o.Info.Name != c.pointerOutput && c.grab == 0 {
765 c.pointerOutput = o.Info.Name
766 if o.Info.Name != c.cur().name() {
767 c.focusScreen = c.screenIndex(o.Info.Name)
768 if err := c.publish(ctx); err != nil {
769 return nil
770 }
771 }
772 }
773 id, x, y := c.hit(c.cursorX, c.cursorY)
774 // Layout changes are intentionally re-hit-tested only on motion.
775 if id != c.pointer {
776 c.pointer = id
777 if err := c.command(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil {
778 return nil
779 }
780 }
781 if id != 0 {
782 if err := c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, DX: v.DX, DY: v.DY, UnaccelDX: v.UnaccelDX, UnaccelDY: v.UnaccelDY, TimeMsec: v.TimeMsec, TimeUsec: v.TimeUsec}); err != nil {
783 return nil
784 }
785 }
786 continue
787 case ports.PointerButton:
788 id := c.pointer
789 if c.grab != 0 {
790 id = c.grab
791 }
792 if v.Pressed && len(c.buttons) == 0 {
793 // A click outside an open menu closes it.
794 if err := c.dismissGrabs(ctx, id); err != nil {
795 return nil
796 }
797 }
798 if v.Pressed {
799 if len(c.buttons) == 0 {
800 c.grab = id
801 }
802 c.buttons[v.Button] = true
803 } else {
804 delete(c.buttons, v.Button)
805 }
806 if id != 0 {
807 if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, TimeMsec: v.TimeMsec}); err != nil {
808 return nil
809 }
810 // A click on an on-demand layer gives it the keyboard.
811 if v.Pressed && c.onDemand(id) && c.layerFocus != id {
812 c.layerFocus = id
813 c.layerOver = c.windowFocus()
814 if err := c.publish(ctx); err != nil {
815 return nil
816 }
817 } else if v.Pressed && c.layerFocus != 0 && id != c.layerFocus && c.popupRoot(id) != c.layerFocus {
818 // A click anywhere else takes the keyboard back.
819 c.layerFocus = 0
820 if err := c.publish(ctx); err != nil {
821 return nil
822 }
823 }
824 // A click focuses the window and its output.
825 s, w := c.screenOf(id)
826 if v.Pressed && s != nil && w == s.mon.Current() && (c.focus != id || s != c.cur()) {
827 w.FocusID(id)
828 c.focusScreen = c.screenIndex(s.name())
829 if err := c.publish(ctx); err != nil {
830 return nil
831 }
832 }
833 }
834 if len(c.buttons) == 0 {
835 c.grab = 0
836 }
837 continue
838 case ports.PointerAxis:
839 // Scroll goes to the window under the pointer, which has the
840 // pointer focus even mid-drag.
841 if c.pointer != 0 {
842 if err := c.command(ctx, ports.PointerAxisTo{ID: c.pointer, Axis: v}); err != nil {
843 return nil
844 }
845 }
846 continue
847 }
848 key, ok := ev.(ports.KeyEvent)
849 if !ok {
850 continue
851 }
852 // A focused window inhibiting shortcuts gets every key:
853 // no bind runs (emergency quit and VT switch are handled by
854 // input before core).
855 if c.inhibiting != 0 && c.inhibiting == c.keyboardFocus() {
856 if held := heldKey(key); !key.Pressed && c.pressed[held] {
857 // Its press ran a bind before inhibiting began: the
858 // window never saw it.
859 delete(c.pressed, held)
860 continue
861 }
862 if err := c.command(ctx, ports.ForwardKey{ID: c.inhibiting, Key: key}); err != nil {
863 return nil
864 }
865 continue
866 }
867 name := keyName(key.Keysym)
868 // A bind on the keysym wins; then the unshifted keysym (Cmd+Shift+1
869 // prints exclam on US); then the physical key (code:N).
870 action, bound := c.binds[binding{key: name, mods: key.Mods}]
871 if !bound && key.Base != "" {
872 action, bound = c.binds[binding{key: keyName(key.Base), mods: key.Mods}]
873 }
874 if !bound && key.Keycode != 0 {
875 action, bound = c.binds[binding{key: "code:" + strconv.FormatUint(uint64(key.Keycode), 10), mods: key.Mods}]
876 }
877 // Track presses by physical key: Shift may be released before the key.
878 held := heldKey(key)
879 if !key.Pressed {
880 consumed := c.pressed[held]
881 delete(c.pressed, held)
882 if consumed {
883 continue
884 }
885 }
886 if key.Pressed {
887 if bound {
888 c.pressed[held] = true
889 if action == ActionScaleUp || action == ActionScaleDown {
890 dir := 1
891 if action == ActionScaleDown {
892 dir = -1
893 }
894 sc := c.cur()
895 sc.setScale(StepScale(sc.info.Width, sc.info.Height, sc.scale, dir))
896 c.order()
897 if err := c.publish(ctx); err != nil {
898 return nil
899 }
900 continue
901 }
902 before := c.cur().mon.Current()
903 c.layerFocus = 0 // a bind acts on the windows
904 effect := c.applyAction(action)
905 if effect.Quit {
906 return ErrQuit
907 }
908 if effect.Spawn {
909 argv := effect.Argv
910 if argv == nil {
911 argv = append([]string(nil), c.cfg.Terminal.Command...)
912 }
913 select {
914 case <-ctx.Done():
915 return nil
916 case c.ch.Spawn <- ports.SpawnRequest{Argv: argv}:
917 }
918 }
919 if c.afterShow(ctx, before) != nil {
920 return nil
921 }
922 if effect.Close != 0 {
923 if err := c.command(ctx, ports.CloseWindow{ID: effect.Close}); err != nil {
924 return nil
925 }
926 }
927 if err := c.publish(ctx); err != nil {
928 return nil
929 }
930 continue
931 }
932 c.pressed[held] = false
933 }
934 if id := c.keyboardFocus(); id != 0 {
935 if err := c.command(ctx, ports.ForwardKey{ID: id, Key: key}); err != nil {
936 return nil
937 }
938 }
939 }
940 }
941}
942
943// afterShow keeps slots and guests right once a workspace came on screen;
944// before is the focused screen's workspace before the change.
945func (c *Core) afterShow(ctx context.Context, before *Workspace) error {
946 // Showing a declared workspace refills its empty slots.
947 c.releaseSlots()
948 c.settleGuests()
949 return c.spawnSlots(ctx, c.cur().mon.Current() != before)
950}
951
952// activate brings a window forward for a valid xdg-activation token. The
953// token proves a user action in the requesting client (a click on a link,
954// a notification), so unlike other client requests it may move the user
955// (the exception to the ADR 011 golden rule).
956func (c *Core) activate(ctx context.Context, id WindowID) error {
957 s, w := c.screenOf(id)
958 if s == nil || c.popups[id] != nil {
959 return nil
960 }
961 before := c.cur().mon.Current()
962 if w != s.mon.Current() {
963 s.mon.show(w)
964 }
965 w.Activate(id)
966 c.focusScreen = c.screenIndex(s.name())
967 return c.afterShow(ctx, before)
968}
969
970// heldKey names a key for press tracking: by physical key, since Shift
971// may be released before the key.
972func heldKey(key ports.KeyEvent) string {
973 if key.Keycode != 0 {
974 return "#" + strconv.FormatUint(uint64(key.Keycode), 10)
975 }
976 return keyName(key.Keysym)
977}