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

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 26

Tags 10

↑↓ moveenter openesc close