refactor/kvconf 3462c76

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

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