1package core
2
3import (
4 "testing"
5
6 "github.com/bnema/neferwl/internal/ports"
7)
8
9const (
10 keyH = 43
11 keyLeft = 113
12 keyF = 41
13 keyShift = 50
14)
15
16// repeatCore is a Core on a test clock with four columns, the last one
17// focused, and a few move and toggle binds.
18func repeatCore(t *testing.T) (*Core, *indicatorClock) {
19 t.Helper()
20 c, ic := indicatorCore(t)
21 cfg := c.cfg
22 cfg.Layout.MaxColumns = 4
23 cfg.Binds = map[string]string{
24 "Cmd+h": string(ActionFocusColumnLeft),
25 "Cmd+l": string(ActionFocusColumnRight),
26 "Cmd+Left": string(ActionFocusColumnLeft),
27 "Cmd+f": string(ActionToggleFullscreen),
28 "Cmd+o": string(ActionToggleOverview),
29 "Cmd+Shift+h": string(ActionMoveColumnLeft),
30 "Cmd+Ctrl+Left": string(ActionFocusWorkspacePrev),
31 }
32 if err := c.apply(cfg); err != nil {
33 t.Fatal(err)
34 }
35 m := c.cur().mon
36 for id := WindowID(1); id <= 4; id++ {
37 m.AddWindow(id)
38 }
39 m.Current().FocusID(4)
40 indicatorScene(t, c)
41 return c, ic
42}
43
44func key(sym string, code uint32, mods ports.Mods, pressed bool) ports.KeyEvent {
45 return ports.KeyEvent{Keysym: sym, Keycode: code, Mods: mods, Pressed: pressed}
46}
47
48// tick fires the repeat timer once, as Run does on its channel.
49func tick(t *testing.T, c *Core) {
50 t.Helper()
51 if c.repeatC == nil {
52 t.Fatal("no repeat armed")
53 }
54 if err := c.repeatTick(t.Context()); err != nil {
55 t.Fatal(err)
56 }
57}
58
59// A held move bind repeats: the first repeat after repeatDelay, then one
60// per repeatInterval, until the key is released.
61func TestHeldBindRepeats(t *testing.T) {
62 c, ic := repeatCore(t)
63 before := len(ic.timers)
64 sendKeys(t, c, key("h", keyH, ports.ModSuper, true))
65 if focusedID(c.cur().mon) != 3 {
66 t.Fatalf("press: focus %d, want 3", focusedID(c.cur().mon))
67 }
68 if len(ic.timers) != before+1 || ic.timers[len(ic.timers)-1] != repeatDelay {
69 t.Fatalf("repeat timer %v, want %v", ic.timers[before:], repeatDelay)
70 }
71 tick(t, c)
72 tick(t, c)
73 if focusedID(c.cur().mon) != 1 {
74 t.Fatalf("two repeats: focus %d, want 1", focusedID(c.cur().mon))
75 }
76 if r := ic.resets[len(ic.resets)-1]; r != repeatInterval {
77 t.Fatalf("next repeat in %v, want %v", r, repeatInterval)
78 }
79 sendKeys(t, c, key("h", keyH, ports.ModSuper, false))
80 if c.repeatC != nil || c.repeat.timer != nil {
81 t.Fatal("release did not stop the repeat")
82 }
83}
84
85// Only moves repeat: a held toggle runs once.
86func TestHeldToggleDoesNotRepeat(t *testing.T) {
87 c, _ := repeatCore(t)
88 sendKeys(t, c, key("f", keyF, ports.ModSuper, true))
89 if c.repeatC != nil {
90 t.Fatal("toggle-fullscreen repeats")
91 }
92 for _, a := range []Action{ActionToggleFullscreen, ActionToggleOverview, ActionCloseWindow, ActionSpawnTerminal, ActionQuit, ActionToggleWindowStash, ActionMaximizeColumn} {
93 if repeats(a) {
94 t.Errorf("%s repeats", a)
95 }
96 }
97 for _, a := range []Action{ActionFocusColumnLeft, ActionMoveColumnRight, ActionFocusWorkspaceNext, ActionMoveWindowToWorkspacePrev, "set-column-width +1"} {
98 if !repeats(a) {
99 t.Errorf("%s does not repeat", a)
100 }
101 }
102}
103
104// Another key pressed or the modifiers changed end the repeat: releasing
105// Cmd first does not leave the move running.
106func TestRepeatStopsOnKeyOrModifierChange(t *testing.T) {
107 c, _ := repeatCore(t)
108 sendKeys(t, c, key("h", keyH, ports.ModSuper, true))
109 sendKeys(t, c, key("Super_L", keySuper, 0, false))
110 if c.repeatC != nil {
111 t.Fatal("releasing Cmd did not stop the repeat")
112 }
113 sendKeys(t, c, key("h", keyH, 0, false))
114
115 sendKeys(t, c, key("h", keyH, ports.ModSuper, true))
116 sendKeys(t, c, key("Shift_L", keyShift, ports.ModSuper|ports.ModShift, true))
117 if c.repeatC != nil {
118 t.Fatal("pressing Shift did not stop the repeat")
119 }
120}
121
122// In the overview, held h/j/k/l or arrows repeat without Cmd, and so do
123// the Cmd binds: holding Left walks the unfolded stash card by card.
124func TestOverviewHeldKeyRepeats(t *testing.T) {
125 c, _ := repeatCore(t)
126 m := c.cur().mon
127 w := m.Current()
128 w.Overflow = OverflowCascade
129 for id := WindowID(40); id < 44; id++ {
130 w.AddWindow(id)
131 w.FocusID(id)
132 w.ToggleWindowStash()
133 }
134 m.ToggleOverview()
135 m.selectCard(w, 3)
136 sendKeys(t, c, key("Left", keyLeft, 0, true))
137 if m.cardAt(w) != 2 {
138 t.Fatalf("press: card %d, want 2", m.cardAt(w))
139 }
140 tick(t, c)
141 tick(t, c)
142 if m.cardAt(w) != 0 {
143 t.Fatalf("two repeats: card %d, want 0", m.cardAt(w))
144 }
145 sendKeys(t, c, key("Left", keyLeft, 0, false))
146 if c.repeatC != nil {
147 t.Fatal("release did not stop the repeat")
148 }
149
150 // Cmd+Left, a bind, repeats in the overview too.
151 m.selectCard(w, 3)
152 sendKeys(t, c, key("Left", keyLeft, ports.ModSuper, true))
153 tick(t, c)
154 if m.cardAt(w) != 1 {
155 t.Fatalf("Cmd+Left held: card %d, want 1", m.cardAt(w))
156 }
157 sendKeys(t, c, key("Left", keyLeft, ports.ModSuper, false))
158
159 // The overview closing ends a repeat, of an overview key or a bind:
160 // Cmd+Left would go on in the layout.
161 for _, mods := range []ports.Mods{0, ports.ModSuper} {
162 if !m.ov.open {
163 m.ToggleOverview()
164 }
165 sendKeys(t, c, key("Left", keyLeft, mods, true))
166 m.CancelOverview()
167 focus := focusedID(m)
168 tick(t, c)
169 if c.repeatC != nil || focusedID(m) != focus {
170 t.Fatalf("mods %v: the repeat outlived the overview", mods)
171 }
172 sendKeys(t, c, key("Left", keyLeft, mods, false))
173 }
174}