v0.8.0 446d1e3

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

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