detached d5d679d

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

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