core.go RawHistoryFolder

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

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 26

Tags 10

↑↓ moveenter openesc close