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, captured: capture != nil && capture.window == p.ID}
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 t.captured && p.Hidden {
590				// A captured hidden window is sized like its capture.
591				t.client = capture.windowSz
592			} else if p.Preview > 0 && !p.Hidden {
593				if rp, ok := real[p.ID]; ok && !rp.Hidden {
594					t.realTiled = !rp.Floating
595					t.client = c.clientRect(rp)
596				}
597			}
598			cp, t := c.captureConfigure(sc, p, t)
599			if v, send := c.configures.nextWithCapture(p, t, cp); send {
600				if err := c.command(ctx, v); err != nil {
601					return err
602				}
603				c.configures.mark(v)
604			}
605		}
606		scene.Windows = append(scene.Windows, c.scenePopups(sc)...)
607		scene.CaptureIndicators = c.captureIndicators(sc)
608		if scene.Capture = c.captureSceneFor(sc, capture); scene.Capture != nil {
609			switch sc {
610			case capture.hiddenScr:
611				scene.CaptureScene = c.captureScene(scene.Seq)
612			case capture.windowScr:
613				scene.CaptureScene = c.captureWindowScene(capture, scene.Seq)
614			}
615		}
616		scenes = append(scenes, scene)
617	}
618	c.configures.prune()
619	// A pulse nothing shows (fullscreen, overview) asks for no frames.
620	if c.pulse.drawable = drawable; !drawable {
621		c.pulse.id = 0
622	}
623	// A workspace switch can hide the window under the pointer; it must not get
624	// clicks. The next motion re-runs hit-testing.
625	if c.pointer != 0 && !c.visible(c.pointer) {
626		c.pointer = 0
627		if c.grab == 0 {
628			if err := c.command(ctx, ports.PointerFocus{}); err != nil {
629				return err
630			}
631		}
632	}
633	if err := c.syncFocus(ctx, focus); err != nil {
634		return err
635	}
636	if err := c.updateInhibit(ctx); err != nil {
637		return err
638	}
639	c.resolveConstraint()
640	if c.inputEpochChanged() {
641		return errSecurityChanged
642	}
643	if c.syncSecurity() {
644		return c.publish(ctx)
645	}
646	latest(c.ch.Scenes, scenes)
647	c.publishState()
648	c.publishWorkspaces()
649	c.armCaptureTimer()
650	// A running slide moves on the next flip, or on the fallback timer.
651	if c.animating() {
652		if c.frameC == nil {
653			c.armFrame()
654		}
655	} else {
656		c.stopFrame()
657	}
658	return nil
659}
660
661// step moves the running slides to now and publishes the frame.
662func (c *Core) step(ctx context.Context) error {
663	c.stopFrame()
664	c.animate(c.now())
665	return c.slid(ctx, false)
666}
667
668// slid publishes a moved view; shown reports another workspace came on
669// screen.
670func (c *Core) slid(ctx context.Context, shown bool) error {
671	if err := c.workspaceVisible(ctx, shown); err != nil {
672		return err
673	}
674	if err := c.publish(ctx); err != nil {
675		return err
676	}
677	return c.rehit(ctx)
678}
679
680// rehit points the pointer at what now lies under a still cursor, as
681// windows slide under it. A held button keeps its grab.
682func (c *Core) rehit(ctx context.Context) error {
683	if c.grab != 0 || c.drag != nil {
684		return nil
685	}
686	id, x, y := c.hit(c.cursorX, c.cursorY)
687	if id != c.pointer {
688		c.pointer, c.pointerAt = id, [2]float64{x, y}
689		return c.command(ctx, ports.PointerFocus{ID: id, X: x, Y: y})
690	}
691	if id == 0 || c.pointerAt == [2]float64{x, y} {
692		return nil
693	}
694	c.pointerAt = [2]float64{x, y}
695	// The same window moved under the cursor: it moves in the window. No
696	// relative motion: the device did not move. The input clock is the
697	// device's; the last motion's time is the closest core knows.
698	return c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, TimeMsec: c.motionMsec})
699}
700
701// updateInhibit makes the shortcuts inhibitor of the keyboard focus the
702// active one and tells wayland when it changes.
703func (c *Core) updateInhibit(ctx context.Context) error {
704	if c.security.Protected {
705		return nil
706	}
707	f := c.keyboardFocus()
708	release, activate, changed := c.keyboard.inhibit(f, c.windows.lookup(f).inhibitShortcuts)
709	if !changed {
710		return nil
711	}
712	if release != 0 {
713		if err := c.command(ctx, ports.ShortcutsInhibitState{Window: release}); err != nil {
714			return err
715		}
716	}
717	if activate != 0 {
718		return c.command(ctx, ports.ShortcutsInhibitState{Window: activate, Active: true})
719	}
720	return nil
721}
722
723// resolveConstraint turns the active constraint into global coordinates
724// and sends it to input when it changed. A hidden window holds nothing.
725func (c *Core) resolveConstraint() {
726	var g ports.PointerConstraint
727	if full, vis, ok := c.shownClient(c.constrained.ID); ok {
728		r := full
729		if w := c.constrained.Rect; w.W > 0 && w.H > 0 {
730			// The region is window-local: placed from the full client origin,
731			// clipped to the window, then to what the viewport shows.
732			x0, y0 := max(full.X, full.X+w.X), max(full.Y, full.Y+w.Y)
733			x1, y1 := min(full.X+full.W, full.X+w.X+w.W), min(full.Y+full.H, full.Y+w.Y+w.H)
734			if x1 > x0 && y1 > y0 {
735				r = Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0}
736			}
737		}
738		r = intersect(r, vis)
739		if r.W > 0 && r.H > 0 {
740			g = ports.PointerConstraint{Mode: c.constrained.Mode, Rect: r}
741		} else {
742			// The region lies outside the viewport: hold the visible client.
743			g = ports.PointerConstraint{Mode: c.constrained.Mode, Rect: vis}
744		}
745	}
746	// The cursor follows core; input resyncs to it only on a change.
747	if g.Mode == c.constraint.Mode && g.Rect == c.constraint.Rect {
748		return
749	}
750	// A layout change must not leave a locked cursor off its window.
751	c.cursorX, c.cursorY = g.Clamp(c.cursorX, c.cursorY)
752	g.X, g.Y = c.cursorX, c.cursorY
753	c.constraint = g
754	if c.ch.Constraints != nil {
755		latest(c.ch.Constraints, g)
756	}
757}
758
759// intersect is the overlap of a and b, empty (zero size) when disjoint.
760func intersect(a, b Rect) Rect {
761	x0, y0 := max(a.X, b.X), max(a.Y, b.Y)
762	x1, y1 := min(a.X+a.W, b.X+b.W), min(a.Y+a.H, b.Y+b.H)
763	if x1 <= x0 || y1 <= y0 {
764		return Rect{}
765	}
766	return Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0}
767}
768
769// shownClient is the global logical client rectangle of a window drawn on
770// its output, whole (window-local coordinates start at its origin), and the
771// part of it inside the workspace viewport, which alone takes the pointer.
772// ok is false when it is hidden, a preview or entirely outside the viewport.
773func (c *Core) shownClient(id WindowID) (full, visible Rect, ok bool) {
774	if id == 0 {
775		return Rect{}, Rect{}, false
776	}
777	s, _ := c.screenOf(id)
778	if s == nil {
779		return Rect{}, Rect{}, false
780	}
781	for _, p := range s.mon.Layout() {
782		if p.ID != id || p.Hidden || p.Preview > 0 {
783			continue
784		}
785		r := c.clientRect(p)
786		full = Rect{X: r.X + s.x, Y: r.Y + s.y, W: r.W, H: r.H}
787		f := s.mon.Frame()
788		visible = intersect(r, f)
789		visible.X, visible.Y = visible.X+s.x, visible.Y+s.y
790		if visible.W <= 0 || visible.H <= 0 {
791			return Rect{}, Rect{}, false
792		}
793		return full, visible, true
794	}
795	return Rect{}, Rect{}, false
796}
797
798// shownClientRect is the visible part of shownClient.
799func (c *Core) shownClientRect(id WindowID) (Rect, bool) {
800	_, vis, ok := c.shownClient(id)
801	return vis, ok
802}
803
804// warpPointer moves the cursor to a window-local point of the window under
805// it (wp_pointer_warp_v1). Input takes the new position, and the window
806// gets the motion; the active constraint still bounds the point.
807func (c *Core) warpPointer(ctx context.Context, v ports.PointerWarp) error {
808	if v.ID == 0 || v.ID != c.pointer || c.grab != 0 && c.grab != v.ID {
809		return nil
810	}
811	full, vis, ok := c.shownClient(v.ID)
812	if !ok || v.X < 0 || v.Y < 0 || v.X >= float64(full.W) || v.Y >= float64(full.H) {
813		return nil
814	}
815	// Local to the whole client, and refused outside the viewport.
816	gx, gy := float64(full.X)+v.X, float64(full.Y)+v.Y
817	if gx < float64(vis.X) || gy < float64(vis.Y) || gx >= float64(vis.X+vis.W) || gy >= float64(vis.Y+vis.H) {
818		return nil
819	}
820	c.cursorX, c.cursorY = c.constraint.Clamp(gx, gy)
821	if c.ch.Constraints != nil {
822		g := c.constraint
823		g.X, g.Y, g.Warp = c.cursorX, c.cursorY, true
824		latest(c.ch.Constraints, g)
825	}
826	// A locked pointer moved: the client knows where it put it.
827	if c.constraint.Mode == ports.ConstraintLock {
828		return nil
829	}
830	return c.rehit(ctx)
831}
832
833// syncOutputs sends the output layout when it changed; input learns the
834// geometry only when the outputs themselves moved.
835func (c *Core) syncOutputs(ctx context.Context) error {
836	v := ports.SetOutputs{Outputs: c.layout(), Focused: c.cur().name(), Off: c.offOutputs()}
837	if sameOutputs(v, c.sentOutputs) {
838		return nil
839	}
840	if err := c.command(ctx, v); err != nil {
841		return err
842	}
843	if c.ch.Layouts != nil && !slices.Equal(v.Outputs, c.sentOutputs.Outputs) {
844		latest(c.ch.Layouts, v.Outputs)
845	}
846	c.sentOutputs = v
847	return nil
848}
849
850// syncFocus tells wayland the keyboard focus when it changed.
851func (c *Core) syncFocus(ctx context.Context, focus WindowID) error {
852	if focus == c.keyboard.sent {
853		return nil
854	}
855	if err := c.command(ctx, ports.FocusWindow{ID: focus}); err != nil {
856		return err
857	}
858	c.keyboard.sent = focus
859	return nil
860}
861
862// latest replaces any unread value: only the owner sends and drains;
863// consumers only receive.
864func latest[T any](ch chan T, v T) {
865	select {
866	case ch <- v:
867	default:
868		select {
869		case <-ch:
870		default:
871		}
872		select {
873		case ch <- v:
874		default:
875		}
876	}
877}
878
879func sameOutputs(a, b ports.SetOutputs) bool {
880	return a.Focused == b.Focused && slices.Equal(a.Outputs, b.Outputs) && slices.Equal(a.Off, b.Off)
881}
882
883// allLayers lists the layer surfaces of every output.
884func (c *Core) allLayers() []ports.LayerSurface {
885	var all []ports.LayerSurface
886	for _, sc := range c.screens {
887		all = append(all, sc.layers...)
888	}
889	return all
890}
891
892func (c *Core) acceptsInput(id WindowID, x, y float64) bool {
893	return c.windows.acceptsInput(id, x, y)
894}
895
896// hit returns the surface under the global logical point and the point in
897// its surface coordinates: popups, then overlay and top layers, then
898// windows (last visible placement is topmost), then bottom and
899// background layers.
900func (c *Core) hit(x, y float64) (WindowID, float64, float64) {
901	if c.security.Protected {
902		return c.lockHit(x, y)
903	}
904	o, ok := c.layout().At(x, y)
905	if !ok {
906		return 0, 0, 0
907	}
908	lx, ly := x-float64(o.X), y-float64(o.Y)
909	sc := c.screens[c.screenIndex(o.Info.Name)]
910	// Layer popups are over everything; window popups are over the windows
911	// only, under the top and overlay layers (as drawn).
912	if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 {
913		return id, px, py
914	}
915	if id, px, py := c.layerAt(sc, lx, ly, true); id != 0 {
916		return id, px, py
917	}
918	if id, px, py := c.popupAt(sc, lx, ly, false); id != 0 {
919		return id, px, py
920	}
921	if sc.mon.ov.open {
922		// Previews and the layers under them take no input: a click
923		// picks a preview (overviewClick).
924		return 0, 0, 0
925	}
926	if !sc.mon.frameHas(lx, ly) {
927		return c.layerAt(sc, lx, ly, false)
928	}
929	var id WindowID
930	var sx, sy float64
931	for _, p := range sc.mon.Layout() {
932		r := c.clientRect(p)
933		// A peek is clickable wherever it shows, border included: it may
934		// be narrower than its border. The point is clamped to its client.
935		box, lx, ly := r, lx, ly
936		if p.Peek {
937			box = p.Rect
938		}
939		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) {
940			if p.Peek {
941				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))
942			}
943			if c.acceptsInput(p.ID, lx-float64(r.X), ly-float64(r.Y)) {
944				// Layout is bottom to top, including fullscreen and floats.
945				id, sx, sy = p.ID, lx-float64(r.X), ly-float64(r.Y)
946			}
947		}
948	}
949	if id == 0 {
950		return c.layerAt(sc, lx, ly, false)
951	}
952	return id, sx, sy
953}
954
955func (c *Core) Run(ctx context.Context) error {
956	c.syncSecurity()
957	defer c.stopFrame()
958	defer c.stopCaptureTimer()
959	defer c.stopPulseTimer()
960	// Startup commands run once per session. Full launcher queues retain a
961	// bounded remainder, selectable alongside owner events without polling.
962	c.startup = make([][]string, len(c.cfg.Startup))
963	for i, argv := range c.cfg.Startup {
964		c.startup[i] = slices.Clone(argv)
965	}
966	c.spawnStartup(ctx)
967	if c.spawnSlots(ctx, false) != nil || c.publishPending(ctx) != nil {
968		return nil
969	}
970	c.publishState()
971	c.publishWorkspaces()
972	for {
973		if c.securityCheckpoint(ctx) != nil {
974			return nil
975		}
976		startupC, startupRequest := c.pendingStartup()
977		select {
978		case <-ctx.Done():
979			return nil
980		case startupC <- startupRequest:
981			// Successful transfer consumes startup once. Only the receiver can
982			// decide execution admission; a later gate read cannot infer it.
983			c.startup = c.startup[1:]
984			continue
985		case ev, ok := <-c.ch.Client:
986			if !ok {
987				c.ch.Client = nil
988				continue
989			}
990			if c.securityCheckpoint(ctx) != nil {
991				return nil
992			}
993			if c.blockProtected(ev) {
994				continue
995			}
996			switch v := ev.(type) {
997			case ports.SessionLockChanged:
998				if !c.applyLockChanged(v) {
999					continue
1000				}
1001			case ports.CaptureSessionOpen:
1002				c.captureOpen(v)
1003			case ports.CaptureSessionClose:
1004				c.captureClose(v.ID)
1005			case ports.CaptureFrameTaken:
1006				// Nothing new to show (the target flashes already): no scene.
1007				if !c.captureFrame(v) {
1008					continue
1009				}
1010			case ports.CaptureExclusionBegin:
1011				c.captureExclusionBegin(v)
1012			case ports.CaptureExclusionLayer:
1013				c.captureExclusionLayer(v)
1014			case ports.CaptureExclusionEnd:
1015				c.captureExclusionEnd(v.Session)
1016			case ports.LayerChanged:
1017				c.layerChanged = true
1018				c.setLayers(v.Layers)
1019			case ports.InputRegionChanged:
1020				c.windows.setRegion(v)
1021			case ports.WindowMapped:
1022				c.windows.mapped(v, c.now())
1023				c.placement.place(c, v)
1024			case ports.WindowResized:
1025				if _, w := c.screenOf(v.ID); w != nil {
1026					w.ResizeFloating(v.ID, v.Width, v.Height)
1027				}
1028			case ports.PopupRequest:
1029				if err := c.placePopup(ctx, v); err != nil {
1030					return nil
1031				}
1032			case ports.PopupMapped:
1033				if p := c.popups[v.ID]; p != nil {
1034					p.mapped = true
1035				}
1036			case ports.ShortcutsInhibit:
1037				c.windows.setInhibitShortcuts(v.Window, v.Active)
1038				if err := c.updateInhibit(ctx); err != nil {
1039					return nil
1040				}
1041				continue
1042			case ports.OutputPower:
1043				if i := c.screenIndex(v.Output); i >= 0 && c.screens[i].off == v.On {
1044					c.screens[i].off = !v.On
1045				}
1046			case ports.IdleInhibit:
1047				c.windows.setIdleInhibit(v.Window, v.Active)
1048				c.publishState()
1049				continue
1050			case ports.WindowAppID:
1051				c.windows.setAppID(v.ID, v.AppID)
1052			case ports.WindowParent:
1053				if _, w := c.screenOf(v.ID); w != nil {
1054					w.SetDialogParent(v.ID, v.Parent)
1055				}
1056			case ports.WindowUnmapped:
1057				if c.popups[v.ID] != nil {
1058					if err := c.dropPopup(ctx, v.ID); err != nil {
1059						return nil
1060					}
1061				}
1062				if err := c.closePopupsOf(ctx, v.ID); err != nil {
1063					return nil
1064				}
1065				c.windows.drop(v.ID)
1066				if c.drag != nil && c.drag.id == v.ID {
1067					c.abortDrag()
1068				}
1069				if s, _ := c.screenOf(v.ID); s != nil {
1070					s.mon.RemoveWindow(v.ID)
1071				}
1072				c.releaseSlots()
1073				if c.pointer == v.ID {
1074					c.pointer = 0
1075					if err := c.command(ctx, ports.PointerFocus{}); err != nil {
1076						return nil
1077					}
1078				}
1079			case ports.PointerConstrained:
1080				c.constrained = v
1081			case ports.PointerWarp:
1082				if c.warpPointer(ctx, v) != nil {
1083					return nil
1084				}
1085			case ports.WindowMoveRequest:
1086				if c.clientDrag(ctx, v) != nil {
1087					return nil
1088				}
1089			case ports.WindowFullscreenRequest:
1090				c.configures.answer[v.ID] = true
1091				if v.Fullscreen && !v.External && c.now().Sub(c.windows.lookup(v.ID).mappedAt) < fullscreenGrace {
1092					break
1093				}
1094				// A taskbar request is a user action on that window, as an
1095				// activation: it comes on screen with the focus, leaving
1096				// another window's fullscreen.
1097				if v.External && v.Fullscreen && c.activate(ctx, v.ID) != nil {
1098					return nil
1099				}
1100				if s, _ := c.screenOf(v.ID); s != nil {
1101					s.mon.SetFullscreen(v.ID, v.Fullscreen)
1102				}
1103			case ports.WorkspaceActivate:
1104				before := c.cur().mon.Current()
1105				for _, id := range v.IDs {
1106					for i, sc := range c.screens {
1107						if sc.name() == "" {
1108							continue
1109						}
1110						for w := range sc.mon.all() {
1111							if w.ID == id {
1112								c.focusScreen = i
1113								sc.mon.show(w)
1114								break
1115							}
1116						}
1117					}
1118				}
1119				if c.workspaceVisible(ctx, c.cur().mon.Current() != before) != nil {
1120					return nil
1121				}
1122			case ports.WindowActivate:
1123				if c.activate(ctx, v.ID) != nil {
1124					return nil
1125				}
1126			}
1127			if err := c.publish(ctx); err != nil {
1128				return nil
1129			}
1130		case f, ok := <-c.ch.Frames:
1131			if c.securityCheckpoint(ctx) != nil {
1132				return nil
1133			}
1134			if !ok {
1135				c.ch.Frames = nil
1136				continue
1137			}
1138			if c.sliding(f.Output) && c.step(ctx) != nil {
1139				return nil
1140			}
1141			continue
1142		case <-c.frameC:
1143			if c.securityCheckpoint(ctx) != nil {
1144				return nil
1145			}
1146			c.frameC, c.frameStop = nil, nil
1147			if c.step(ctx) != nil {
1148				return nil
1149			}
1150			continue
1151		case <-c.pulse.timerC:
1152			if c.securityCheckpoint(ctx) != nil {
1153				return nil
1154			}
1155			if c.pulseTick() && c.publish(ctx) != nil {
1156				return nil
1157			}
1158			continue
1159		case <-c.capt.timerC:
1160			c.capt.timerC, c.capt.timerStop = nil, nil
1161			if c.captureFlashTick() && c.publish(ctx) != nil {
1162				return nil
1163			}
1164			continue
1165		case ev, ok := <-c.ch.Output:
1166			if !ok {
1167				c.ch.Output = nil
1168				continue
1169			}
1170			if c.securityCheckpoint(ctx) != nil {
1171				return nil
1172			}
1173			switch v := ev.(type) {
1174			case ports.OutputAdded:
1175				c.addScreen(v.Info)
1176				if c.layerChanged {
1177					c.setLayers(c.allLayers())
1178				}
1179			case ports.OutputRemoved:
1180				// A drop target may be on the output going away.
1181				c.abortDrag()
1182				c.removeScreen(v.Name)
1183				if c.layerChanged {
1184					c.setLayers(c.allLayers())
1185				}
1186				c.cursorX, c.cursorY = c.layout().Clamp(c.cursorX, c.cursorY, c.cursorX, c.cursorY)
1187			}
1188			if c.workspaceVisible(ctx, false) != nil {
1189				return nil
1190			}
1191			if err := c.publish(ctx); err != nil {
1192				return nil
1193			}
1194		case ev, ok := <-c.ch.Config:
1195			if !ok {
1196				c.ch.Config = nil
1197				continue
1198			}
1199			if c.securityCheckpoint(ctx) != nil {
1200				return nil
1201			}
1202			if err := c.apply(ev.Config); err != nil {
1203				select {
1204				case c.ch.ConfigErrors <- err:
1205				default:
1206				}
1207				continue
1208			}
1209			if c.workspaceVisible(ctx, false) != nil {
1210				return nil
1211			}
1212			if err := c.publish(ctx); err != nil {
1213				return nil
1214			}
1215		case ev, ok := <-c.ch.Input:
1216			if !ok {
1217				c.ch.Input = nil
1218				continue
1219			}
1220			if c.securityCheckpoint(ctx) != nil {
1221				return nil
1222			}
1223			// Even stale input is activity, but never becomes client input.
1224			if err := c.userActivity(ctx); err != nil {
1225				return nil
1226			}
1227			// Commands can backpressure; recheck after activity before admits/binds.
1228			if c.securityCheckpoint(ctx) != nil {
1229				return nil
1230			}
1231			var admitted bool
1232			ev, admitted = c.admitInput(ev)
1233			if !admitted {
1234				continue
1235			}
1236			if err := c.handleInput(ctx, ev); err != nil {
1237				if errors.Is(err, errSecurityChanged) {
1238					continue
1239				}
1240				return err
1241			}
1242		}
1243	}
1244}
1245
1246// workspaceVisible settles guests and refills slots on a show. Publication
1247// subsequently decides whether visible empty workspaces need terminals.
1248func (c *Core) workspaceVisible(ctx context.Context, shown bool) error {
1249	c.releaseSlots()
1250	c.settleGuests()
1251	return c.spawnSlots(ctx, shown)
1252}
1253
1254// spawnEmpty requests terminals for visible empty workspaces. publish calls
1255// it after the output layout and before scenes, so a terminal never maps
1256// before wayland knows its output.
1257func (c *Core) spawnEmpty(ctx context.Context) error {
1258	if c.protectionRequested() {
1259		return nil
1260	}
1261	first := c.firstTerminalResolved
1262	terms := c.fillEmpty()
1263	err := c.publishPending(ctx)
1264	for i, req := range terms {
1265		if err != nil || !c.trySpawn(ctx, req) {
1266			for _, suppressed := range terms[i:] {
1267				c.cancelTerminal(suppressed)
1268			}
1269			if i == 0 {
1270				c.firstTerminalResolved = first
1271			}
1272			return err
1273		}
1274	}
1275	return err
1276}
1277
1278// activate brings a window forward for a valid xdg-activation token. The
1279// token proves a user action in the requesting client (a click on a link,
1280// a notification), so unlike other client requests it may move the user
1281// (the exception to the ADR 011 golden rule).
1282func (c *Core) activate(ctx context.Context, id WindowID) error {
1283	s, w := c.screenOf(id)
1284	if s == nil || c.popups[id] != nil {
1285		return nil
1286	}
1287	before := c.cur().mon.Current()
1288	if w != s.mon.Current() {
1289		s.mon.show(w)
1290	}
1291	w.Activate(id)
1292	c.focusScreen = c.screenIndex(s.name())
1293	return c.workspaceVisible(ctx, c.cur().mon.Current() != before)
1294}
1295
1296// heldKey names a key for press tracking: by physical key, since Shift
1297// may be released before the key.
1298func heldKey(key ports.KeyEvent) string {
1299	if key.Keycode != 0 {
1300		return "#" + strconv.FormatUint(uint64(key.Keycode), 10)
1301	}
1302	return keyName(key.Keysym)
1303}

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