detached df056ef

fix(keys): include Belgian AZERTY digit aliases

commit
df056ef3fbe322df530e3548830a03c7ced5cbe5
author
brice · · 2026-07-04 02:01 UTC
parent
3493a07
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +55 -35
M internal/usecase/keys/router.go +17 -8
@@ -221,18 +221,27 @@ func binding(data []byte) (Action, int, bool) {
221221 return 0, 0, false
222222 }
223223
224-var azertyTopRowDigitVariants = []rune{'&', 'é', '"', '\'', '(', '-', 'è', '_', 'ç'}
224+var topRowDigitAliases = [][]rune{
225+ {'1', '&'},
226+ {'2', 'é'},
227+ {'3', '"'},
228+ {'4', '\''},
229+ {'5', '('},
230+ {'6', '-', '§'},
231+ {'7', 'è'},
232+ {'8', '_', '!'},
233+ {'9', 'ç'},
234+}
225235
226-// topRowDigitIndex maps the symbols emitted by physical top-row digit keys to
236+// topRowDigitIndex maps symbols emitted by physical top-row digit keys to
227237 // zero-based digit positions. It is modifier-agnostic so Alt+digit and future
228238 // Ctrl+digit bindings can share the same layout support.
229239 func topRowDigitIndex(key rune) (int, bool) {
230- if key >= '1' && key <= '9' {
231- return int(key - '1'), true
232- }
233- for idx, variant := range azertyTopRowDigitVariants {
234- if key == variant {
235- return idx, true
240+ for idx, aliases := range topRowDigitAliases {
241+ for _, alias := range aliases {
242+ if key == alias {
243+ return idx, true
244+ }
236245 }
237246 }
238247 return 0, false
@@ -221,18 +221,27 @@ func binding(data []byte) (Action, int, bool) {
221 return 0, 0, false221 return 0, 0, false
222 }222 }
223 223
224-var azertyTopRowDigitVariants = []rune{'&', 'é', '"', '\'', '(', '-', 'è', '_', 'ç'}224+var topRowDigitAliases = [][]rune{
225+ {'1', '&'},
226+ {'2', 'é'},
227+ {'3', '"'},
228+ {'4', '\''},
229+ {'5', '('},
230+ {'6', '-', '§'},
231+ {'7', 'è'},
232+ {'8', '_', '!'},
233+ {'9', 'ç'},
234+}
225 235
226-// topRowDigitIndex maps the symbols emitted by physical top-row digit keys to236+// topRowDigitIndex maps symbols emitted by physical top-row digit keys to
227 // zero-based digit positions. It is modifier-agnostic so Alt+digit and future237 // zero-based digit positions. It is modifier-agnostic so Alt+digit and future
228 // Ctrl+digit bindings can share the same layout support.238 // Ctrl+digit bindings can share the same layout support.
229 func topRowDigitIndex(key rune) (int, bool) {239 func topRowDigitIndex(key rune) (int, bool) {
230- if key >= '1' && key <= '9' {240+ for idx, aliases := range topRowDigitAliases {
231- return int(key - '1'), true241+ for _, alias := range aliases {
232- }242+ if key == alias {
233- for idx, variant := range azertyTopRowDigitVariants {243+ return idx, true
234- if key == variant {244+ }
235- return idx, true
236 }245 }
237 }246 }
238 return 0, false247 return 0, false
M internal/usecase/keys/router_test.go +38 -27
@@ -65,15 +65,28 @@ func TestRouterInterceptsAltDigits(t *testing.T) {
6565 require.Empty(t, h.forwards)
6666 }
6767
68-func TestRouterInterceptsAltAZERTYTopRowAsDigits(t *testing.T) {
69- clk := &fakeClock{}
70- h := &captureHandler{}
71- r := NewRouter(clk, h)
72- for _, key := range []string{"&", "é", "\"", "'", "(", "-", "è", "_", "ç"} {
73- r.Route(append([]byte{ESC}, []byte(key)...))
68+func TestRouterInterceptsAltLayoutDigitAliases(t *testing.T) {
69+ cases := []struct {
70+ name string
71+ keys []string
72+ }{
73+ {name: "QWERTY", keys: []string{"1", "2", "3", "4", "5", "6", "7", "8", "9"}},
74+ {name: "French AZERTY", keys: []string{"&", "é", "\"", "'", "(", "-", "è", "_", "ç"}},
75+ {name: "Belgian AZERTY", keys: []string{"&", "é", "\"", "'", "(", "§", "è", "!", "ç"}},
76+ }
77+
78+ for _, tc := range cases {
79+ t.Run(tc.name, func(t *testing.T) {
80+ clk := &fakeClock{}
81+ h := &captureHandler{}
82+ r := NewRouter(clk, h)
83+ for _, key := range tc.keys {
84+ r.Route(append([]byte{ESC}, []byte(key)...))
85+ }
86+ require.Equal(t, switchTabActions(), h.actions)
87+ require.Empty(t, h.forwards)
88+ })
7489 }
75- require.Equal(t, switchTabActions(), h.actions)
76- require.Empty(t, h.forwards)
7790 }
7891
7992 func TestRouterInterceptsAltAZERTYUTF8SplitAcrossReads(t *testing.T) {
@@ -159,25 +172,23 @@ func assertRouterPendingCleared(t *testing.T, r *Router) {
159172 require.Nil(t, r.pendingDone)
160173 }
161174
162-func TestTopRowDigitIndexAcceptsQWERTYAndAZERTYVariants(t *testing.T) {
163- cases := []struct {
164- key rune
165- want int
166- }{
167- {key: '1', want: 0}, {key: '&', want: 0},
168- {key: '2', want: 1}, {key: 'é', want: 1},
169- {key: '3', want: 2}, {key: '"', want: 2},
170- {key: '4', want: 3}, {key: '\'', want: 3},
171- {key: '5', want: 4}, {key: '(', want: 4},
172- {key: '6', want: 5}, {key: '-', want: 5},
173- {key: '7', want: 6}, {key: 'è', want: 6},
174- {key: '8', want: 7}, {key: '_', want: 7},
175- {key: '9', want: 8}, {key: 'ç', want: 8},
176- }
177- for _, tc := range cases {
178- got, ok := topRowDigitIndex(tc.key)
179- require.True(t, ok, "key %q", tc.key)
180- require.Equal(t, tc.want, got, "key %q", tc.key)
175+func TestTopRowDigitIndexAcceptsLayoutAliases(t *testing.T) {
176+ for want, aliases := range [][]rune{
177+ {'1', '&'},
178+ {'2', 'é'},
179+ {'3', '"'},
180+ {'4', '\''},
181+ {'5', '('},
182+ {'6', '-', '§'},
183+ {'7', 'è'},
184+ {'8', '_', '!'},
185+ {'9', 'ç'},
186+ } {
187+ for _, key := range aliases {
188+ got, ok := topRowDigitIndex(key)
189+ require.True(t, ok, "key %q", key)
190+ require.Equal(t, want, got, "key %q", key)
191+ }
181192 }
182193
183194 _, ok := topRowDigitIndex('0')
@@ -65,15 +65,28 @@ func TestRouterInterceptsAltDigits(t *testing.T) {
65 require.Empty(t, h.forwards)65 require.Empty(t, h.forwards)
66 }66 }
67 67
68-func TestRouterInterceptsAltAZERTYTopRowAsDigits(t *testing.T) {68+func TestRouterInterceptsAltLayoutDigitAliases(t *testing.T) {
69- clk := &fakeClock{}69+ cases := []struct {
70- h := &captureHandler{}70+ name string
71- r := NewRouter(clk, h)71+ keys []string
72- for _, key := range []string{"&", "é", "\"", "'", "(", "-", "è", "_", "ç"} {72+ }{
73- r.Route(append([]byte{ESC}, []byte(key)...))73+ {name: "QWERTY", keys: []string{"1", "2", "3", "4", "5", "6", "7", "8", "9"}},
74+ {name: "French AZERTY", keys: []string{"&", "é", "\"", "'", "(", "-", "è", "_", "ç"}},
75+ {name: "Belgian AZERTY", keys: []string{"&", "é", "\"", "'", "(", "§", "è", "!", "ç"}},
76+ }
77+
78+ for _, tc := range cases {
79+ t.Run(tc.name, func(t *testing.T) {
80+ clk := &fakeClock{}
81+ h := &captureHandler{}
82+ r := NewRouter(clk, h)
83+ for _, key := range tc.keys {
84+ r.Route(append([]byte{ESC}, []byte(key)...))
85+ }
86+ require.Equal(t, switchTabActions(), h.actions)
87+ require.Empty(t, h.forwards)
88+ })
74 }89 }
75- require.Equal(t, switchTabActions(), h.actions)
76- require.Empty(t, h.forwards)
77 }90 }
78 91
79 func TestRouterInterceptsAltAZERTYUTF8SplitAcrossReads(t *testing.T) {92 func TestRouterInterceptsAltAZERTYUTF8SplitAcrossReads(t *testing.T) {
@@ -159,25 +172,23 @@ func assertRouterPendingCleared(t *testing.T, r *Router) {
159 require.Nil(t, r.pendingDone)172 require.Nil(t, r.pendingDone)
160 }173 }
161 174
162-func TestTopRowDigitIndexAcceptsQWERTYAndAZERTYVariants(t *testing.T) {175+func TestTopRowDigitIndexAcceptsLayoutAliases(t *testing.T) {
163- cases := []struct {176+ for want, aliases := range [][]rune{
164- key rune177+ {'1', '&'},
165- want int178+ {'2', 'é'},
166- }{179+ {'3', '"'},
167- {key: '1', want: 0}, {key: '&', want: 0},180+ {'4', '\''},
168- {key: '2', want: 1}, {key: 'é', want: 1},181+ {'5', '('},
169- {key: '3', want: 2}, {key: '"', want: 2},182+ {'6', '-', '§'},
170- {key: '4', want: 3}, {key: '\'', want: 3},183+ {'7', 'è'},
171- {key: '5', want: 4}, {key: '(', want: 4},184+ {'8', '_', '!'},
172- {key: '6', want: 5}, {key: '-', want: 5},185+ {'9', 'ç'},
173- {key: '7', want: 6}, {key: 'è', want: 6},186+ } {
174- {key: '8', want: 7}, {key: '_', want: 7},187+ for _, key := range aliases {
175- {key: '9', want: 8}, {key: 'ç', want: 8},188+ got, ok := topRowDigitIndex(key)
176- }189+ require.True(t, ok, "key %q", key)
177- for _, tc := range cases {190+ require.Equal(t, want, got, "key %q", key)
178- got, ok := topRowDigitIndex(tc.key)191+ }
179- require.True(t, ok, "key %q", tc.key)
180- require.Equal(t, tc.want, got, "key %q", tc.key)
181 }192 }
182 193
183 _, ok := topRowDigitIndex('0')194 _, ok := topRowDigitIndex('0')

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 12

Tags 11

↑↓ moveenter openesc close