fix(keys): include Belgian AZERTY digit aliases
- commit
- df056ef3fbe322df530e3548830a03c7ced5cbe5
- author
- brice · · 2026-07-04 02:01 UTC
- parent
- 3493a07
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 221 | 221 | 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 to | |
| 236 | + | // topRowDigitIndex maps symbols emitted by physical top-row digit keys to | |
| 227 | 237 | // zero-based digit positions. It is modifier-agnostic so Alt+digit and future | |
| 228 | 238 | // Ctrl+digit bindings can share the same layout support. | |
| 229 | 239 | 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 | + | } | |
| 236 | 245 | } | |
| 237 | 246 | } | |
| 238 | 247 | return 0, false | |
| @@ -221,18 +221,27 @@ func binding(data []byte) (Action, int, bool) { | |||||
| 221 | return 0, 0, false | 221 | 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 to | 236 | + | // topRowDigitIndex maps symbols emitted by physical top-row digit keys to |
| 227 | // zero-based digit positions. It is modifier-agnostic so Alt+digit and future | 237 | // 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'), true | 241 | + | 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, false | 247 | return 0, false | ||
M internal/usecase/keys/router_test.go +38 -27
| @@ -65,15 +65,28 @@ func TestRouterInterceptsAltDigits(t *testing.T) { | |||
| 65 | 65 | require.Empty(t, h.forwards) | |
| 66 | 66 | } | |
| 67 | 67 | ||
| 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 | + | }) | |
| 74 | 89 | } | |
| 75 | - | require.Equal(t, switchTabActions(), h.actions) | |
| 76 | - | require.Empty(t, h.forwards) | |
| 77 | 90 | } | |
| 78 | 91 | ||
| 79 | 92 | func TestRouterInterceptsAltAZERTYUTF8SplitAcrossReads(t *testing.T) { | |
| @@ -159,25 +172,23 @@ func assertRouterPendingCleared(t *testing.T, r *Router) { | |||
| 159 | 172 | require.Nil(t, r.pendingDone) | |
| 160 | 173 | } | |
| 161 | 174 | ||
| 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 | + | } | |
| 181 | 192 | } | |
| 182 | 193 | ||
| 183 | 194 | _, 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 rune | 177 | + | {'1', '&'}, |
| 165 | - | want int | 178 | + | {'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') | ||