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

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