feat/data-dnd e05cb15

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

Keyboard shortcuts

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

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

Branches 25

Tags 10

↑↓ moveenter openesc close