v0.2.0 13c6a90

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

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