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}