feat/data-dnd e05cb15

overview_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// A launcher holding the keyboard over the overview gets its keys; the
12// overview takes them back once it closes.
13func TestOverviewLauncherKeepsKeys(t *testing.T) {
14	cfg := scrollDefaults()
15	client := make(chan ports.ClientEvent, 8)
16	input := make(chan ports.InputEvent, 8)
17	output := make(chan ports.OutputEvent, 8)
18	commands := make(chan ports.ClientCommand, 256)
19	scenes := make(chan []ports.Scene, 1)
20	c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{})
21	if err != nil {
22		t.Fatal(err)
23	}
24	ctx, cancel := context.WithCancel(context.Background())
25	defer cancel()
26	go c.Run(ctx)
27	output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 300, Height: 200}}
28	scene(t, scenes)
29	client <- ports.WindowMapped{ID: 1}
30	scene(t, scenes)
31	client <- ports.WindowMapped{ID: 2}
32	scene(t, scenes)
33	input <- ports.KeyEvent{Keysym: "o", Mods: ports.ModSuper, Pressed: true}
34	sceneMatch(t, scenes, func(s ports.Scene) bool { return len(s.Windows) == 2 && s.Windows[0].Preview > 0 })
35	input <- ports.KeyEvent{Keysym: "o", Mods: ports.ModSuper}
36
37	launcher := ports.LayerSurface{ID: 7, Layer: ports.LayerOverlay, Width: 100, Height: 40, Keyboard: 1}
38	client <- ports.LayerChanged{Layers: []ports.LayerSurface{launcher}}
39	scene(t, scenes)
40	for len(commands) > 0 {
41		<-commands
42	}
43	input <- ports.KeyEvent{Keysym: "h", Pressed: true}
44	if v := next(t, commands, anyOf[ports.ForwardKey]); v.ID != 7 || v.Key.Keysym != "h" {
45		t.Fatalf("forward %+v, want the launcher", v)
46	}
47	input <- ports.KeyEvent{Keysym: "h"}
48	next(t, commands, anyOf[ports.ForwardKey])
49
50	client <- ports.LayerChanged{}
51	scene(t, scenes)
52	input <- ports.KeyEvent{Keysym: "h", Pressed: true}
53	sceneMatch(t, scenes, func(s ports.Scene) bool {
54		for _, w := range s.Windows {
55			if w.ID == 1 && w.Focused && w.Preview > 0 {
56				return true
57			}
58		}
59		return false
60	})
61}
62
63// Focus binds (cmd+arrows and their Vim twins) navigate the overview over
64// a covering float: cmd+k/j round-trips through the columns card, then
65// cmd+up brings it forward, cmd+left selects a column and cmd+down returns.
66func TestOverviewFocusBindsNavigate(t *testing.T) {
67	cfg := scrollDefaults()
68	client := make(chan ports.ClientEvent, 8)
69	input := make(chan ports.InputEvent, 8)
70	output := make(chan ports.OutputEvent, 8)
71	commands := make(chan ports.ClientCommand, 256)
72	scenes := make(chan []ports.Scene, 1)
73	c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{})
74	if err != nil {
75		t.Fatal(err)
76	}
77	ctx, cancel := context.WithCancel(context.Background())
78	defer cancel()
79	go c.Run(ctx)
80	output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 300, Height: 200}}
81	scene(t, scenes)
82	for id := ports.WindowID(1); id <= 2; id++ {
83		client <- ports.WindowMapped{ID: id}
84		scene(t, scenes)
85	}
86	client <- ports.WindowMapped{ID: 9, Floating: true, Width: 300, Height: 200}
87	scene(t, scenes)
88	focused := func(id ports.WindowID) func(ports.Scene) bool {
89		return func(s ports.Scene) bool {
90			for _, w := range s.Windows {
91				if w.ID == id && w.Focused && w.Preview > 0 && w.Dim == 0 {
92					return true
93				}
94			}
95			return false
96		}
97	}
98	press := func(key string) {
99		input <- ports.KeyEvent{Keysym: key, Mods: ports.ModSuper, Pressed: true}
100		input <- ports.KeyEvent{Keysym: key, Mods: ports.ModSuper}
101	}
102	press("o")
103	sceneMatch(t, scenes, focused(9))
104	press("k")
105	sceneMatch(t, scenes, focused(2))
106	press("j")
107	sceneMatch(t, scenes, focused(9))
108	press("Up")
109	sceneMatch(t, scenes, focused(2))
110	press("Left")
111	sceneMatch(t, scenes, focused(1))
112	press("Down")
113	sceneMatch(t, scenes, focused(9))
114}
115
116// Closing through a real bind follows the preview, not the covering float.
117func TestOverviewCloseBindTargetsPreview(t *testing.T) {
118	cfg := scrollDefaults()
119	client := make(chan ports.ClientEvent, 8)
120	input := make(chan ports.InputEvent, 8)
121	output := make(chan ports.OutputEvent, 8)
122	commands := make(chan ports.ClientCommand, 256)
123	scenes := make(chan []ports.Scene, 1)
124	c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{})
125	if err != nil {
126		t.Fatal(err)
127	}
128	ctx, cancel := context.WithCancel(context.Background())
129	defer cancel()
130	go c.Run(ctx)
131	output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 300, Height: 200}}
132	scene(t, scenes)
133	for _, id := range []ports.WindowID{1, 2} {
134		client <- ports.WindowMapped{ID: id}
135		scene(t, scenes)
136	}
137	client <- ports.WindowMapped{ID: 9, Floating: true, Width: 300, Height: 200}
138	scene(t, scenes)
139	press := func(key string, mods ports.Mods) {
140		input <- ports.KeyEvent{Keysym: key, Mods: mods, Pressed: true}
141		input <- ports.KeyEvent{Keysym: key, Mods: mods}
142	}
143	press("o", ports.ModSuper)
144	sceneMatch(t, scenes, func(s ports.Scene) bool {
145		for _, w := range s.Windows {
146			if w.ID == 9 && w.Focused && w.Preview > 0 {
147				return true
148			}
149		}
150		return false
151	})
152	press("Up", 0)
153	sceneMatch(t, scenes, func(s ports.Scene) bool {
154		for _, w := range s.Windows {
155			if w.ID == 2 && w.Focused && w.Preview > 0 {
156				return true
157			}
158		}
159		return false
160	})
161	press("q", ports.ModSuper)
162	if got := next(t, commands, anyOf[ports.CloseWindow]); got.ID != 2 {
163		t.Fatalf("close target %d, want preview 2", got.ID)
164	}
165}
166
167func TestFloatOnlyMonitorDirectionalExit(t *testing.T) {
168	for _, dir := range []int{-1, 1} {
169		r := startMulti(t, nil, left, right)
170		if dir < 0 {
171			r.key(t, "Right", ports.ModAlt|ports.ModCtrl)
172		}
173		r.client <- ports.WindowMapped{ID: 9, Floating: true, Width: 20, Height: 20}
174		receive(t, r.scenes)
175		key := "Right"
176		want := right.Name
177		if dir < 0 {
178			key, want = "Left", left.Name
179		}
180		r.key(t, key, ports.ModAlt)
181		out := lastOutputs(t, r.commands)
182		if out.Focused != want {
183			t.Fatalf("direction %d: focused output %q, want %q", dir, out.Focused, want)
184		}
185	}
186}
187
188// Three-finger navigation changes selection during movement and resets on lift.
189func TestOverviewThreeFingerNavigation(t *testing.T) {
190	cfg := scrollDefaults()
191	client := make(chan ports.ClientEvent, 8)
192	input := make(chan ports.InputEvent, 8)
193	output := make(chan ports.OutputEvent, 8)
194	commands := make(chan ports.ClientCommand, 256)
195	scenes := make(chan []ports.Scene, 1)
196	c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{})
197	if err != nil {
198		t.Fatal(err)
199	}
200	ctx, cancel := context.WithCancel(context.Background())
201	defer cancel()
202	go c.Run(ctx)
203	output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 300, Height: 200}}
204	scene(t, scenes)
205	for id := ports.WindowID(1); id <= 3; id++ {
206		client <- ports.WindowMapped{ID: id}
207		scene(t, scenes)
208	}
209	focused := func(id ports.WindowID) func(ports.Scene) bool {
210		return func(s ports.Scene) bool {
211			for _, w := range s.Windows {
212				if w.ID == id && w.Focused && w.Preview > 0 {
213					return true
214				}
215			}
216			return false
217		}
218	}
219	input <- ports.KeyEvent{Keysym: "o", Mods: ports.ModSuper, Pressed: true}
220	sceneMatch(t, scenes, focused(3))
221	input <- ports.KeyEvent{Keysym: "o", Mods: ports.ModSuper}
222	finger := func(dx, dy float64) ports.SwipeUpdate {
223		return ports.SwipeUpdate{DX: dx, DY: dy}
224	}
225	input <- ports.SwipeBegin{Fingers: 3}
226	// 50 left, lifted, then 50, 50 and 100 left: one step (to 2), the
227	// next ones are longer. Without the reset it would be two (to 1), and
228	// the l below would land on 2.
229	input <- finger(-50, 5)
230	input <- ports.SwipeEnd{}
231	input <- ports.SwipeBegin{Fingers: 3}
232	input <- finger(-50, 5)
233	input <- finger(-50, 0)
234	input <- finger(-100, 0)
235	sceneMatch(t, scenes, focused(2))
236	input <- ports.SwipeEnd{}
237	input <- ports.SwipeBegin{Fingers: 3}
238	input <- ports.KeyEvent{Keysym: "l", Pressed: true}
239	sceneMatch(t, scenes, focused(3))
240	input <- ports.KeyEvent{Keysym: "l"}
241	input <- finger(-60, 0)
242	sceneMatch(t, scenes, focused(2))
243	input <- ports.SwipeEnd{}
244	input <- ports.SwipeBegin{Fingers: 3}
245	// A wheel notch down has nowhere to go (no workspace below with
246	// windows); a finger scroll left moves on.
247	input <- ports.PointerAxis{Source: ports.AxisWheel, Vertical: ports.ScrollAxis{Set: true, Value: 15, V120: 120}}
248	input <- finger(-70, 0)
249	sceneMatch(t, scenes, focused(1))
250}

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