core.go RawHistoryFolder

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close