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

Keyboard shortcuts

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

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

Branches 26

Tags 10

↑↓ moveenter openesc close