v0.1.0 d9218d6

layer_input_test.go RawHistoryFolder

1package core_test
2
3import (
4	"context"
5	"testing"
6
7	"github.com/bnema/neferwl/internal/adapters/config"
8	"github.com/bnema/neferwl/internal/core"
9	"github.com/bnema/neferwl/internal/ports"
10)
11
12// next returns the next command of type T matching ok, skipping others.
13func next[T ports.ClientCommand](t *testing.T, ch <-chan ports.ClientCommand, ok func(T) bool) T {
14	t.Helper()
15	for {
16		if v, is := command(t, ch).(T); is && ok(v) {
17			return v
18		}
19	}
20}
21
22func anyOf[T any](T) bool { return true }
23
24// Bars and launchers get the pointer, popups and, on click, the keyboard.
25func TestLayerPointerPopupAndFocus(t *testing.T) {
26	cfg := config.Defaults()
27	cfg.Border.Width = 0
28	client := make(chan ports.ClientEvent, 8)
29	input := make(chan ports.InputEvent, 8)
30	output := make(chan ports.OutputEvent, 8)
31	commands := make(chan ports.ClientCommand, 256)
32	scenes := make(chan []ports.Scene, 1)
33	c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes})
34	if err != nil {
35		t.Fatal(err)
36	}
37	ctx, cancel := context.WithCancel(context.Background())
38	defer cancel()
39	go c.Run(ctx)
40	output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
41	scene(t, scenes)
42	client <- ports.WindowMapped{ID: 1}
43	scene(t, scenes)
44	if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 1 {
45		t.Fatalf("focus %d", f.ID)
46	}
47	bar := ports.LayerSurface{ID: 6, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Width: 100, Height: 10, ExclusiveZone: 10, Keyboard: 2}
48	client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}}
49	scene(t, scenes)
50
51	// The bar gets the pointer, in its own coordinates.
52	input <- ports.PointerMotion{X: 50, Y: 5}
53	if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 6 || v.X != 50 || v.Y != 5 {
54		t.Fatalf("pointer focus %+v, want the bar", v)
55	}
56	// A click gives the on-demand bar the keyboard.
57	input <- ports.PointerButton{Button: 0x110, Pressed: true}
58	if v := next(t, commands, anyOf[ports.PointerButtonTo]); v.ID != 6 {
59		t.Fatalf("button to %d", v.ID)
60	}
61	if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 6 {
62		t.Fatalf("focus %d, want the bar", f.ID)
63	}
64	input <- ports.PointerButton{Button: 0x110}
65
66	// A menu from the bar is placed under it and slides inside the output.
67	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}
68	client <- ports.PopupRequest{ID: 9, Parent: 6, Positioner: pos, Grab: true}
69	cp := next(t, commands, func(v ports.ConfigurePopup) bool { return v.ID == 9 })
70	if cp.Rect != (ports.Rect{X: 70, Y: 10, W: 30, H: 20}) {
71		t.Fatalf("popup rect %+v", cp.Rect)
72	}
73	client <- ports.PopupMapped{ID: 9}
74	sceneMatch(t, scenes, func(sc ports.Scene) bool {
75		w := sc.Windows[len(sc.Windows)-1]
76		return w.Popup && w.ID == 9 && w.Rect == (ports.Rect{X: 70, Y: 10, W: 30, H: 20})
77	})
78	if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 9 {
79		t.Fatalf("grab focus %d", f.ID)
80	}
81	input <- ports.PointerMotion{X: 80, Y: 15}
82	if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 9 || v.X != 10 || v.Y != 5 {
83		t.Fatalf("pointer focus %+v, want the menu", v)
84	}
85
86	// A click on the window closes the menu and takes the keyboard back.
87	input <- ports.PointerMotion{X: 20, Y: 50}
88	if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 1 {
89		t.Fatalf("pointer focus %+v, want the window", v)
90	}
91	input <- ports.PointerButton{Button: 0x110, Pressed: true}
92	if v := next(t, commands, anyOf[ports.ClosePopup]); v.ID != 9 {
93		t.Fatalf("closed %d", v.ID)
94	}
95	if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 1 {
96		t.Fatalf("focus %d, want the window", f.ID)
97	}
98	input <- ports.PointerButton{Button: 0x110}
99
100	// An exclusive launcher wins over a clicked on-demand bar.
101	input <- ports.PointerMotion{X: 50, Y: 5}
102	next(t, commands, func(v ports.PointerFocus) bool { return v.ID == 6 })
103	input <- ports.PointerButton{Button: 0x110, Pressed: true}
104	next(t, commands, func(v ports.FocusWindow) bool { return v.ID == 6 })
105	input <- ports.PointerButton{Button: 0x110}
106	launcher := ports.LayerSurface{ID: 7, Layer: ports.LayerOverlay, Width: 20, Height: 20, Keyboard: 1}
107	client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar, launcher}}
108	if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 7 {
109		t.Fatalf("focus %d, want the launcher", f.ID)
110	}
111	client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}}
112	if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 6 {
113		t.Fatalf("focus %d, want the bar back", f.ID)
114	}
115
116	// Unmapped then mapped again, the bar does not take the keyboard back.
117	client <- ports.LayerChanged{}
118	next(t, commands, func(v ports.FocusWindow) bool { return v.ID == 1 })
119	client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}}
120	scene(t, scenes)
121	input <- ports.PointerMotion{X: 20, Y: 50} // publishes
122	next(t, commands, func(v ports.PointerFocus) bool { return v.ID == 1 })
123	select {
124	case v := <-commands:
125		if f, ok := v.(ports.FocusWindow); ok {
126			t.Fatalf("focus moved to %d", f.ID)
127		}
128	default:
129	}
130
131	// A window focus change by keyboard or a new window takes it back too.
132	input <- ports.PointerMotion{X: 50, Y: 5}
133	next(t, commands, func(v ports.PointerFocus) bool { return v.ID == 6 })
134	input <- ports.PointerButton{Button: 0x110, Pressed: true}
135	next(t, commands, func(v ports.FocusWindow) bool { return v.ID == 6 })
136	input <- ports.PointerButton{Button: 0x110}
137	client <- ports.WindowMapped{ID: 2}
138	if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 2 {
139		t.Fatalf("focus %d, want the new window", f.ID)
140	}
141}
142
143// A window's menu stays with the windows: an overlay layer covers it.
144func TestWindowPopupUnderOverlay(t *testing.T) {
145	cfg := config.Defaults()
146	cfg.Border.Width = 0
147	client := make(chan ports.ClientEvent, 8)
148	input := make(chan ports.InputEvent, 8)
149	output := make(chan ports.OutputEvent, 8)
150	commands := make(chan ports.ClientCommand, 256)
151	scenes := make(chan []ports.Scene, 1)
152	c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes})
153	if err != nil {
154		t.Fatal(err)
155	}
156	ctx, cancel := context.WithCancel(context.Background())
157	defer cancel()
158	go c.Run(ctx)
159	output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
160	scene(t, scenes)
161	client <- ports.WindowMapped{ID: 1}
162	scene(t, scenes)
163	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}}}
164	next(t, commands, func(v ports.ConfigurePopup) bool { return v.ID == 9 })
165	client <- ports.PopupMapped{ID: 9}
166	sc := sceneMatch(t, scenes, func(sc ports.Scene) bool { return len(sc.Windows) == 2 })
167	if w := sc.Windows[1]; !w.Popup || w.OverLayers {
168		t.Fatalf("window popup %+v", w)
169	}
170	osd := ports.LayerSurface{ID: 7, Layer: ports.LayerOverlay, Anchor: 15}
171	client <- ports.LayerChanged{Layers: []ports.LayerSurface{osd}}
172	scene(t, scenes)
173	input <- ports.PointerMotion{X: 5, Y: 5}
174	if v := next(t, commands, anyOf[ports.PointerMotionTo]); v.ID != 7 {
175		t.Fatalf("hit %d, want the overlay", v.ID)
176	}
177}
178
179// A fullscreen window hides top and bottom layers from the pointer, not
180// overlay; background and bottom layers get it only where no window is.
181func TestLayerHitOrder(t *testing.T) {
182	cfg := config.Defaults()
183	cfg.Border.Width = 0
184	client := make(chan ports.ClientEvent, 8)
185	input := make(chan ports.InputEvent, 8)
186	output := make(chan ports.OutputEvent, 8)
187	commands := make(chan ports.ClientCommand, 256)
188	scenes := make(chan []ports.Scene, 1)
189	c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes})
190	if err != nil {
191		t.Fatal(err)
192	}
193	ctx, cancel := context.WithCancel(context.Background())
194	defer cancel()
195	go c.Run(ctx)
196	output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
197	scene(t, scenes)
198	wallpaper := ports.LayerSurface{ID: 5, Layer: ports.LayerBackground, Anchor: 15}
199	bar := ports.LayerSurface{ID: 6, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Height: 10}
200	osd := ports.LayerSurface{ID: 7, Layer: ports.LayerOverlay, Anchor: ports.AnchorBottom, Width: 20, Height: 10}
201	client <- ports.LayerChanged{Layers: []ports.LayerSurface{wallpaper, bar, osd}}
202	scene(t, scenes)
203	hit := func(x, y float64, want ports.WindowID) {
204		t.Helper()
205		input <- ports.PointerMotion{X: x, Y: y}
206		// A focus change is sent only when the target changes; motion always.
207		if v := next(t, commands, anyOf[ports.PointerMotionTo]); v.ID != want {
208			t.Fatalf("(%v,%v) hit %d, want %d", x, y, v.ID, want)
209		}
210	}
211	hit(50, 40, 5)
212	hit(50, 5, 6)
213	client <- ports.WindowMapped{ID: 1}
214	scene(t, scenes)
215	hit(50, 40, 1)
216	hit(50, 5, 6)
217	hit(50, 75, 7)
218	client <- ports.WindowFullscreenRequest{ID: 1, Fullscreen: true}
219	scene(t, scenes)
220	hit(50, 5, 1)
221	hit(50, 75, 7)
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 := config.Defaults()
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})
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}

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