v0.9.0 458251f

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

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