main f2d166b

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	c.cfg = cfg
357	c.binds = binds
358	c.cmdMod = map[string]ports.Mods{"super": ports.ModSuper, "alt": ports.ModAlt, "ctrl": ports.ModCtrl}[cfg.Keyboard.CmdKey]
359	c.specs, c.rules = named, rules
360	c.toSpawn = append(c.toSpawn, c.updateSlots(specs)...)
361	c.named()
362	for _, s := range c.screens {
363		c.settings(s.mon)
364	}
365	c.applyOutputConfig()
366	for _, s := range c.screens {
367		old, now := axes[s.mon], s.mon.Current().policy()
368		if old.content != now.content || old.workspace != now.workspace {
369			s.mon.ov.dropScroll()
370		}
371	}
372	return nil
373}
374
375// spawnSlots starts the slots queued by config. With shown set (the user
376// just switched to another workspace), it also refills the empty slots of
377// the workspace now on screen.
378func (c *Core) spawnSlots(ctx context.Context, shown bool) error {
379	if c.protectionRequested() {
380		return nil
381	}
382	keys := c.toSpawn
383	c.toSpawn = nil
384	if shown {
385		keys = append(keys, c.refill()...)
386	}
387	started := map[slotKey]bool{}
388	for i, key := range keys {
389		if c.protectionRequested() {
390			c.toSpawn = append(c.toSpawn, keys[i:]...)
391			return nil
392		}
393		st, ok := c.slots[key]
394		// A key can be queued twice (new slot on the workspace on screen).
395		if !ok || started[key] || st.window != 0 {
396			continue
397		}
398		started[key] = true
399		req := c.spawnSlot(key)
400		if err := ctx.Err(); err != nil || c.protectionRequested() {
401			c.placement.dropSlot(key)
402			st.stale = false
403			c.toSpawn = append(c.toSpawn, keys[i:]...)
404			return err
405		}
406		if !c.trySpawn(ctx, req) {
407			c.placement.dropSlot(key)
408			st.stale = false
409			c.toSpawn = append(c.toSpawn, keys[i:]...)
410			return ctx.Err()
411		}
412	}
413	return nil
414}
415func New(cfg ports.Config, ch Channels, opts Options) (*Core, error) {
416	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) {
417		return nil, fmt.Errorf("scenes, layouts, constraints, state and workspaces must have capacity 1")
418	}
419	// A placeholder screen holds windows until the first output arrives.
420	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()}
421	c.screens = []*screen{{mon: newMonitorWithIDs("", "", &c.nextWorkspaceID), scale: 1, cfgScale: 1}}
422	if err := c.apply(cfg); err != nil {
423		return nil, err
424	}
425	return c, nil
426}
427
428// newTimer makes a timer on clock, or the system timer when clock is nil.
429func newTimer(clock ports.Clock, d time.Duration) (<-chan time.Time, func() bool) {
430	t := newPortTimer(clock, d)
431	return t.C(), t.Stop
432}
433
434// newPortTimer makes a ports.Timer on clock, or on the system clock when
435// clock is nil.
436func newPortTimer(clock ports.Clock, d time.Duration) ports.Timer {
437	if clock != nil {
438		return clock.NewTimer(d)
439	}
440	return sysTimer{time.NewTimer(d)}
441}
442
443// sysTimer is a time.Timer as a ports.Timer.
444type sysTimer struct{ t *time.Timer }
445
446func (t sysTimer) C() <-chan time.Time        { return t.t.C }
447func (t sysTimer) Stop() bool                 { return t.t.Stop() }
448func (t sysTimer) Reset(d time.Duration) bool { return t.t.Reset(d) }
449
450func (c *Core) now() time.Time {
451	if c.opts.Clock != nil {
452		return c.opts.Clock.Now()
453	}
454	return time.Now()
455}
456
457// command queues v for the wayland goroutine and never blocks: a busy
458// display must not stall keyboard, scenes or frame wakes. The sender
459// goroutine of Run delivers the queue in order.
460func (c *Core) command(ctx context.Context, v ports.ClientCommand) error {
461	_, err := c.queue(ctx, v)
462	return err
463}
464
465// queue pushes v to the outbox and returns its sequence number (0: not
466// queued).
467func (c *Core) queue(ctx context.Context, v ports.ClientCommand) (uint64, error) {
468	if err := ctx.Err(); err != nil {
469		return 0, err
470	}
471	if c.inputEpochChanged() {
472		return 0, errSecurityChanged
473	}
474	if c.ch.Commands == nil {
475		// Nobody receives: queueing would only grow without bound.
476		return 0, nil
477	}
478	seq := c.out.push(c.ch.Commands, v, c.security, c.opts.Security != nil)
479	if c.out.backlog() > outboxLimit {
480		if err := c.out.flush(ctx, c.ch.Commands, seq-outboxLimit); err != nil {
481			return seq, err
482		}
483	}
484	// The queued command keeps the epoch it was made under, so a display
485	// that has moved on drops it. Report the change all the same: the rest
486	// of the event was decided under the old epoch and must not go on.
487	if c.inputEpochChanged() {
488		return seq, errSecurityChanged
489	}
490	return seq, nil
491}
492
493// clientRect is the placement minus the border on its inset sides.
494// The renderer applies the same inset (ports.Border).
495func (c *Core) clientRect(p Placement) Rect {
496	if p.Fullscreen {
497		return p.Rect
498	}
499	return p.Rect.Inset(p.Inset, c.cfg.Border.Width)
500}
501
502// viewport is where a workspace is seen: frame is the whole viewport,
503// usable the part exclusive layers (panels) leave to the tiles.
504type viewport struct{ frame, usable Rect }
505
506// whole is a viewport with no panel: all of r is usable.
507func whole(r Rect) viewport { return viewport{frame: r, usable: r} }
508
509// area is where p can be seen, drawn and pointed at. A tile lives in the
510// usable area and is cut at its edge (Scene.TileInset), so a cascade band
511// or a column scrolled under a panel is not seen; a float, a fullscreen
512// window and an overview preview are placed against the frame.
513func (v viewport) area(p Placement) Rect {
514	if p.Floating || p.Fullscreen || p.Preview > 0 {
515		return v.frame
516	}
517	return v.usable
518}
519
520// shows reports whether the user sees p: not hidden, and overlapping its
521// area.
522func (v viewport) shows(p Placement) bool {
523	return !p.Hidden && p.Rect.Overlaps(v.area(p))
524}
525
526// floatDim is the veil opacity of a layout: dim only when a float is
527// drawn above the tiles, never for a demoted covering float alone nor for
528// an overview preview. A leaving float (fading out after it closed or hid)
529// drops the veil at once: a veil that fades changes Scene.Dim every frame,
530// which redraws the whole output for the length of the fade, where the
531// float's own fade redraws only its rect.
532func floatDim(layout []Placement, v viewport, dim float64) float64 {
533	for _, p := range layout {
534		if p.Floating && !p.Below && p.Preview == 0 && v.shows(p) {
535			if p.Fullscreen {
536				return 0
537			}
538			return dim
539		}
540	}
541	return 0
542}
543
544// visible reports whether the window, layer surface or popup is on screen
545// on any output.
546func (c *Core) visible(id WindowID) bool {
547	_, _, ok := c.windowRect(id)
548	return ok
549}
550
551// windowFocus is the focused window of each output.
552func (c *Core) windowFocus() map[*screen]WindowID {
553	m := make(map[*screen]WindowID, len(c.screens))
554	for _, sc := range c.screens {
555		m[sc], _ = sc.mon.Focused()
556	}
557	return m
558}
559
560// keyboardFocus is the surface holding the keyboard (see keyboard.focus).
561func (c *Core) keyboardFocus() WindowID {
562	if c.security.Protected {
563		return c.lockKeyboardFocus()
564	}
565	var exclusive WindowID
566	for _, sc := range c.screens {
567		for _, l := range sc.layers {
568			if l.Keyboard == 1 && (l.Layer == ports.LayerTop || l.Layer == ports.LayerOverlay) && l.ID > exclusive {
569				exclusive = l.ID
570			}
571		}
572	}
573	window, _ := c.cur().mon.Focused()
574	return c.keyboard.focus(keyboardCandidates{
575		exclusive: exclusive,
576		grab:      c.grabFocus(),
577		window:    window,
578		layerShown: func(id WindowID) bool {
579			return c.onDemand(id) && c.visible(id)
580		},
581		windows: c.windowFocus,
582	})
583}
584
585// setLayers splits the mapped layer surfaces by output. A surface without an
586// output, or on an unknown one, goes to the focused output.
587func (c *Core) setLayers(all []ports.LayerSurface) {
588	previous := make(map[WindowID]bool)
589	for _, sc := range c.screens {
590		for _, l := range sc.layers {
591			previous[l.ID] = true
592		}
593		sc.layers = nil
594	}
595	for _, l := range all {
596		delete(previous, l.ID)
597		sc := c.cur()
598		if i := c.screenIndex(l.Output); i >= 0 {
599			sc = c.screens[i]
600		}
601		sc.layers = append(sc.layers, l)
602	}
603	for id := range previous {
604		c.windows.layerGone(id)
605	}
606	c.pruneRetained()
607	c.clampCaptureKeyboard()
608	for _, sc := range c.screens {
609		sc.arrange()
610	}
611}
612
613func (c *Core) publish(ctx context.Context) error { return c.publishFrame(ctx, nil) }
614
615// publishFrame publishes the scenes. only is the screen whose page flip
616// moved the springs (nil: any, or none moved): only a flip's own screen gets
617// a fresh Seq for its animation, so another output keeps its Seq while it draws the same.
618func (c *Core) publishFrame(ctx context.Context, only *screen) error {
619	if c.inputEpochChanged() {
620		return errSecurityChanged
621	}
622	c.syncSecurity()
623	c.updatePositionHints(only)
624	c.hintValidate()
625	if c.security.Protected {
626		return c.publishProtected(ctx)
627	}
628	if c.switching() && c.switcherStale() {
629		// Another workspace came on screen under the switcher: it is over.
630		c.cancelSwitcher()
631	}
632	for _, sc := range c.screens {
633		// The overview's selection is provisional: it focuses nothing yet.
634		if !sc.mon.ov.open {
635			sc.mon.Current().settleLeft()
636		}
637		// A switcher's selection is provisional too.
638		if !sc.mon.previewing() {
639			sc.mon.Current().noteFocus()
640		}
641	}
642	if c.pgrab != nil && !c.pgrab.valid() {
643		if err := c.abortPointerGrab(ctx); err != nil {
644			return err
645		}
646	}
647	c.refreshShown()
648	c.captureExpire()
649	capture, err := c.captureEvaluate(ctx)
650	if err != nil {
651		return err
652	}
653	// The workspace a session renders off screen keeps its real layout for
654	// the configures, popups and CaptureScene of this publish.
655	c.captureTrack(capture)
656	if err := c.closeHiddenPopups(ctx); err != nil {
657		return err
658	}
659	// Clients learn outputs and scales before the configures sized for them.
660	if err := c.syncOutputs(ctx); err != nil {
661		return err
662	}
663	// Output layout must be known before an automatic terminal can map.
664	if err := c.spawnEmpty(ctx); err != nil {
665		return err
666	}
667	focus := c.keyboardFocus()
668	c.advanceFlash(only)
669	scenes := make([]ports.Scene, 0, len(c.screens))
670	// Before the first output (and after the last is unplugged) the
671	// placeholder's scene has no output name: no renderer draws it.
672	for i, sc := range c.screens {
673		scene, err := c.sceneFor(ctx, i, sc, only, capture)
674		if err != nil {
675			return err
676		}
677		scenes = append(scenes, scene)
678	}
679	c.configures.prune()
680	for i, sc := range c.screens {
681		sc.last = scenes[i]
682	}
683	// A workspace switch can hide the window under the pointer; it must not get
684	// clicks. The next motion re-runs hit-testing.
685	if c.pointer != 0 && !c.visible(c.pointer) {
686		c.pointer = 0
687		if c.grab == 0 {
688			if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil {
689				return err
690			}
691		}
692	}
693	if err := c.syncFocus(ctx, focus); err != nil {
694		return err
695	}
696	if err := c.updateInhibit(ctx); err != nil {
697		return err
698	}
699	c.resolveConstraint()
700	if c.inputEpochChanged() {
701		return errSecurityChanged
702	}
703	if c.syncSecurity() {
704		return c.publishFrame(ctx, only)
705	}
706	latest(c.ch.Scenes, scenes)
707	c.publishState()
708	c.publishWorkspaces()
709	c.armCaptureTimer()
710	// A running slide moves on the next flip, or on the fallback timer.
711	if c.animating() {
712		if c.frameC == nil {
713			c.armFrame()
714		}
715	} else {
716		c.stopFrame()
717	}
718	return nil
719}
720
721// step moves the running slides of only (nil: every screen) to now and
722// publishes the frame.
723func (c *Core) step(ctx context.Context, only *screen) error {
724	// The fallback timer guards every animating output: a flip of one keeps
725	// it running while another still animates, or that one could starve.
726	if only == nil || !c.animatingOther(only) {
727		c.stopFrame()
728	}
729	c.animate(c.now(), only)
730	return c.slidFrame(ctx, false, only)
731}
732
733// animatingOther reports whether a spring runs on a screen
734// other than only.
735func (c *Core) animatingOther(only *screen) bool {
736	for _, sc := range c.screens {
737		if sc != only && (sc.springing() || c.flashing(sc) || c.positionHintFading(sc)) {
738			return true
739		}
740	}
741	return false
742}
743
744// slid publishes a moved view; shown reports another workspace came on
745// screen.
746func (c *Core) slid(ctx context.Context, shown bool) error {
747	return c.slidFrame(ctx, shown, nil)
748}
749
750// slidFrame is slid for the page flip of only (nil: any screen).
751func (c *Core) slidFrame(ctx context.Context, shown bool, only *screen) error {
752	if err := c.workspaceVisible(ctx, shown); err != nil {
753		return err
754	}
755	if err := c.publishFrame(ctx, only); err != nil {
756		return err
757	}
758	return c.rehit(ctx)
759}
760
761// rehit points the pointer at what now lies under a still cursor, as
762// windows slide under it. A held button keeps its grab.
763func (c *Core) rehit(ctx context.Context) error {
764	if c.dataDrag != nil {
765		// The drag target follows the layout too.
766		return c.dataDragRetarget(ctx, c.motionTime)
767	}
768	if c.grab != 0 || c.pointerBusy() {
769		return nil
770	}
771	id, x, y := c.hit(c.cursorX, c.cursorY)
772	if id != c.pointer {
773		c.pointer, c.pointerAt = id, [2]float64{x, y}
774		return c.pointerFocus(ctx, ports.PointerFocus{ID: id, X: x, Y: y})
775	}
776	if id == 0 || c.pointerAt == [2]float64{x, y} {
777		return nil
778	}
779	c.pointerAt = [2]float64{x, y}
780	// The same window moved under the cursor: it moves in the window. No
781	// relative motion: the device did not move. The input clock is the
782	// device's; the last motion's time is the closest core knows.
783	return c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, Time: c.motionTime})
784}
785
786// updateInhibit makes the shortcuts inhibitor of the keyboard focus the
787// active one and tells wayland when it changes.
788func (c *Core) updateInhibit(ctx context.Context) error {
789	if c.security.Protected {
790		return nil
791	}
792	f := c.keyboardFocus()
793	release, activate, changed := c.keyboard.inhibit(f, c.windows.lookup(f).inhibitShortcuts)
794	if !changed {
795		return nil
796	}
797	if release != 0 {
798		if err := c.command(ctx, ports.ShortcutsInhibitState{Window: release}); err != nil {
799			return err
800		}
801	}
802	if activate != 0 {
803		return c.command(ctx, ports.ShortcutsInhibitState{Window: activate, Active: true})
804	}
805	return nil
806}
807
808// resolveConstraint turns the active constraint into global coordinates
809// and sends it to input when it changed. A hidden window holds nothing.
810func (c *Core) resolveConstraint() {
811	var g ports.PointerConstraint
812	if full, vis, ok := c.shownClient(c.constrained.ID); ok {
813		r := full
814		if w := c.constrained.Rect; w.W > 0 && w.H > 0 {
815			// The region is window-local: placed from the full client origin,
816			// clipped to the window, then to what the viewport shows.
817			x0, y0 := max(full.X, full.X+w.X), max(full.Y, full.Y+w.Y)
818			x1, y1 := min(full.X+full.W, full.X+w.X+w.W), min(full.Y+full.H, full.Y+w.Y+w.H)
819			if x1 > x0 && y1 > y0 {
820				r = Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0}
821			}
822		}
823		r = intersect(r, vis)
824		if r.W > 0 && r.H > 0 {
825			g = ports.PointerConstraint{Mode: c.constrained.Mode, Rect: r}
826		} else {
827			// The region lies outside the viewport: hold the visible client.
828			g = ports.PointerConstraint{Mode: c.constrained.Mode, Rect: vis}
829		}
830	}
831	// The cursor follows core; input resyncs to it only on a change.
832	if g.Mode == c.constraint.Mode && g.Rect == c.constraint.Rect {
833		return
834	}
835	// A layout change must not leave a locked cursor off its window.
836	c.cursorX, c.cursorY = g.Clamp(c.cursorX, c.cursorY)
837	g.X, g.Y = c.cursorX, c.cursorY
838	c.constraint = g
839	if c.ch.Constraints != nil {
840		latest(c.ch.Constraints, g)
841	}
842}
843
844// intersect is the overlap of a and b, empty (zero size) when disjoint.
845func intersect(a, b Rect) Rect {
846	x0, y0 := max(a.X, b.X), max(a.Y, b.Y)
847	x1, y1 := min(a.X+a.W, b.X+b.W), min(a.Y+a.H, b.Y+b.H)
848	if x1 <= x0 || y1 <= y0 {
849		return Rect{}
850	}
851	return Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0}
852}
853
854// shownClient is the global logical client rectangle of a window drawn on
855// its output, whole (window-local coordinates start at its origin), and the
856// part of it inside the workspace viewport, which alone takes the pointer.
857// ok is false when it is hidden, a preview or entirely outside the viewport.
858func (c *Core) shownClient(id WindowID) (full, visible Rect, ok bool) {
859	if id == 0 {
860		return Rect{}, Rect{}, false
861	}
862	s, _ := c.screenOf(id)
863	if s == nil {
864		return Rect{}, Rect{}, false
865	}
866	for _, p := range s.shownLayout() {
867		if p.ID != id || p.Hidden || p.Preview > 0 {
868			continue
869		}
870		r := c.clientRect(p)
871		full = Rect{X: r.X + s.x, Y: r.Y + s.y, W: r.W, H: r.H}
872		visible = intersect(r, s.mon.viewport().area(p))
873		visible.X, visible.Y = visible.X+s.x, visible.Y+s.y
874		if visible.W <= 0 || visible.H <= 0 {
875			return Rect{}, Rect{}, false
876		}
877		return full, visible, true
878	}
879	return Rect{}, Rect{}, false
880}
881
882// shownClientRect is the visible part of shownClient.
883func (c *Core) shownClientRect(id WindowID) (Rect, bool) {
884	_, vis, ok := c.shownClient(id)
885	return vis, ok
886}
887
888// warpPointer moves the cursor to a window-local point of the window under
889// it (wp_pointer_warp_v1). Input takes the new position, and the window
890// gets the motion; the active constraint still bounds the point.
891func (c *Core) warpPointer(ctx context.Context, v ports.PointerWarp) error {
892	if v.ID == 0 || v.ID != c.pointer || c.grab != 0 && c.grab != v.ID {
893		return nil
894	}
895	full, vis, ok := c.shownClient(v.ID)
896	if !ok || v.X < 0 || v.Y < 0 || v.X >= float64(full.W) || v.Y >= float64(full.H) {
897		return nil
898	}
899	// Local to the whole client, and refused outside the viewport.
900	gx, gy := float64(full.X)+v.X, float64(full.Y)+v.Y
901	if gx < float64(vis.X) || gy < float64(vis.Y) || gx >= float64(vis.X+vis.W) || gy >= float64(vis.Y+vis.H) {
902		return nil
903	}
904	c.cursorX, c.cursorY = c.constraint.Clamp(gx, gy)
905	if c.ch.Constraints != nil {
906		g := c.constraint
907		g.X, g.Y, g.Warp = c.cursorX, c.cursorY, true
908		latest(c.ch.Constraints, g)
909	}
910	// A locked pointer moved: the client knows where it put it.
911	if c.constraint.Mode == ports.ConstraintLock {
912		return nil
913	}
914	return c.rehit(ctx)
915}
916
917// syncOutputs sends the output layout when it changed; input learns the
918// geometry only when the outputs themselves moved.
919func (c *Core) syncOutputs(ctx context.Context) error {
920	// Built in the scratch every publish, copied only when sent.
921	c.scratch.layout = c.layoutInto(c.scratch.layout)
922	v := ports.SetOutputs{Outputs: c.scratch.layout, Focused: c.cur().name(), Off: c.offOutputs()}
923	if sameOutputs(v, c.sent.outputs) {
924		return nil
925	}
926	v.Outputs = append(make(ports.Layout, 0, len(v.Outputs)), v.Outputs...)
927	if err := c.command(ctx, v); err != nil {
928		return err
929	}
930	if c.ch.Layouts != nil && !slices.Equal(v.Outputs, c.sent.outputs.Outputs) {
931		latest(c.ch.Layouts, v.Outputs)
932	}
933	c.sent.outputs = v
934	return nil
935}
936
937// syncFocus tells wayland the keyboard focus when it changed.
938func (c *Core) syncFocus(ctx context.Context, focus WindowID) error {
939	if focus == c.keyboard.sent {
940		return nil
941	}
942	if err := c.command(ctx, ports.FocusWindow{ID: focus}); err != nil {
943		return err
944	}
945	c.keyboard.sent = focus
946	return nil
947}
948
949// latest replaces any unread value: only the owner sends and drains;
950// consumers only receive.
951func latest[T any](ch chan T, v T) {
952	select {
953	case ch <- v:
954	default:
955		select {
956		case <-ch:
957		default:
958		}
959		select {
960		case ch <- v:
961		default:
962		}
963	}
964}
965
966func sameOutputs(a, b ports.SetOutputs) bool {
967	return a.Focused == b.Focused && slices.Equal(a.Outputs, b.Outputs) && slices.Equal(a.Off, b.Off)
968}
969
970// allLayers lists the layer surfaces of every output.
971func (c *Core) allLayers() []ports.LayerSurface {
972	var all []ports.LayerSurface
973	for _, sc := range c.screens {
974		all = append(all, sc.layers...)
975	}
976	return all
977}
978
979func (c *Core) acceptsInput(id WindowID, x, y float64) bool {
980	return c.windows.acceptsInput(id, x, y)
981}
982
983// hit returns the surface under the global logical point and the point in
984// its surface coordinates: popups, then overlay and top layers, then
985// windows (last visible placement is topmost), then bottom and
986// background layers.
987func (c *Core) hit(x, y float64) (WindowID, float64, float64) {
988	if c.security.Protected {
989		return c.lockHit(x, y)
990	}
991	i := c.screenAt(x, y)
992	if i < 0 {
993		return 0, 0, 0
994	}
995	sc := c.screens[i]
996	lx, ly := x-float64(sc.x), y-float64(sc.y)
997	// Layer popups are over everything; window popups are over the windows
998	// only, under the top and overlay layers (as drawn).
999	if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 {
1000		return id, px, py
1001	}
1002	if id, px, py := c.layerAt(sc, lx, ly, true); id != 0 {
1003		return id, px, py
1004	}
1005	if id, px, py := c.popupAt(sc, lx, ly, false); id != 0 {
1006		return id, px, py
1007	}
1008	if sc.mon.previewing() {
1009		// Previews and the layers under them take no input: a click
1010		// picks a preview (overviewClick, switcherClick).
1011		return 0, 0, 0
1012	}
1013	if !sc.mon.frameHas(lx, ly) {
1014		return c.layerAt(sc, lx, ly, false)
1015	}
1016	var id WindowID
1017	var sx, sy float64
1018	view := sc.mon.viewport()
1019	for _, p := range sc.shownLayout() {
1020		r := c.clientRect(p)
1021		// A peek is clickable wherever it shows, border included: it may
1022		// be narrower than its border. The point is clamped to its client.
1023		box, lx, ly := r, lx, ly
1024		if p.Peek {
1025			box = p.Rect
1026		}
1027		// Only what is drawn takes the pointer: a tile not over a panel.
1028		box = intersect(box, view.area(p))
1029		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) {
1030			if p.Peek {
1031				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))
1032			}
1033			if c.acceptsInput(p.ID, lx-float64(r.X), ly-float64(r.Y)) {
1034				// Layout is bottom to top, including fullscreen and floats.
1035				id, sx, sy = p.ID, lx-float64(r.X), ly-float64(r.Y)
1036			}
1037		}
1038	}
1039	if id == 0 {
1040		return c.layerAt(sc, lx, ly, false)
1041	}
1042	return id, sx, sy
1043}
1044
1045// Ping returns once Run handled a probe, or once ctx ends: the watchdog
1046// uses it to detect a stuck owner loop.
1047func (c *Core) Ping(ctx context.Context) {
1048	ack := make(chan struct{})
1049	select {
1050	case c.ping <- ack:
1051	case <-ctx.Done():
1052		return
1053	}
1054	select {
1055	case <-ack:
1056	case <-ctx.Done():
1057	}
1058}
1059
1060// CommandStats returns the command outbox counters since the last call.
1061// Any goroutine may call it.
1062func (c *Core) CommandStats() CommandStats { return c.out.takeStats() }
1063
1064func (c *Core) Run(ctx context.Context) error {
1065	// The sender stops with Run, not only with ctx: Run also returns on
1066	// quit and on errors. Run waits for it so nothing sends after Run.
1067	sendCtx, stopSender := context.WithCancel(ctx)
1068	senderDone := make(chan struct{})
1069	if c.ch.Commands != nil {
1070		c.out.start()
1071	}
1072	go func() {
1073		defer close(senderDone)
1074		if c.ch.Commands != nil {
1075			c.out.run(sendCtx, c.ch.Commands)
1076		}
1077	}()
1078	defer func() {
1079		stopSender()
1080		<-senderDone
1081	}()
1082	c.syncSecurity()
1083	defer c.stopFrame()
1084	defer c.stopCaptureTimer()
1085	defer c.stopFlashTimer()
1086	defer c.stopPositionHintTimer()
1087	defer c.cancelSwitcher()
1088	defer c.stopRepeat()
1089	// Startup commands run once per session. Full launcher queues retain a
1090	// bounded remainder, selectable alongside owner events without polling.
1091	c.startup = make([][]string, len(c.cfg.Startup))
1092	for i, argv := range c.cfg.Startup {
1093		c.startup[i] = slices.Clone(argv)
1094	}
1095	c.spawnStartup(ctx)
1096	if c.spawnSlots(ctx, false) != nil {
1097		return nil
1098	}
1099	c.publishState()
1100	c.publishWorkspaces()
1101	for {
1102		if c.securityCheckpoint(ctx) != nil {
1103			return nil
1104		}
1105		startupC, startupRequest := c.pendingStartup()
1106		select {
1107		case <-ctx.Done():
1108			return nil
1109		case startupC <- startupRequest:
1110			// Successful transfer consumes startup once. Only the receiver can
1111			// decide execution admission; a later gate read cannot infer it.
1112			c.startup = c.startup[1:]
1113			continue
1114		case ev, ok := <-c.ch.Client:
1115			if !ok {
1116				c.ch.Client = nil
1117				continue
1118			}
1119			if c.securityCheckpoint(ctx) != nil {
1120				return nil
1121			}
1122			if c.blockProtected(ev) {
1123				continue
1124			}
1125			publish, stop := c.clientEvent(ctx, ev)
1126			if stop {
1127				return nil
1128			}
1129			if publish {
1130				if c.publish(ctx) != nil {
1131					return nil
1132				}
1133				if c.dataDrag != nil && dataDragHitChanged(ev) && c.dataDragRetarget(ctx, c.motionTime) != nil {
1134					return nil
1135				}
1136			}
1137		case f, ok := <-c.ch.Frames:
1138			if c.securityCheckpoint(ctx) != nil {
1139				return nil
1140			}
1141			if !ok {
1142				c.ch.Frames = nil
1143				continue
1144			}
1145			if i := c.screenIndex(f.Output); i >= 0 && c.sliding(f.Output) && c.step(ctx, c.screens[i]) != nil {
1146				return nil
1147			}
1148			continue
1149		case ack := <-c.ping:
1150			close(ack)
1151			continue
1152		case <-c.frameC:
1153			if c.securityCheckpoint(ctx) != nil {
1154				return nil
1155			}
1156			c.frameC = nil
1157			if c.step(ctx, nil) != nil {
1158				return nil
1159			}
1160			continue
1161		case <-c.positionHintTimer.timerC:
1162			c.positionHintTimer.timerC = nil
1163			if c.securityCheckpoint(ctx) != nil || c.publish(ctx) != nil {
1164				return nil
1165			}
1166			continue
1167		case <-c.flash.timerC:
1168			if c.securityCheckpoint(ctx) != nil {
1169				return nil
1170			}
1171			if c.flashTick() && c.publish(ctx) != nil {
1172				return nil
1173			}
1174			continue
1175		case <-c.switcher.timerC:
1176			if c.securityCheckpoint(ctx) != nil {
1177				return nil
1178			}
1179			if c.switcherTick() && (c.publish(ctx) != nil || c.rehit(ctx) != nil) {
1180				return nil
1181			}
1182			continue
1183		case <-c.repeatC:
1184			if c.securityCheckpoint(ctx) != nil {
1185				return nil
1186			}
1187			if !c.repeatLive() {
1188				continue
1189			}
1190			// A held key is activity, as its press was: idle timers
1191			// must not fire while it repeats.
1192			if err := c.userActivity(ctx); err != nil {
1193				return nil
1194			}
1195			if c.securityCheckpoint(ctx) != nil {
1196				return nil
1197			}
1198			if err := c.repeatTick(ctx); err != nil {
1199				if errors.Is(err, errSecurityChanged) {
1200					continue
1201				}
1202				return err
1203			}
1204			continue
1205		case <-c.capt.timerC:
1206			c.capt.timerC, c.capt.timerStop = nil, nil
1207			if c.captureFlashTick() && c.publish(ctx) != nil {
1208				return nil
1209			}
1210			continue
1211		case res, ok := <-c.ch.Hints:
1212			if !ok {
1213				c.ch.Hints = nil
1214				continue
1215			}
1216			if c.securityCheckpoint(ctx) != nil {
1217				return nil
1218			}
1219			if c.hintResult(res) && c.publish(ctx) != nil {
1220				return nil
1221			}
1222		case ev, ok := <-c.ch.Output:
1223			if !ok {
1224				c.ch.Output = nil
1225				continue
1226			}
1227			if c.securityCheckpoint(ctx) != nil {
1228				return nil
1229			}
1230			switch v := ev.(type) {
1231			case ports.OutputAdded:
1232				c.addScreen(v.Info)
1233				if c.layerChanged {
1234					c.setLayers(c.allLayers())
1235				}
1236				c.cursorX, c.cursorY = c.clampPointer(c.cursorX, c.cursorY, c.cursorX, c.cursorY)
1237			case ports.OutputRemoved:
1238				// A drop target may be on the output going away.
1239				c.abortDrag()
1240				c.removeScreen(v.Name)
1241				if c.layerChanged {
1242					c.setLayers(c.allLayers())
1243				}
1244				c.cursorX, c.cursorY = c.clampPointer(c.cursorX, c.cursorY, c.cursorX, c.cursorY)
1245			}
1246			if c.workspaceVisible(ctx, false) != nil {
1247				return nil
1248			}
1249			if err := c.publish(ctx); err != nil {
1250				return nil
1251			}
1252		case ev, ok := <-c.ch.Config:
1253			if !ok {
1254				c.ch.Config = nil
1255				continue
1256			}
1257			if c.securityCheckpoint(ctx) != nil {
1258				return nil
1259			}
1260			// A new keymap quarantines held keys: their release never
1261			// comes, so no repeat may outlive a reload.
1262			c.stopRepeat()
1263			if err := c.apply(ev.Config); err != nil {
1264				select {
1265				case c.ch.ConfigErrors <- err:
1266				default:
1267				}
1268				continue
1269			}
1270			if !c.animOn() {
1271				c.stopAnimations()
1272			}
1273			// A rotation can shrink the layout under a still pointer.
1274			c.cursorX, c.cursorY = c.clampPointer(c.cursorX, c.cursorY, c.cursorX, c.cursorY)
1275			if c.workspaceVisible(ctx, false) != nil {
1276				return nil
1277			}
1278			if err := c.publish(ctx); err != nil {
1279				return nil
1280			}
1281		case ev, ok := <-c.ch.Input:
1282			if !ok {
1283				c.ch.Input = nil
1284				continue
1285			}
1286			if c.securityCheckpoint(ctx) != nil {
1287				return nil
1288			}
1289			// Even stale input is activity, but never becomes client input.
1290			if err := c.userActivity(ctx); err != nil {
1291				return nil
1292			}
1293			// Commands can backpressure; recheck after activity before admits/binds.
1294			if c.securityCheckpoint(ctx) != nil {
1295				return nil
1296			}
1297			var admitted bool
1298			ev, admitted = c.admitInput(ev)
1299			if !admitted {
1300				continue
1301			}
1302			if err := c.handleInput(ctx, ev); err != nil {
1303				if errors.Is(err, errSecurityChanged) {
1304					continue
1305				}
1306				return err
1307			}
1308		}
1309	}
1310}
1311
1312// workspaceVisible settles guests and refills slots on a show. Publication
1313// subsequently decides whether visible empty workspaces need terminals.
1314func (c *Core) workspaceVisible(ctx context.Context, shown bool) error {
1315	c.releaseSlots()
1316	c.settleGuests()
1317	return c.spawnSlots(ctx, shown)
1318}
1319
1320// spawnEmpty requests terminals for visible empty workspaces. publish calls
1321// it after the output layout and before scenes, so a terminal never maps
1322// before wayland knows its output.
1323func (c *Core) spawnEmpty(ctx context.Context) error {
1324	if c.protectionRequested() {
1325		return nil
1326	}
1327	first := c.firstTerminalResolved
1328	terms := c.fillEmpty()
1329	issued, err := c.dispatch(ctx, terms)
1330	if len(terms) > 0 && issued == 0 {
1331		c.firstTerminalResolved = first
1332	}
1333	return err
1334}
1335
1336// dispatch sends reqs to the launcher and reports how many it issued.
1337// From the first request that is not issued (context ended, or the gate
1338// suppressed the send), the rest are not sent and their claims are cancelled.
1339func (c *Core) dispatch(ctx context.Context, reqs []ports.SpawnRequest) (issued int, err error) {
1340	for i, req := range reqs {
1341		err = ctx.Err()
1342		if err != nil || !c.trySpawn(ctx, req) {
1343			for _, suppressed := range reqs[i:] {
1344				c.cancelClaim(suppressed)
1345			}
1346			return i, err
1347		}
1348	}
1349	return len(reqs), err
1350}
1351
1352// activate brings a window forward for a valid xdg-activation token. The
1353// token proves a user action in the requesting client (a click on a link,
1354// a notification), so unlike other client requests it may move the user
1355// (the exception to the ADR 011 golden rule).
1356func (c *Core) activate(ctx context.Context, id WindowID) error {
1357	s, w := c.screenOf(id)
1358	if s == nil || c.popups[id] != nil {
1359		return nil
1360	}
1361	before := c.cur().mon.Current()
1362	if w != s.mon.Current() {
1363		s.mon.show(w)
1364	}
1365	w.Activate(id)
1366	c.focusScreen = c.screenIndex(s.name())
1367	return c.workspaceVisible(ctx, c.cur().mon.Current() != before)
1368}
1369
1370// heldKey names a key for press tracking: by physical key, since Shift
1371// may be released before the key.
1372func heldKey(key ports.KeyEvent) string {
1373	if key.Keycode != 0 {
1374		return "#" + strconv.FormatUint(uint64(key.Keycode), 10)
1375	}
1376	return keyName(key.Keysym)
1377}

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