feat/hints a471fa4

core.go RawHistoryFolder

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

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