feat/hints a471fa4

layer_input_test.go RawHistoryFolder

1package core_test
2
3import (
4	"context"
5	"testing"
6
7	"github.com/bnema/neferwl/internal/core"
8	"github.com/bnema/neferwl/internal/ports"
9)
10
11// next returns the next command of type T matching ok, skipping others.
12func next[T ports.ClientCommand](t *testing.T, ch <-chan ports.ClientCommand, ok func(T) bool) T {
13	t.Helper()
14	for {
15		if v, is := command(t, ch).(T); is && ok(v) {
16			return v
17		}
18	}
19}
20
21func anyOf[T any](T) bool { return true }
22
23// Bars and launchers get the pointer, popups and, on click, the keyboard.
24func TestLayerPointerPopupAndFocus(t *testing.T) {
25	cfg := scrollDefaults()
26	cfg.Border.Width = 0
27	client := make(chan ports.ClientEvent, 8)
28	input := make(chan ports.InputEvent, 8)
29	output := make(chan ports.OutputEvent, 8)
30	commands := make(chan ports.ClientCommand, 256)
31	scenes := make(chan []ports.Scene, 1)
32	c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{})
33	if err != nil {
34		t.Fatal(err)
35	}
36	ctx, cancel := context.WithCancel(context.Background())
37	defer cancel()
38	go c.Run(ctx)
39	output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
40	scene(t, scenes)
41	client <- ports.WindowMapped{ID: 1}
42	scene(t, scenes)
43	if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 1 {
44		t.Fatalf("focus %d", f.ID)
45	}
46	bar := ports.LayerSurface{ID: 6, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Width: 100, Height: 10, ExclusiveZone: 10, Keyboard: 2}
47	client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}}
48	scene(t, scenes)
49
50	// The bar gets the pointer, in its own coordinates.
51	input <- ports.PointerMotion{X: 50, Y: 5}
52	if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 6 || v.X != 50 || v.Y != 5 {
53		t.Fatalf("pointer focus %+v, want the bar", v)
54	}
55	// A click gives the on-demand bar the keyboard.
56	input <- ports.PointerButton{Button: 0x110, Pressed: true}
57	if v := next(t, commands, anyOf[ports.PointerButtonTo]); v.ID != 6 {
58		t.Fatalf("button to %d", v.ID)
59	}
60	if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 6 {
61		t.Fatalf("focus %d, want the bar", f.ID)
62	}
63	input <- ports.PointerButton{Button: 0x110}
64
65	// A menu from the bar is placed under it and slides inside the output.
66	pos := ports.Positioner{Width: 30, Height: 20, AnchorRect: ports.Rect{X: 90, Y: 0, W: 10, H: 10}, Anchor: ports.EdgeBottomLeft, Gravity: ports.EdgeBottomRight, Adjust: ports.AdjustSlideX}
67	client <- ports.PopupRequest{ID: 9, Parent: 6, Positioner: pos, Grab: true}
68	cp := next(t, commands, func(v ports.ConfigurePopup) bool { return v.ID == 9 })
69	if cp.Rect != (ports.Rect{X: 70, Y: 10, W: 30, H: 20}) {
70		t.Fatalf("popup rect %+v", cp.Rect)
71	}
72	client <- ports.PopupMapped{ID: 9}
73	sceneMatch(t, scenes, func(sc ports.Scene) bool {
74		w := sc.Windows[len(sc.Windows)-1]
75		return w.Popup && w.ID == 9 && w.Rect == (ports.Rect{X: 70, Y: 10, W: 30, H: 20})
76	})
77	if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 9 {
78		t.Fatalf("grab focus %d", f.ID)
79	}
80	input <- ports.PointerMotion{X: 80, Y: 15}
81	if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 9 || v.X != 10 || v.Y != 5 {
82		t.Fatalf("pointer focus %+v, want the menu", v)
83	}
84
85	// A click on the window closes the menu and takes the keyboard back.
86	input <- ports.PointerMotion{X: 20, Y: 50}
87	if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 1 {
88		t.Fatalf("pointer focus %+v, want the window", v)
89	}
90	input <- ports.PointerButton{Button: 0x110, Pressed: true}
91	if v := next(t, commands, anyOf[ports.ClosePopup]); v.ID != 9 {
92		t.Fatalf("closed %d", v.ID)
93	}
94	if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 1 {
95		t.Fatalf("focus %d, want the window", f.ID)
96	}
97	input <- ports.PointerButton{Button: 0x110}
98
99	// An exclusive launcher wins over a clicked on-demand bar.
100	input <- ports.PointerMotion{X: 50, Y: 5}
101	next(t, commands, func(v ports.PointerFocus) bool { return v.ID == 6 })
102	input <- ports.PointerButton{Button: 0x110, Pressed: true}
103	next(t, commands, func(v ports.FocusWindow) bool { return v.ID == 6 })
104	input <- ports.PointerButton{Button: 0x110}
105	launcher := ports.LayerSurface{ID: 7, Layer: ports.LayerOverlay, Width: 20, Height: 20, Keyboard: 1}
106	client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar, launcher}}
107	if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 7 {
108		t.Fatalf("focus %d, want the launcher", f.ID)
109	}
110	client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}}
111	if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 6 {
112		t.Fatalf("focus %d, want the bar back", f.ID)
113	}
114
115	// Unmapped then mapped again, the bar does not take the keyboard back.
116	client <- ports.LayerChanged{}
117	next(t, commands, func(v ports.FocusWindow) bool { return v.ID == 1 })
118	client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}}
119	scene(t, scenes)
120	input <- ports.PointerMotion{X: 20, Y: 50} // publishes
121	next(t, commands, func(v ports.PointerFocus) bool { return v.ID == 1 })
122	select {
123	case v := <-commands:
124		if f, ok := v.(ports.FocusWindow); ok {
125			t.Fatalf("focus moved to %d", f.ID)
126		}
127	default:
128	}
129
130	// A window focus change by keyboard or a new window takes it back too.
131	input <- ports.PointerMotion{X: 50, Y: 5}
132	next(t, commands, func(v ports.PointerFocus) bool { return v.ID == 6 })
133	input <- ports.PointerButton{Button: 0x110, Pressed: true}
134	next(t, commands, func(v ports.FocusWindow) bool { return v.ID == 6 })
135	input <- ports.PointerButton{Button: 0x110}
136	client <- ports.WindowMapped{ID: 2}
137	if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 2 {
138		t.Fatalf("focus %d, want the new window", f.ID)
139	}
140}
141
142// A window's menu stays with the windows: an overlay layer covers it.
143func TestWindowPopupUnderOverlay(t *testing.T) {
144	cfg := scrollDefaults()
145	cfg.Border.Width = 0
146	client := make(chan ports.ClientEvent, 8)
147	input := make(chan ports.InputEvent, 8)
148	output := make(chan ports.OutputEvent, 8)
149	commands := make(chan ports.ClientCommand, 256)
150	scenes := make(chan []ports.Scene, 1)
151	c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{})
152	if err != nil {
153		t.Fatal(err)
154	}
155	ctx, cancel := context.WithCancel(context.Background())
156	defer cancel()
157	go c.Run(ctx)
158	output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
159	scene(t, scenes)
160	client <- ports.WindowMapped{ID: 1}
161	scene(t, scenes)
162	client <- ports.PopupRequest{ID: 9, Parent: 1, Positioner: ports.Positioner{Width: 30, Height: 20, AnchorRect: ports.Rect{X: 0, Y: 0, W: 10, H: 10}}}
163	next(t, commands, func(v ports.ConfigurePopup) bool { return v.ID == 9 })
164	client <- ports.PopupMapped{ID: 9}
165	sc := sceneMatch(t, scenes, func(sc ports.Scene) bool { return len(sc.Windows) == 2 })
166	if w := sc.Windows[1]; !w.Popup || w.OverLayers {
167		t.Fatalf("window popup %+v", w)
168	}
169	osd := ports.LayerSurface{ID: 7, Layer: ports.LayerOverlay, Anchor: 15}
170	client <- ports.LayerChanged{Layers: []ports.LayerSurface{osd}}
171	scene(t, scenes)
172	input <- ports.PointerMotion{X: 5, Y: 5}
173	if v := next(t, commands, anyOf[ports.PointerMotionTo]); v.ID != 7 {
174		t.Fatalf("hit %d, want the overlay", v.ID)
175	}
176}
177
178// A fullscreen window hides top and bottom layers from the pointer, not
179// overlay; background and bottom layers get it only where no window is.
180func TestLayerHitOrder(t *testing.T) {
181	cfg := scrollDefaults()
182	cfg.Border.Width = 0
183	client := make(chan ports.ClientEvent, 8)
184	input := make(chan ports.InputEvent, 8)
185	output := make(chan ports.OutputEvent, 8)
186	commands := make(chan ports.ClientCommand, 256)
187	scenes := make(chan []ports.Scene, 1)
188	c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{Clock: steppingClock(t)})
189	if err != nil {
190		t.Fatal(err)
191	}
192	ctx, cancel := context.WithCancel(context.Background())
193	defer cancel()
194	go c.Run(ctx)
195	output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
196	scene(t, scenes)
197	wallpaper := ports.LayerSurface{ID: 5, Layer: ports.LayerBackground, Anchor: 15}
198	bar := ports.LayerSurface{ID: 6, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Height: 10}
199	osd := ports.LayerSurface{ID: 7, Layer: ports.LayerOverlay, Anchor: ports.AnchorBottom, Width: 20, Height: 10}
200	client <- ports.LayerChanged{Layers: []ports.LayerSurface{wallpaper, bar, osd}}
201	scene(t, scenes)
202	hit := func(x, y float64, want ports.WindowID) {
203		t.Helper()
204		input <- ports.PointerMotion{X: x, Y: y}
205		// A focus change is sent only when the target changes; motion always.
206		if v := next(t, commands, anyOf[ports.PointerMotionTo]); v.ID != want {
207			t.Fatalf("(%v,%v) hit %d, want %d", x, y, v.ID, want)
208		}
209	}
210	hit(50, 40, 5)
211	hit(50, 5, 6)
212	client <- ports.WindowMapped{ID: 1}
213	scene(t, scenes)
214	hit(50, 40, 1)
215	hit(50, 5, 6)
216	hit(50, 75, 7)
217	// A fullscreen window is exclusive: bars and notifications are hidden.
218	client <- ports.WindowFullscreenRequest{ID: 1, Fullscreen: true}
219	scene(t, scenes)
220	hit(50, 5, 1)
221	hit(50, 75, 1)
222}
223
224// The pointer moving to another output does not take the keyboard from a
225// clicked on-demand layer: keys still reach it.
226func TestLayerFocusAcrossOutputs(t *testing.T) {
227	cfg := scrollDefaults()
228	cfg.Border.Width = 0
229	client := make(chan ports.ClientEvent, 8)
230	input := make(chan ports.InputEvent, 8)
231	output := make(chan ports.OutputEvent, 8)
232	commands := make(chan ports.ClientCommand, 256)
233	scenes := make(chan []ports.Scene, 1)
234	c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{})
235	if err != nil {
236		t.Fatal(err)
237	}
238	ctx, cancel := context.WithCancel(context.Background())
239	defer cancel()
240	go c.Run(ctx)
241	output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
242	scene(t, scenes)
243	output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-2", Width: 100, Height: 80}}
244	scene(t, scenes)
245	client <- ports.WindowMapped{ID: 1}
246	scene(t, scenes)
247	bar := ports.LayerSurface{ID: 6, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Height: 10, Keyboard: 2, Output: "OUT-1"}
248	client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}}
249	scene(t, scenes)
250	input <- ports.PointerMotion{X: 50, Y: 5}
251	next(t, commands, func(v ports.PointerFocus) bool { return v.ID == 6 })
252	input <- ports.PointerButton{Button: 0x110, Pressed: true}
253	next(t, commands, func(v ports.FocusWindow) bool { return v.ID == 6 })
254	input <- ports.PointerButton{Button: 0x110}
255	input <- ports.PointerMotion{X: 100, Y: 40} // onto OUT-2, now focused
256	next(t, commands, func(v ports.PointerFocus) bool { return v.ID == 0 })
257	input <- ports.KeyEvent{Keysym: "a", Pressed: true}
258	if v := next(t, commands, anyOf[ports.ForwardKey]); v.ID != 6 {
259		t.Fatalf("key to %d, want the bar", v.ID)
260	}
261	input <- ports.KeyEvent{Keysym: "a"}
262	next(t, commands, anyOf[ports.ForwardKey])
263
264	// A window mapped on OUT-2 (focused there) takes the keyboard back.
265	client <- ports.WindowMapped{ID: 2}
266	next(t, commands, func(v ports.FocusWindow) bool { return v.ID == 2 })
267	input <- ports.KeyEvent{Keysym: "b", Pressed: true}
268	if v := next(t, commands, anyOf[ports.ForwardKey]); v.ID != 2 {
269		t.Fatalf("key to %d, want the new window", v.ID)
270	}
271}
272
273// A layer's transparent margin passes pointer focus through to the window.
274func TestLayerInputRegionPassThrough(t *testing.T) {
275	cfg := scrollDefaults()
276	cfg.Border.Width = 0
277	client := make(chan ports.ClientEvent, 16)
278	input := make(chan ports.InputEvent, 16)
279	output := make(chan ports.OutputEvent, 16)
280	commands := make(chan ports.ClientCommand, 256)
281	scenes := make(chan []ports.Scene, 1)
282	c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{})
283	if err != nil {
284		t.Fatal(err)
285	}
286	ctx, cancel := context.WithCancel(context.Background())
287	defer cancel()
288	go c.Run(ctx)
289	output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
290	scene(t, scenes)
291	client <- ports.WindowMapped{ID: 1}
292	scene(t, scenes)
293	client <- ports.LayerChanged{Layers: []ports.LayerSurface{{ID: 6, Layer: ports.LayerOverlay, Width: 100, Height: 80}}}
294	scene(t, scenes)
295	client <- ports.InputRegionChanged{ID: 6, Rects: []ports.Rect{{X: 10, Y: 10, W: 80, H: 60}}}
296	scene(t, scenes)
297	input <- ports.PointerMotion{X: 5, Y: 5}
298	if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 1 {
299		t.Fatalf("margin focus: %+v", v)
300	}
301	input <- ports.PointerMotion{X: 20, Y: 20}
302	if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 6 {
303		t.Fatalf("interior focus: %+v", v)
304	}
305	client <- ports.InputRegionChanged{ID: 6}
306	scene(t, scenes)
307	input <- ports.PointerMotion{X: 20, Y: 21}
308	if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 1 {
309		t.Fatalf("empty focus: %+v", v)
310	}
311	client <- ports.InputRegionChanged{ID: 6, All: true}
312	scene(t, scenes)
313	input <- ports.PointerMotion{X: 5, Y: 5}
314	if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 6 {
315		t.Fatalf("reset focus: %+v", v)
316	}
317}
318
319func TestRemovedLayerInputRegionCleared(t *testing.T) {
320	cfg := scrollDefaults()
321	cfg.Border.Width = 0
322	client := make(chan ports.ClientEvent, 16)
323	input := make(chan ports.InputEvent, 16)
324	output := make(chan ports.OutputEvent, 16)
325	commands := make(chan ports.ClientCommand, 256)
326	scenes := make(chan []ports.Scene, 1)
327	c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{})
328	if err != nil {
329		t.Fatal(err)
330	}
331	ctx, cancel := context.WithCancel(context.Background())
332	defer cancel()
333	go c.Run(ctx)
334	output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
335	scene(t, scenes)
336	client <- ports.WindowMapped{ID: 1}
337	scene(t, scenes)
338	client <- ports.InputRegionChanged{ID: 1}
339	scene(t, scenes) // window's empty region must survive layer changes
340	layer := ports.LayerSurface{ID: 6, Layer: ports.LayerOverlay, Width: 100, Height: 80}
341	client <- ports.LayerChanged{Layers: []ports.LayerSurface{layer}}
342	scene(t, scenes)
343	client <- ports.InputRegionChanged{ID: 6}
344	scene(t, scenes)
345	client <- ports.LayerChanged{}
346	scene(t, scenes)
347	client <- ports.LayerChanged{Layers: []ports.LayerSurface{layer}}
348	scene(t, scenes)
349	input <- ports.PointerMotion{X: 20, Y: 20}
350	if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 6 {
351		t.Fatalf("remapped layer retains old region: %+v", v)
352	}
353	client <- ports.LayerChanged{}
354	scene(t, scenes)
355	input <- ports.PointerMotion{X: 21, Y: 21}
356	if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 0 {
357		t.Fatalf("window region removed with layer: %+v", v)
358	}
359}
360
361func TestPopupInputRegionPassesThroughToWindow(t *testing.T) {
362	cfg := scrollDefaults()
363	cfg.Border.Width = 0
364	client := make(chan ports.ClientEvent, 16)
365	input := make(chan ports.InputEvent, 16)
366	output := make(chan ports.OutputEvent, 16)
367	commands := make(chan ports.ClientCommand, 256)
368	scenes := make(chan []ports.Scene, 1)
369	c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{})
370	if err != nil {
371		t.Fatal(err)
372	}
373	ctx, cancel := context.WithCancel(context.Background())
374	defer cancel()
375	go c.Run(ctx)
376	output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
377	scene(t, scenes)
378	client <- ports.WindowMapped{ID: 1}
379	scene(t, scenes)
380	client <- ports.PopupRequest{ID: 9, Parent: 1, Positioner: ports.Positioner{Width: 30, Height: 20, AnchorRect: ports.Rect{X: 10, Y: 10, W: 10, H: 10}}}
381	next(t, commands, func(v ports.ConfigurePopup) bool { return v.ID == 9 })
382	client <- ports.PopupMapped{ID: 9}
383	sc := sceneMatch(t, scenes, func(sc ports.Scene) bool { return len(sc.Windows) == 2 })
384	client <- ports.InputRegionChanged{ID: 9, Rects: []ports.Rect{{X: 10, Y: 0, W: 20, H: 20}}}
385	scene(t, scenes)
386	popup := sc.Windows[1].Rect
387	input <- ports.PointerMotion{X: float64(popup.X + 2), Y: float64(popup.Y + 2)}
388	if v := next(t, commands, anyOf[ports.PointerMotionTo]); v.ID != 1 {
389		t.Fatalf("popup margin motion: %+v, popup %+v", v, popup)
390	}
391	input <- ports.PointerMotion{X: float64(popup.X + 12), Y: float64(popup.Y + 2)}
392	if v := next(t, commands, anyOf[ports.PointerMotionTo]); v.ID != 9 {
393		t.Fatalf("popup interior motion: %+v", v)
394	}
395}
396
397// A grabbing popup hidden with its parent under a fullscreen window is
398// closed, and the keyboard goes to the fullscreen window.
399func TestHiddenGrabPopupLosesKeyboard(t *testing.T) {
400	cfg := scrollDefaults()
401	cfg.Border.Width = 0
402	client := make(chan ports.ClientEvent, 8)
403	output := make(chan ports.OutputEvent, 8)
404	commands := make(chan ports.ClientCommand, 64)
405	scenes := make(chan []ports.Scene, 1)
406	c, err := core.New(cfg, core.Channels{Client: client, Output: output, Commands: commands, Scenes: scenes}, core.Options{Clock: steppingClock(t)})
407	if err != nil {
408		t.Fatal(err)
409	}
410	ctx, cancel := context.WithCancel(context.Background())
411	defer cancel()
412	go c.Run(ctx)
413	output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
414	<-scenes
415	client <- ports.WindowMapped{ID: 1}
416	<-scenes
417	pos := ports.Positioner{Width: 10, Height: 10, AnchorRect: ports.Rect{W: 1, H: 1}, Anchor: ports.EdgeBottomRight, Gravity: ports.EdgeBottomRight}
418	client <- ports.PopupRequest{ID: 3, Parent: 1, Positioner: pos, Grab: true}
419	next(t, commands, func(v ports.ConfigurePopup) bool { return v.ID == 3 })
420	client <- ports.PopupMapped{ID: 3}
421	if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 3 {
422		t.Fatalf("focus %d, want the grabbing popup", f.ID)
423	}
424	// A new fullscreen window covers its parent: the popup is hidden.
425	client <- ports.WindowMapped{ID: 2}
426	client <- ports.WindowFullscreenRequest{ID: 2, Fullscreen: true}
427	next(t, commands, func(v ports.ClosePopup) bool { return v.ID == 3 })
428	if f := next(t, commands, func(v ports.FocusWindow) bool { return v.ID != 3 }); f.ID != 2 {
429		t.Fatalf("focus %d, want the fullscreen window", f.ID)
430	}
431}

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