v0.4.0 b6be3a8

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// Two-finger scrolling moves the overview selection one column per step,
190// along the axis the fingers move most; a small scroll does nothing and
191// lifting the fingers starts over. Outside the overview it goes to the
192// window.
193func TestOverviewTwoFingerScroll(t *testing.T) {
194	cfg := config.Defaults()
195	client := make(chan ports.ClientEvent, 8)
196	input := make(chan ports.InputEvent, 8)
197	output := make(chan ports.OutputEvent, 8)
198	commands := make(chan ports.ClientCommand, 256)
199	scenes := make(chan []ports.Scene, 1)
200	c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes})
201	if err != nil {
202		t.Fatal(err)
203	}
204	ctx, cancel := context.WithCancel(context.Background())
205	defer cancel()
206	go c.Run(ctx)
207	output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 300, Height: 200}}
208	scene(t, scenes)
209	for id := ports.WindowID(1); id <= 3; id++ {
210		client <- ports.WindowMapped{ID: id}
211		scene(t, scenes)
212	}
213	focused := func(id ports.WindowID) func(ports.Scene) bool {
214		return func(s ports.Scene) bool {
215			for _, w := range s.Windows {
216				if w.ID == id && w.Focused && w.Preview > 0 {
217					return true
218				}
219			}
220			return false
221		}
222	}
223	input <- ports.KeyEvent{Keysym: "o", Mods: ports.ModSuper, Pressed: true}
224	sceneMatch(t, scenes, focused(3))
225	input <- ports.KeyEvent{Keysym: "o", Mods: ports.ModSuper}
226	finger := func(dx, dy float64) ports.PointerAxis {
227		return ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Value: dx}, Vertical: ports.ScrollAxis{Set: true, Value: dy}}
228	}
229	// 50 left, lifted, then 50, 50 and 100 left: one step (to 2), the
230	// next ones are longer. Without the reset it would be two (to 1), and
231	// the l below would land on 2.
232	input <- finger(-50, 5)
233	input <- ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}}
234	input <- finger(-50, 5)
235	input <- finger(-50, 0)
236	input <- finger(-100, 0)
237	sceneMatch(t, scenes, focused(2))
238	input <- ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}}
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.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}}
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