core.go RawHistoryFolder

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

Keyboard shortcuts

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

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

Branches 25

Tags 10

↑↓ moveenter openesc close