v0.4.0 b6be3a8

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

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 25

Tags 10

↑↓ moveenter openesc close