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

Keyboard shortcuts

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

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

Branches 23

Tags 10

↑↓ moveenter openesc close