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, ActionFocusWorkspacePrev, ActionFocusWorkspaceNext, ActionMoveColumnToWorkspacePrev, ActionMoveColumnToWorkspaceNext, ActionMoveWindowToWorkspacePrev, ActionMoveWindowToWorkspaceNext, ActionFocusMonitorLeft, ActionFocusMonitorRight, ActionFocusMonitorUp, ActionFocusMonitorDown, ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight, ActionMoveWorkspaceToMonitorUp, ActionMoveWorkspaceToMonitorDown, ActionConsumeOrExpelLeft, ActionConsumeOrExpelRight,
246			ActionMoveWindowUp, ActionMoveWindowDown, ActionMoveWorkspacePrev, ActionMoveWorkspaceNext, 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	c.cfg = cfg
291	c.binds = binds
292	c.cmdMod = map[string]ports.Mods{"super": ports.ModSuper, "alt": ports.ModAlt, "ctrl": ports.ModCtrl}[cfg.Keyboard.CmdKey]
293	c.specs, c.presets = named, presets
294	c.toSpawn = append(c.toSpawn, c.updateSlots(specs)...)
295	c.named()
296	for _, s := range c.screens {
297		c.settings(s.mon)
298	}
299	c.applyOutputConfig()
300	return nil
301}
302
303// publishPending tells wayland at once when slots start waiting, before
304// their windows can map.
305func (c *Core) publishPending(ctx context.Context) error {
306	if p := c.placement.anyPending(); p != c.sentPending {
307		if err := c.command(ctx, ports.SlotsPending{Pending: p}); err != nil {
308			return err
309		}
310		c.sentPending = p
311	}
312	return nil
313}
314
315// spawnSlots starts the slots queued by config. With shown set (the user
316// just switched to another workspace), it also refills the empty slots of
317// the workspace now on screen.
318func (c *Core) spawnSlots(ctx context.Context, shown bool) error {
319	if c.protectionRequested() {
320		return nil
321	}
322	keys := c.toSpawn
323	c.toSpawn = nil
324	if shown {
325		keys = append(keys, c.refill()...)
326	}
327	started := map[slotKey]bool{}
328	for i, key := range keys {
329		if c.protectionRequested() {
330			c.toSpawn = append(c.toSpawn, keys[i:]...)
331			return nil
332		}
333		st, ok := c.slots[key]
334		// A key can be queued twice (new slot on the workspace on screen).
335		if !ok || started[key] || st.window != 0 {
336			continue
337		}
338		started[key] = true
339		req := c.spawnSlot(key)
340		// Wayland must read slot tokens before this window can map.
341		pendingErr := c.publishPending(ctx)
342		if pendingErr != nil || c.protectionRequested() {
343			c.placement.dropSlot(key)
344			st.stale = false
345			c.toSpawn = append(c.toSpawn, keys[i:]...)
346			return pendingErr
347		}
348		if !c.trySpawn(ctx, req) {
349			c.placement.dropSlot(key)
350			st.stale = false
351			c.toSpawn = append(c.toSpawn, keys[i:]...)
352			return ctx.Err()
353		}
354	}
355	return nil
356}
357func New(cfg ports.Config, ch Channels, opts Options) (*Core, error) {
358	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) {
359		return nil, fmt.Errorf("scenes, layouts, constraints, state and workspaces must have capacity 1")
360	}
361	// A placeholder screen holds windows until the first output arrives.
362	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()}
363	c.screens = []*screen{{mon: newMonitorWithIDs("", "", &c.nextWorkspaceID), scale: 1, cfgScale: 1}}
364	if err := c.apply(cfg); err != nil {
365		return nil, err
366	}
367	return c, nil
368}
369
370// newTimer makes a timer on clock, or the system timer when clock is nil.
371func newTimer(clock ports.Clock, d time.Duration) (<-chan time.Time, func() bool) {
372	t := newPortTimer(clock, d)
373	return t.C(), t.Stop
374}
375
376// newPortTimer makes a ports.Timer on clock, or on the system clock when
377// clock is nil.
378func newPortTimer(clock ports.Clock, d time.Duration) ports.Timer {
379	if clock != nil {
380		return clock.NewTimer(d)
381	}
382	return sysTimer{time.NewTimer(d)}
383}
384
385// sysTimer is a time.Timer as a ports.Timer.
386type sysTimer struct{ t *time.Timer }
387
388func (t sysTimer) C() <-chan time.Time        { return t.t.C }
389func (t sysTimer) Stop() bool                 { return t.t.Stop() }
390func (t sysTimer) Reset(d time.Duration) bool { return t.t.Reset(d) }
391
392func (c *Core) now() time.Time {
393	if c.opts.Clock != nil {
394		return c.opts.Clock.Now()
395	}
396	return time.Now()
397}
398
399func (c *Core) command(ctx context.Context, v ports.ClientCommand) error {
400	if c.inputEpochChanged() {
401		return errSecurityChanged
402	}
403	if c.opts.Security != nil {
404		v = ports.SecurityCommand{State: c.security, Command: v}
405	}
406	select {
407	case <-ctx.Done():
408		return ctx.Err()
409	case c.ch.Commands <- v:
410		if c.inputEpochChanged() {
411			return errSecurityChanged
412		}
413		return nil
414	}
415}
416
417// clientRect is the placement minus the border on its inset sides.
418// The renderer applies the same inset (ports.Border).
419func (c *Core) clientRect(p Placement) Rect {
420	if p.Fullscreen {
421		return p.Rect
422	}
423	return p.Rect.Inset(p.Inset, c.cfg.Border.Width)
424}
425
426// onScreen reports whether a placement is drawn on its output o: not
427// hidden and not scrolled off.
428func onScreen(p Placement, o Rect) bool {
429	return !p.Hidden && p.Rect.Overlaps(o)
430}
431
432// floatDim is the veil opacity of a layout: dim only when a float is
433// drawn above the tiles, never for a demoted covering float alone nor for
434// an overview preview. A leaving float (fading out after it closed or hid)
435// drops the veil at once: a veil that fades changes Scene.Dim every frame,
436// which redraws the whole output for the length of the fade, where the
437// float's own fade redraws only its rect.
438func floatDim(layout []Placement, o Rect, dim float64) float64 {
439	for _, p := range layout {
440		if p.Floating && !p.Below && p.Preview == 0 && onScreen(p, o) {
441			if p.Fullscreen {
442				return 0
443			}
444			return dim
445		}
446	}
447	return 0
448}
449
450// visible reports whether the window, layer surface or popup is on screen
451// on any output.
452func (c *Core) visible(id WindowID) bool {
453	_, _, ok := c.windowRect(id)
454	return ok
455}
456
457// windowFocus is the focused window of each output.
458func (c *Core) windowFocus() map[*screen]WindowID {
459	m := make(map[*screen]WindowID, len(c.screens))
460	for _, sc := range c.screens {
461		m[sc], _ = sc.mon.Focused()
462	}
463	return m
464}
465
466// keyboardFocus is the surface holding the keyboard (see keyboard.focus).
467func (c *Core) keyboardFocus() WindowID {
468	if c.security.Protected {
469		return c.lockKeyboardFocus()
470	}
471	var exclusive WindowID
472	for _, sc := range c.screens {
473		for _, l := range sc.layers {
474			if l.Keyboard == 1 && (l.Layer == ports.LayerTop || l.Layer == ports.LayerOverlay) && l.ID > exclusive {
475				exclusive = l.ID
476			}
477		}
478	}
479	window, _ := c.cur().mon.Focused()
480	return c.keyboard.focus(keyboardCandidates{
481		exclusive: exclusive,
482		grab:      c.grabFocus(),
483		window:    window,
484		layerShown: func(id WindowID) bool {
485			return c.onDemand(id) && c.visible(id)
486		},
487		windows: c.windowFocus,
488	})
489}
490
491// setLayers splits the mapped layer surfaces by output. A surface without an
492// output, or on an unknown one, goes to the focused output.
493func (c *Core) setLayers(all []ports.LayerSurface) {
494	previous := make(map[WindowID]bool)
495	for _, sc := range c.screens {
496		for _, l := range sc.layers {
497			previous[l.ID] = true
498		}
499		sc.layers = nil
500	}
501	for _, l := range all {
502		delete(previous, l.ID)
503		sc := c.cur()
504		if i := c.screenIndex(l.Output); i >= 0 {
505			sc = c.screens[i]
506		}
507		sc.layers = append(sc.layers, l)
508	}
509	for id := range previous {
510		c.windows.layerGone(id)
511	}
512	c.pruneRetained()
513	c.clampCaptureKeyboard()
514	for _, sc := range c.screens {
515		sc.arrange()
516	}
517}
518
519func (c *Core) publish(ctx context.Context) error { return c.publishFrame(ctx, nil) }
520
521// publishFrame publishes the scenes. only is the screen whose page flip
522// moved the springs (nil: any, or none moved): the pulse advances with the
523// focused screen's flip, and only a flip's own screen gets a fresh Seq for
524// its animation, so another output keeps its Seq while it draws the same.
525func (c *Core) publishFrame(ctx context.Context, only *screen) error {
526	if c.inputEpochChanged() {
527		return errSecurityChanged
528	}
529	c.syncSecurity()
530	if c.security.Protected {
531		return c.publishProtected(ctx)
532	}
533	for _, sc := range c.screens {
534		// The overview's selection is provisional: it focuses nothing yet.
535		if !sc.mon.ov.open {
536			sc.mon.Current().settleLeft()
537		}
538	}
539	c.refreshShown()
540	c.captureExpire()
541	capture, err := c.captureEvaluate(ctx)
542	if err != nil {
543		return err
544	}
545	// The workspace a session renders off screen keeps its real layout for
546	// the configures, popups and CaptureScene of this publish.
547	c.captureTrack(capture)
548	if err := c.closeHiddenPopups(ctx); err != nil {
549		return err
550	}
551	// Clients learn outputs and scales before the configures sized for them.
552	if err := c.syncOutputs(ctx); err != nil {
553		return err
554	}
555	// Output layout must be known before an automatic terminal can map.
556	if err := c.spawnEmpty(ctx); err != nil {
557		return err
558	}
559	if err := c.publishPending(ctx); err != nil {
560		return err
561	}
562	focus := c.keyboardFocus()
563	window, _ := c.cur().mon.Focused()
564	c.pulseFocus(window, focus == window)
565	// The pulse runs on the focused output: a flip of another output reuses
566	// its value so the focused scene keeps its Seq.
567	if c.pulse.id == 0 {
568		c.pulse.value = 0
569	} else if only == nil || only == c.cur() {
570		c.pulse.value = c.advancePulse(c.now())
571	}
572	pulse := c.pulse.value
573	// drawable: the scene can show a pulse on the focused window.
574	drawable := false
575	scenes := make([]ports.Scene, 0, len(c.screens))
576	// Before the first output (and after the last is unplugged) the
577	// placeholder's scene has no output name: no renderer draws it.
578	for i, sc := range c.screens {
579		o := sc.mon.Output()
580		// frame is the viewport of the workspace on screen: the whole output
581		// unless it has a size override (never in the overview).
582		frame := sc.mon.Frame()
583		var clip Rect
584		if frame != (Rect{W: o.W, H: o.H}) {
585			clip = frame
586		}
587		// layout is what is drawn; settled (same indexes) is where the
588		// windows are going, and alone sizes the configures.
589		layout, settled := sc.shown, sc.settledLayout
590		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)}
591		var real map[WindowID]Placement
592		if sc.mon.ov.open {
593			// Local to this screen's build: reused, cleared each time.
594			if c.overviewReal == nil {
595				c.overviewReal = make(map[WindowID]Placement)
596			}
597			clear(c.overviewReal)
598			real = c.overviewReal
599			for w := range sc.mon.all() {
600				c.realBuf = w.layoutInto(c.realBuf)
601				for _, p := range c.realBuf {
602					real[p.ID] = p
603				}
604			}
605		}
606		scene.Dim = floatDim(layout, frame, c.cfg.Floating.Dim)
607		if sc.mon.ov.open {
608			// Darken the wallpaper around the previews.
609			scene.Dim = c.cfg.Floating.Dim
610			scene.DimBehind = scene.Dim > 0
611		}
612		if d := c.drag; d != nil && d.target.screen == sc && d.target.kind != dropNone {
613			scene.DropHints = slices.Clone(d.target.hints)
614		}
615		if sc.mon.ov.open {
616			// Frame the selection and separate numbered and named row groups.
617			_, scene.Separators = sc.mon.overviewRows()
618			scene.Separators = append(scene.Separators, overviewOutline(layout, max(c.cfg.Border.Width, 2))...)
619		} else {
620			// Only the focused output lights the focused window's lines.
621			scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), frame, i == c.focusScreen)
622		}
623		// A window alone on screen needs no pulse to show it has the focus.
624		alone := i == c.focusScreen && c.pulse.target != 0 && visibleCount(layout, frame) == 1
625		for k, p := range layout {
626			ps := settled[k]
627			// Only the focused output has an activated window.
628			focused := p.Focused && i == c.focusScreen
629			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}
630			if p.Zoom > 0 && (p.Zoom < 1 || p.Preview > 0) {
631				// A scale motion: the content follows the drawn size. A
632				// card in flight drawn at its size still needs Zoom 1:
633				// without it the renderer would shrink it by Preview.
634				sw.Zoom = p.Zoom
635			}
636			// A peek's veil is the configured one plus its animated offset.
637			sw.Dim = max(0, min(p.Dim+c.peekDim(p), 1))
638			if p.Leaving {
639				// A window fading out after it closed or hid: drawn, but
640				// hidden to everything else, its configures included.
641				sw.Hidden = false
642				scene.Windows = append(scene.Windows, sw)
643				// A window the workspace still holds (a hidden stash
644				// window) keeps its last configure: prune would forget it,
645				// and its next one would start from nothing. A closed one
646				// was forgotten at its unmap and stays so.
647				if w, _ := sc.mon.find(p.ID); w != nil {
648					c.configures.keep(p.ID)
649				}
650				continue
651			}
652			if focused && p.ID == c.pulse.target && !alone && !p.Fullscreen && !p.Hidden && p.Preview == 0 && !sc.mon.ov.open {
653				drawable = true
654				if p.ID == c.pulse.id {
655					sw.FocusEffect = pulse
656				}
657			}
658			scene.Windows = append(scene.Windows, sw)
659			t := configureTarget{output: sc.name(), area: frame, focused: focused, captured: capture != nil && capture.window == p.ID}
660			if !p.Hidden && p.Preview == 0 {
661				// Only a sized configure needs the client size.
662				t.client, t.imposed = c.clientRect(ps), sc.mon.Current().imposedFloat(p.ID)
663			} else if t.captured && p.Hidden {
664				// A captured hidden window is sized like its capture.
665				t.client = capture.windowSz
666			} else if p.Preview > 0 && !p.Hidden {
667				if rp, ok := real[p.ID]; ok && !rp.Hidden {
668					t.realTiled = !rp.Floating
669					t.client = c.clientRect(rp)
670				}
671			}
672			cp, t := c.captureConfigure(sc, ps, t)
673			if v, send := c.configures.nextWithCapture(ps, t, cp); send {
674				if err := c.command(ctx, v); err != nil {
675					return err
676				}
677				c.configures.mark(v)
678			}
679		}
680		scene.Windows = append(scene.Windows, c.scenePopups(sc)...)
681		scene.CaptureIndicators = c.captureIndicators(sc)
682		scene.Capture = c.captureSceneFor(sc, capture)
683		// A scene carrying a capture image always gets a fresh Seq, and so
684		// does one of an animating screen (the one being stepped, or any
685		// when none is): the outputs drop a scene they already show, and a
686		// spring that starts at the previous scene, or rounds to it in its
687		// tail, would get no flip and wait for the fallback timer. Any other
688		// keeps its output's Seq while it draws the same, so an idle output
689		// is not recomposed.
690		withCapture := scene.Capture != nil && (sc == capture.hiddenScr || sc == capture.windowScr)
691		animating := (sc.springing() || c.pulsing(sc)) && (only == nil || only == sc)
692		if !withCapture && !animating && sc.last.Seq != 0 && scene.SameAs(sc.last) {
693			scene.Seq = sc.last.Seq
694		} else {
695			c.seq++
696			scene.Seq = c.seq
697		}
698		if withCapture {
699			switch sc {
700			case capture.hiddenScr:
701				scene.CaptureScene = c.captureScene(scene.Seq)
702			case capture.windowScr:
703				scene.CaptureScene = c.captureWindowScene(capture, scene.Seq)
704			}
705		}
706		sc.last = scene
707		scenes = append(scenes, scene)
708	}
709	c.configures.prune()
710	// A pulse nothing shows (fullscreen, overview) asks for no frames.
711	if c.pulse.drawable = drawable; !drawable {
712		c.pulse.id = 0
713	}
714	// A workspace switch can hide the window under the pointer; it must not get
715	// clicks. The next motion re-runs hit-testing.
716	if c.pointer != 0 && !c.visible(c.pointer) {
717		c.pointer = 0
718		if c.grab == 0 {
719			if err := c.command(ctx, ports.PointerFocus{}); err != nil {
720				return err
721			}
722		}
723	}
724	if err := c.syncFocus(ctx, focus); err != nil {
725		return err
726	}
727	if err := c.updateInhibit(ctx); err != nil {
728		return err
729	}
730	c.resolveConstraint()
731	if c.inputEpochChanged() {
732		return errSecurityChanged
733	}
734	if c.syncSecurity() {
735		return c.publishFrame(ctx, only)
736	}
737	latest(c.ch.Scenes, scenes)
738	c.publishState()
739	c.publishWorkspaces()
740	c.armCaptureTimer()
741	// A running slide moves on the next flip, or on the fallback timer.
742	if c.animating() {
743		if c.frameC == nil {
744			c.armFrame()
745		}
746	} else {
747		c.stopFrame()
748	}
749	return nil
750}
751
752// step moves the running slides of only (nil: every screen) to now and
753// publishes the frame.
754func (c *Core) step(ctx context.Context, only *screen) error {
755	// The fallback timer guards every animating output: a flip of one keeps
756	// it running while another still animates, or that one could starve.
757	if only == nil || !c.animatingOther(only) {
758		c.stopFrame()
759	}
760	c.animate(c.now(), only)
761	return c.slidFrame(ctx, false, only)
762}
763
764// animatingOther reports whether a spring or the pulse runs on a screen
765// other than only.
766func (c *Core) animatingOther(only *screen) bool {
767	for _, sc := range c.screens {
768		if sc != only && (sc.springing() || c.pulsing(sc)) {
769			return true
770		}
771	}
772	return false
773}
774
775// slid publishes a moved view; shown reports another workspace came on
776// screen.
777func (c *Core) slid(ctx context.Context, shown bool) error {
778	return c.slidFrame(ctx, shown, nil)
779}
780
781// slidFrame is slid for the page flip of only (nil: any screen).
782func (c *Core) slidFrame(ctx context.Context, shown bool, only *screen) error {
783	if err := c.workspaceVisible(ctx, shown); err != nil {
784		return err
785	}
786	if err := c.publishFrame(ctx, only); err != nil {
787		return err
788	}
789	return c.rehit(ctx)
790}
791
792// rehit points the pointer at what now lies under a still cursor, as
793// windows slide under it. A held button keeps its grab.
794func (c *Core) rehit(ctx context.Context) error {
795	if c.grab != 0 || c.drag != nil {
796		return nil
797	}
798	id, x, y := c.hit(c.cursorX, c.cursorY)
799	if id != c.pointer {
800		c.pointer, c.pointerAt = id, [2]float64{x, y}
801		return c.command(ctx, ports.PointerFocus{ID: id, X: x, Y: y})
802	}
803	if id == 0 || c.pointerAt == [2]float64{x, y} {
804		return nil
805	}
806	c.pointerAt = [2]float64{x, y}
807	// The same window moved under the cursor: it moves in the window. No
808	// relative motion: the device did not move. The input clock is the
809	// device's; the last motion's time is the closest core knows.
810	return c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, Time: c.motionTime})
811}
812
813// updateInhibit makes the shortcuts inhibitor of the keyboard focus the
814// active one and tells wayland when it changes.
815func (c *Core) updateInhibit(ctx context.Context) error {
816	if c.security.Protected {
817		return nil
818	}
819	f := c.keyboardFocus()
820	release, activate, changed := c.keyboard.inhibit(f, c.windows.lookup(f).inhibitShortcuts)
821	if !changed {
822		return nil
823	}
824	if release != 0 {
825		if err := c.command(ctx, ports.ShortcutsInhibitState{Window: release}); err != nil {
826			return err
827		}
828	}
829	if activate != 0 {
830		return c.command(ctx, ports.ShortcutsInhibitState{Window: activate, Active: true})
831	}
832	return nil
833}
834
835// resolveConstraint turns the active constraint into global coordinates
836// and sends it to input when it changed. A hidden window holds nothing.
837func (c *Core) resolveConstraint() {
838	var g ports.PointerConstraint
839	if full, vis, ok := c.shownClient(c.constrained.ID); ok {
840		r := full
841		if w := c.constrained.Rect; w.W > 0 && w.H > 0 {
842			// The region is window-local: placed from the full client origin,
843			// clipped to the window, then to what the viewport shows.
844			x0, y0 := max(full.X, full.X+w.X), max(full.Y, full.Y+w.Y)
845			x1, y1 := min(full.X+full.W, full.X+w.X+w.W), min(full.Y+full.H, full.Y+w.Y+w.H)
846			if x1 > x0 && y1 > y0 {
847				r = Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0}
848			}
849		}
850		r = intersect(r, vis)
851		if r.W > 0 && r.H > 0 {
852			g = ports.PointerConstraint{Mode: c.constrained.Mode, Rect: r}
853		} else {
854			// The region lies outside the viewport: hold the visible client.
855			g = ports.PointerConstraint{Mode: c.constrained.Mode, Rect: vis}
856		}
857	}
858	// The cursor follows core; input resyncs to it only on a change.
859	if g.Mode == c.constraint.Mode && g.Rect == c.constraint.Rect {
860		return
861	}
862	// A layout change must not leave a locked cursor off its window.
863	c.cursorX, c.cursorY = g.Clamp(c.cursorX, c.cursorY)
864	g.X, g.Y = c.cursorX, c.cursorY
865	c.constraint = g
866	if c.ch.Constraints != nil {
867		latest(c.ch.Constraints, g)
868	}
869}
870
871// intersect is the overlap of a and b, empty (zero size) when disjoint.
872func intersect(a, b Rect) Rect {
873	x0, y0 := max(a.X, b.X), max(a.Y, b.Y)
874	x1, y1 := min(a.X+a.W, b.X+b.W), min(a.Y+a.H, b.Y+b.H)
875	if x1 <= x0 || y1 <= y0 {
876		return Rect{}
877	}
878	return Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0}
879}
880
881// shownClient is the global logical client rectangle of a window drawn on
882// its output, whole (window-local coordinates start at its origin), and the
883// part of it inside the workspace viewport, which alone takes the pointer.
884// ok is false when it is hidden, a preview or entirely outside the viewport.
885func (c *Core) shownClient(id WindowID) (full, visible Rect, ok bool) {
886	if id == 0 {
887		return Rect{}, Rect{}, false
888	}
889	s, _ := c.screenOf(id)
890	if s == nil {
891		return Rect{}, Rect{}, false
892	}
893	for _, p := range s.shownLayout() {
894		if p.ID != id || p.Hidden || p.Preview > 0 {
895			continue
896		}
897		r := c.clientRect(p)
898		full = Rect{X: r.X + s.x, Y: r.Y + s.y, W: r.W, H: r.H}
899		f := s.mon.Frame()
900		visible = intersect(r, f)
901		visible.X, visible.Y = visible.X+s.x, visible.Y+s.y
902		if visible.W <= 0 || visible.H <= 0 {
903			return Rect{}, Rect{}, false
904		}
905		return full, visible, true
906	}
907	return Rect{}, Rect{}, false
908}
909
910// shownClientRect is the visible part of shownClient.
911func (c *Core) shownClientRect(id WindowID) (Rect, bool) {
912	_, vis, ok := c.shownClient(id)
913	return vis, ok
914}
915
916// warpPointer moves the cursor to a window-local point of the window under
917// it (wp_pointer_warp_v1). Input takes the new position, and the window
918// gets the motion; the active constraint still bounds the point.
919func (c *Core) warpPointer(ctx context.Context, v ports.PointerWarp) error {
920	if v.ID == 0 || v.ID != c.pointer || c.grab != 0 && c.grab != v.ID {
921		return nil
922	}
923	full, vis, ok := c.shownClient(v.ID)
924	if !ok || v.X < 0 || v.Y < 0 || v.X >= float64(full.W) || v.Y >= float64(full.H) {
925		return nil
926	}
927	// Local to the whole client, and refused outside the viewport.
928	gx, gy := float64(full.X)+v.X, float64(full.Y)+v.Y
929	if gx < float64(vis.X) || gy < float64(vis.Y) || gx >= float64(vis.X+vis.W) || gy >= float64(vis.Y+vis.H) {
930		return nil
931	}
932	c.cursorX, c.cursorY = c.constraint.Clamp(gx, gy)
933	if c.ch.Constraints != nil {
934		g := c.constraint
935		g.X, g.Y, g.Warp = c.cursorX, c.cursorY, true
936		latest(c.ch.Constraints, g)
937	}
938	// A locked pointer moved: the client knows where it put it.
939	if c.constraint.Mode == ports.ConstraintLock {
940		return nil
941	}
942	return c.rehit(ctx)
943}
944
945// syncOutputs sends the output layout when it changed; input learns the
946// geometry only when the outputs themselves moved.
947func (c *Core) syncOutputs(ctx context.Context) error {
948	v := ports.SetOutputs{Outputs: c.layout(), Focused: c.cur().name(), Off: c.offOutputs()}
949	if sameOutputs(v, c.sentOutputs) {
950		return nil
951	}
952	if err := c.command(ctx, v); err != nil {
953		return err
954	}
955	if c.ch.Layouts != nil && !slices.Equal(v.Outputs, c.sentOutputs.Outputs) {
956		latest(c.ch.Layouts, v.Outputs)
957	}
958	c.sentOutputs = v
959	return nil
960}
961
962// syncFocus tells wayland the keyboard focus when it changed.
963func (c *Core) syncFocus(ctx context.Context, focus WindowID) error {
964	if focus == c.keyboard.sent {
965		return nil
966	}
967	if err := c.command(ctx, ports.FocusWindow{ID: focus}); err != nil {
968		return err
969	}
970	c.keyboard.sent = focus
971	return nil
972}
973
974// latest replaces any unread value: only the owner sends and drains;
975// consumers only receive.
976func latest[T any](ch chan T, v T) {
977	select {
978	case ch <- v:
979	default:
980		select {
981		case <-ch:
982		default:
983		}
984		select {
985		case ch <- v:
986		default:
987		}
988	}
989}
990
991func sameOutputs(a, b ports.SetOutputs) bool {
992	return a.Focused == b.Focused && slices.Equal(a.Outputs, b.Outputs) && slices.Equal(a.Off, b.Off)
993}
994
995// allLayers lists the layer surfaces of every output.
996func (c *Core) allLayers() []ports.LayerSurface {
997	var all []ports.LayerSurface
998	for _, sc := range c.screens {
999		all = append(all, sc.layers...)
1000	}
1001	return all
1002}
1003
1004func (c *Core) acceptsInput(id WindowID, x, y float64) bool {
1005	return c.windows.acceptsInput(id, x, y)
1006}
1007
1008// hit returns the surface under the global logical point and the point in
1009// its surface coordinates: popups, then overlay and top layers, then
1010// windows (last visible placement is topmost), then bottom and
1011// background layers.
1012func (c *Core) hit(x, y float64) (WindowID, float64, float64) {
1013	if c.security.Protected {
1014		return c.lockHit(x, y)
1015	}
1016	i := c.screenAt(x, y)
1017	if i < 0 {
1018		return 0, 0, 0
1019	}
1020	sc := c.screens[i]
1021	lx, ly := x-float64(sc.x), y-float64(sc.y)
1022	// Layer popups are over everything; window popups are over the windows
1023	// only, under the top and overlay layers (as drawn).
1024	if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 {
1025		return id, px, py
1026	}
1027	if id, px, py := c.layerAt(sc, lx, ly, true); id != 0 {
1028		return id, px, py
1029	}
1030	if id, px, py := c.popupAt(sc, lx, ly, false); id != 0 {
1031		return id, px, py
1032	}
1033	if sc.mon.ov.open {
1034		// Previews and the layers under them take no input: a click
1035		// picks a preview (overviewClick).
1036		return 0, 0, 0
1037	}
1038	if !sc.mon.frameHas(lx, ly) {
1039		return c.layerAt(sc, lx, ly, false)
1040	}
1041	var id WindowID
1042	var sx, sy float64
1043	for _, p := range sc.shownLayout() {
1044		r := c.clientRect(p)
1045		// A peek is clickable wherever it shows, border included: it may
1046		// be narrower than its border. The point is clamped to its client.
1047		box, lx, ly := r, lx, ly
1048		if p.Peek {
1049			box = p.Rect
1050		}
1051		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) {
1052			if p.Peek {
1053				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))
1054			}
1055			if c.acceptsInput(p.ID, lx-float64(r.X), ly-float64(r.Y)) {
1056				// Layout is bottom to top, including fullscreen and floats.
1057				id, sx, sy = p.ID, lx-float64(r.X), ly-float64(r.Y)
1058			}
1059		}
1060	}
1061	if id == 0 {
1062		return c.layerAt(sc, lx, ly, false)
1063	}
1064	return id, sx, sy
1065}
1066
1067func (c *Core) Run(ctx context.Context) error {
1068	c.syncSecurity()
1069	defer c.stopFrame()
1070	defer c.stopCaptureTimer()
1071	defer c.stopPulseTimer()
1072	// Startup commands run once per session. Full launcher queues retain a
1073	// bounded remainder, selectable alongside owner events without polling.
1074	c.startup = make([][]string, len(c.cfg.Startup))
1075	for i, argv := range c.cfg.Startup {
1076		c.startup[i] = slices.Clone(argv)
1077	}
1078	c.spawnStartup(ctx)
1079	if c.spawnSlots(ctx, false) != nil || c.publishPending(ctx) != nil {
1080		return nil
1081	}
1082	c.publishState()
1083	c.publishWorkspaces()
1084	for {
1085		if c.securityCheckpoint(ctx) != nil {
1086			return nil
1087		}
1088		startupC, startupRequest := c.pendingStartup()
1089		select {
1090		case <-ctx.Done():
1091			return nil
1092		case startupC <- startupRequest:
1093			// Successful transfer consumes startup once. Only the receiver can
1094			// decide execution admission; a later gate read cannot infer it.
1095			c.startup = c.startup[1:]
1096			continue
1097		case ev, ok := <-c.ch.Client:
1098			if !ok {
1099				c.ch.Client = nil
1100				continue
1101			}
1102			if c.securityCheckpoint(ctx) != nil {
1103				return nil
1104			}
1105			if c.blockProtected(ev) {
1106				continue
1107			}
1108			switch v := ev.(type) {
1109			case ports.SessionLockChanged:
1110				if !c.applyLockChanged(v) {
1111					continue
1112				}
1113			case ports.CaptureSessionOpen:
1114				c.captureOpen(v)
1115			case ports.CaptureSessionClose:
1116				c.captureClose(v.ID)
1117			case ports.CaptureFrameTaken:
1118				// Nothing new to show (the target flashes already): no scene.
1119				if !c.captureFrame(v) {
1120					continue
1121				}
1122			case ports.CaptureExclusionBegin:
1123				c.captureExclusionBegin(v)
1124			case ports.CaptureExclusionLayer:
1125				c.captureExclusionLayer(v)
1126			case ports.CaptureExclusionEnd:
1127				c.captureExclusionEnd(v.Session)
1128			case ports.LayerChanged:
1129				c.layerChanged = true
1130				c.setLayers(v.Layers)
1131			case ports.InputRegionChanged:
1132				c.windows.setRegion(v)
1133			case ports.WindowMapped:
1134				c.mapWindow(v)
1135			case ports.WindowResized:
1136				c.resizeFloating(v)
1137			case ports.PopupRequest:
1138				if err := c.placePopup(ctx, v); err != nil {
1139					return nil
1140				}
1141			case ports.PopupMapped:
1142				if p := c.popups[v.ID]; p != nil {
1143					p.mapped = true
1144				}
1145			case ports.ShortcutsInhibit:
1146				c.windows.setInhibitShortcuts(v.Window, v.Active)
1147				if err := c.updateInhibit(ctx); err != nil {
1148					return nil
1149				}
1150				continue
1151			case ports.OutputPower:
1152				if i := c.screenIndex(v.Output); i >= 0 && c.screens[i].off == v.On {
1153					c.screens[i].off = !v.On
1154				}
1155			case ports.IdleInhibit:
1156				c.windows.setIdleInhibit(v.Window, v.Active)
1157				c.publishState()
1158				continue
1159			case ports.WindowAppID:
1160				c.windows.setAppID(v.ID, v.AppID)
1161			case ports.WindowParent:
1162				if _, w := c.screenOf(v.ID); w != nil {
1163					w.SetDialogParent(v.ID, v.Parent)
1164				}
1165			case ports.WindowUnmapped:
1166				if c.popups[v.ID] != nil {
1167					if err := c.dropPopup(ctx, v.ID); err != nil {
1168						return nil
1169					}
1170				}
1171				if err := c.closePopupsOf(ctx, v.ID); err != nil {
1172					return nil
1173				}
1174				c.windows.drop(v.ID)
1175				if c.drag != nil && c.drag.id == v.ID {
1176					c.abortDrag()
1177				}
1178				c.unmapWindow(v)
1179				c.releaseSlots()
1180				if c.pointer == v.ID {
1181					c.pointer = 0
1182					if err := c.command(ctx, ports.PointerFocus{}); err != nil {
1183						return nil
1184					}
1185				}
1186			case ports.PointerConstrained:
1187				c.constrained = v
1188			case ports.PointerWarp:
1189				if c.warpPointer(ctx, v) != nil {
1190					return nil
1191				}
1192			case ports.WindowMoveRequest:
1193				if c.clientDrag(ctx, v) != nil {
1194					return nil
1195				}
1196			case ports.WindowFullscreenRequest:
1197				c.configures.answer[v.ID] = true
1198				if v.Fullscreen && !v.External && c.now().Sub(c.windows.lookup(v.ID).mappedAt) < fullscreenGrace {
1199					break
1200				}
1201				// A taskbar request is a user action on that window, as an
1202				// activation: it comes on screen with the focus, leaving
1203				// another window's fullscreen.
1204				if v.External && v.Fullscreen && c.activate(ctx, v.ID) != nil {
1205					return nil
1206				}
1207				if s, _ := c.screenOf(v.ID); s != nil {
1208					s.mon.SetFullscreen(v.ID, v.Fullscreen)
1209					if w := s.mon.Current(); v.Fullscreen && w.fullscreen == v.ID {
1210						// A taskbar or late fullscreen request that took
1211						// effect: the window does not fade in, it keeps the
1212						// direct scanout path. One that was ignored (the
1213						// grace after the map) leaves the entrance running.
1214						delete(s.rects, v.ID)
1215					}
1216				}
1217			case ports.WorkspaceActivate:
1218				before := c.cur().mon.Current()
1219				for _, id := range v.IDs {
1220					for i, sc := range c.screens {
1221						if sc.name() == "" {
1222							continue
1223						}
1224						for w := range sc.mon.all() {
1225							if w.ID == id {
1226								c.focusScreen = i
1227								sc.mon.show(w)
1228								break
1229							}
1230						}
1231					}
1232				}
1233				if c.workspaceVisible(ctx, c.cur().mon.Current() != before) != nil {
1234					return nil
1235				}
1236			case ports.WindowActivate:
1237				if c.activate(ctx, v.ID) != nil {
1238					return nil
1239				}
1240			}
1241			if err := c.publish(ctx); err != nil {
1242				return nil
1243			}
1244		case f, ok := <-c.ch.Frames:
1245			if c.securityCheckpoint(ctx) != nil {
1246				return nil
1247			}
1248			if !ok {
1249				c.ch.Frames = nil
1250				continue
1251			}
1252			if i := c.screenIndex(f.Output); i >= 0 && c.sliding(f.Output) && c.step(ctx, c.screens[i]) != nil {
1253				return nil
1254			}
1255			continue
1256		case <-c.frameC:
1257			if c.securityCheckpoint(ctx) != nil {
1258				return nil
1259			}
1260			c.frameC = nil
1261			if c.step(ctx, nil) != nil {
1262				return nil
1263			}
1264			continue
1265		case <-c.pulse.timerC:
1266			if c.securityCheckpoint(ctx) != nil {
1267				return nil
1268			}
1269			if c.pulseTick() && c.publish(ctx) != nil {
1270				return nil
1271			}
1272			continue
1273		case <-c.capt.timerC:
1274			c.capt.timerC, c.capt.timerStop = nil, nil
1275			if c.captureFlashTick() && c.publish(ctx) != nil {
1276				return nil
1277			}
1278			continue
1279		case ev, ok := <-c.ch.Output:
1280			if !ok {
1281				c.ch.Output = nil
1282				continue
1283			}
1284			if c.securityCheckpoint(ctx) != nil {
1285				return nil
1286			}
1287			switch v := ev.(type) {
1288			case ports.OutputAdded:
1289				c.addScreen(v.Info)
1290				if c.layerChanged {
1291					c.setLayers(c.allLayers())
1292				}
1293				c.cursorX, c.cursorY = c.clampPointer(c.cursorX, c.cursorY, c.cursorX, c.cursorY)
1294			case ports.OutputRemoved:
1295				// A drop target may be on the output going away.
1296				c.abortDrag()
1297				c.removeScreen(v.Name)
1298				if c.layerChanged {
1299					c.setLayers(c.allLayers())
1300				}
1301				c.cursorX, c.cursorY = c.clampPointer(c.cursorX, c.cursorY, c.cursorX, c.cursorY)
1302			}
1303			if c.workspaceVisible(ctx, false) != nil {
1304				return nil
1305			}
1306			if err := c.publish(ctx); err != nil {
1307				return nil
1308			}
1309		case ev, ok := <-c.ch.Config:
1310			if !ok {
1311				c.ch.Config = nil
1312				continue
1313			}
1314			if c.securityCheckpoint(ctx) != nil {
1315				return nil
1316			}
1317			if err := c.apply(ev.Config); err != nil {
1318				select {
1319				case c.ch.ConfigErrors <- err:
1320				default:
1321				}
1322				continue
1323			}
1324			if !c.animOn() {
1325				c.stopAnimations()
1326			}
1327			// A rotation can shrink the layout under a still pointer.
1328			c.cursorX, c.cursorY = c.clampPointer(c.cursorX, c.cursorY, c.cursorX, c.cursorY)
1329			if c.workspaceVisible(ctx, false) != nil {
1330				return nil
1331			}
1332			if err := c.publish(ctx); err != nil {
1333				return nil
1334			}
1335		case ev, ok := <-c.ch.Input:
1336			if !ok {
1337				c.ch.Input = nil
1338				continue
1339			}
1340			if c.securityCheckpoint(ctx) != nil {
1341				return nil
1342			}
1343			// Even stale input is activity, but never becomes client input.
1344			if err := c.userActivity(ctx); err != nil {
1345				return nil
1346			}
1347			// Commands can backpressure; recheck after activity before admits/binds.
1348			if c.securityCheckpoint(ctx) != nil {
1349				return nil
1350			}
1351			var admitted bool
1352			ev, admitted = c.admitInput(ev)
1353			if !admitted {
1354				continue
1355			}
1356			if err := c.handleInput(ctx, ev); err != nil {
1357				if errors.Is(err, errSecurityChanged) {
1358					continue
1359				}
1360				return err
1361			}
1362		}
1363	}
1364}
1365
1366// workspaceVisible settles guests and refills slots on a show. Publication
1367// subsequently decides whether visible empty workspaces need terminals.
1368func (c *Core) workspaceVisible(ctx context.Context, shown bool) error {
1369	c.releaseSlots()
1370	c.settleGuests()
1371	return c.spawnSlots(ctx, shown)
1372}
1373
1374// spawnEmpty requests terminals for visible empty workspaces. publish calls
1375// it after the output layout and before scenes, so a terminal never maps
1376// before wayland knows its output.
1377func (c *Core) spawnEmpty(ctx context.Context) error {
1378	if c.protectionRequested() {
1379		return nil
1380	}
1381	first := c.firstTerminalResolved
1382	terms := c.fillEmpty()
1383	err := c.publishPending(ctx)
1384	for i, req := range terms {
1385		if err != nil || !c.trySpawn(ctx, req) {
1386			for _, suppressed := range terms[i:] {
1387				c.cancelTerminal(suppressed)
1388			}
1389			if i == 0 {
1390				c.firstTerminalResolved = first
1391			}
1392			return err
1393		}
1394	}
1395	return err
1396}
1397
1398// activate brings a window forward for a valid xdg-activation token. The
1399// token proves a user action in the requesting client (a click on a link,
1400// a notification), so unlike other client requests it may move the user
1401// (the exception to the ADR 011 golden rule).
1402func (c *Core) activate(ctx context.Context, id WindowID) error {
1403	s, w := c.screenOf(id)
1404	if s == nil || c.popups[id] != nil {
1405		return nil
1406	}
1407	before := c.cur().mon.Current()
1408	if w != s.mon.Current() {
1409		s.mon.show(w)
1410	}
1411	w.Activate(id)
1412	c.focusScreen = c.screenIndex(s.name())
1413	return c.workspaceVisible(ctx, c.cur().mon.Current() != before)
1414}
1415
1416// heldKey names a key for press tracking: by physical key, since Shift
1417// may be released before the key.
1418func heldKey(key ports.KeyEvent) string {
1419	if key.Keycode != 0 {
1420		return "#" + strconv.FormatUint(uint64(key.Keycode), 10)
1421	}
1422	return keyName(key.Keysym)
1423}

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