v0.27.2 5960dd8

keycodes_test.go RawHistoryFolder

1package input
2
3import (
4	"fmt"
5	"testing"
6)
7
8// TestKeycodeConstants verifies the hardware keycode constants are correct.
9// These are XKB keycodes (evdev + 8) for the number row keys.
10func TestKeycodeConstants(t *testing.T) {
11	tests := []struct {
12		name     string
13		keycode  uint
14		expected uint
15	}{
16		{"KeycodeDigit1", KeycodeDigit1, 10},
17		{"KeycodeDigit2", KeycodeDigit2, 11},
18		{"KeycodeDigit3", KeycodeDigit3, 12},
19		{"KeycodeDigit4", KeycodeDigit4, 13},
20		{"KeycodeDigit5", KeycodeDigit5, 14},
21		{"KeycodeDigit6", KeycodeDigit6, 15},
22		{"KeycodeDigit7", KeycodeDigit7, 16},
23		{"KeycodeDigit8", KeycodeDigit8, 17},
24		{"KeycodeDigit9", KeycodeDigit9, 18},
25		{"KeycodeDigit0", KeycodeDigit0, 19},
26	}
27
28	for _, tt := range tests {
29		t.Run(tt.name, func(t *testing.T) {
30			if tt.keycode != tt.expected {
31				t.Errorf("%s = %d, want %d", tt.name, tt.keycode, tt.expected)
32			}
33		})
34	}
35}
36
37// TestKeycodeToTabActionMapping verifies all number row keycodes map to the correct tab actions.
38func TestKeycodeToTabActionMapping(t *testing.T) {
39	tests := []struct {
40		keycode        uint
41		expectedAction Action
42	}{
43		{KeycodeDigit1, ActionSwitchTabIndex1},
44		{KeycodeDigit2, ActionSwitchTabIndex2},
45		{KeycodeDigit3, ActionSwitchTabIndex3},
46		{KeycodeDigit4, ActionSwitchTabIndex4},
47		{KeycodeDigit5, ActionSwitchTabIndex5},
48		{KeycodeDigit6, ActionSwitchTabIndex6},
49		{KeycodeDigit7, ActionSwitchTabIndex7},
50		{KeycodeDigit8, ActionSwitchTabIndex8},
51		{KeycodeDigit9, ActionSwitchTabIndex9},
52		{KeycodeDigit0, ActionSwitchTabIndex10},
53	}
54
55	for _, tt := range tests {
56		t.Run(string(tt.expectedAction), func(t *testing.T) {
57			action, ok := KeycodeToTabAction[tt.keycode]
58			if !ok {
59				t.Errorf("keycode %d not found in KeycodeToTabAction map", tt.keycode)
60				return
61			}
62			if action != tt.expectedAction {
63				t.Errorf("KeycodeToTabAction[%d] = %s, want %s", tt.keycode, action, tt.expectedAction)
64			}
65		})
66	}
67}
68
69// TestKeycodeToTabActionCompleteness verifies the map contains exactly 10 entries.
70func TestKeycodeToTabActionCompleteness(t *testing.T) {
71	expectedCount := 10
72	if len(KeycodeToTabAction) != expectedCount {
73		t.Errorf("KeycodeToTabAction has %d entries, want %d", len(KeycodeToTabAction), expectedCount)
74	}
75}
76
77// TestKeycodeToTabActionNoUnexpectedKeycodes verifies only valid keycodes are in the map.
78func TestKeycodeToTabActionNoUnexpectedKeycodes(t *testing.T) {
79	validKeycodes := map[uint]bool{
80		KeycodeDigit1: true,
81		KeycodeDigit2: true,
82		KeycodeDigit3: true,
83		KeycodeDigit4: true,
84		KeycodeDigit5: true,
85		KeycodeDigit6: true,
86		KeycodeDigit7: true,
87		KeycodeDigit8: true,
88		KeycodeDigit9: true,
89		KeycodeDigit0: true,
90	}
91
92	for keycode := range KeycodeToTabAction {
93		if !validKeycodes[keycode] {
94			t.Errorf("unexpected keycode %d in KeycodeToTabAction map", keycode)
95		}
96	}
97}
98
99// European keyboard layout test scenarios.
100// These test the concept that hardware keycodes are layout-independent.
101//
102// On all these layouts, the physical "1" key has hardware keycode 10,
103// regardless of what character it produces:
104//
105// | Layout      | Physical "1" key produces | Keycode |
106// |-------------|---------------------------|---------|
107// | QWERTY (US) | 1                         | 10      |
108// | QWERTY (UK) | 1                         | 10      |
109// | AZERTY (FR) | &                         | 10      |
110// | AZERTY (BE) | &                         | 10      |
111// | QWERTZ (DE) | 1                         | 10      |
112// | QWERTZ (CH) | 1                         | 10      |
113// | Dvorak      | 1                         | 10      |
114
115// TestEuropeanLayoutKeycodes documents and verifies the keycode behavior
116// expected for various European keyboard layouts.
117func TestEuropeanLayoutKeycodes(t *testing.T) {
118	// This test documents that our solution works for all these layouts
119	// because we use hardware keycodes, not keyvals.
120	//
121	// The keycode for the physical number row is always 10-19,
122	// regardless of what character the key produces.
123
124	layouts := []struct {
125		name        string
126		description string
127		// Physical key position (1-9, 0) always maps to keycode 10-19
128		// The keyval differs but we don't care - we use keycode
129	}{
130		{
131			name:        "QWERTY_US",
132			description: "US English - 1234567890 produce digits directly",
133		},
134		{
135			name:        "QWERTY_UK",
136			description: "UK English - 1234567890 produce digits directly",
137		},
138		{
139			name:        "AZERTY_FR",
140			description: "French - &é\"'(-è_çà produce symbols, Shift for digits",
141		},
142		{
143			name:        "AZERTY_BE",
144			description: "Belgian - &é\"'(§è!çà produce symbols, Shift for digits",
145		},
146		{
147			name:        "QWERTZ_DE",
148			description: "German - 1234567890 produce digits directly (symbols on Shift)",
149		},
150		{
151			name:        "QWERTZ_CH",
152			description: "Swiss German - 1234567890 produce digits directly",
153		},
154		{
155			name:        "QWERTZ_AT",
156			description: "Austrian - 1234567890 produce digits directly",
157		},
158		{
159			name:        "Dvorak",
160			description: "Dvorak - number row same as QWERTY",
161		},
162		{
163			name:        "Colemak",
164			description: "Colemak - number row same as QWERTY",
165		},
166		{
167			name:        "Portuguese_PT",
168			description: "Portuguese - 1234567890 produce digits directly",
169		},
170		{
171			name:        "Spanish_ES",
172			description: "Spanish - 1234567890 produce digits directly",
173		},
174		{
175			name:        "Italian_IT",
176			description: "Italian - 1234567890 produce digits directly",
177		},
178		{
179			name:        "Nordic",
180			description: "Nordic layouts (SE/NO/DK/FI) - 1234567890 produce digits directly",
181		},
182	}
183
184	for _, layout := range layouts {
185		t.Run(layout.name, func(t *testing.T) {
186			// For all layouts, pressing Alt + physical "1" key should:
187			// 1. Have hardware keycode 10 (KeycodeDigit1)
188			// 2. Map to ActionSwitchTabIndex1
189
190			action, ok := KeycodeToTabAction[KeycodeDigit1]
191			if !ok {
192				t.Errorf("[%s] keycode 10 (physical '1') not mapped", layout.name)
193				return
194			}
195			if action != ActionSwitchTabIndex1 {
196				t.Errorf("[%s] keycode 10 maps to %s, want %s",
197					layout.name, action, ActionSwitchTabIndex1)
198			}
199
200			// Document the layout for clarity
201			t.Logf("Layout %s: %s - keycode fallback will work", layout.name, layout.description)
202		})
203	}
204}
205
206// TestAZERTYSpecificScenario tests the specific scenario from issue #25.
207// On French AZERTY, pressing Alt+1 produces Alt+& (ampersand) because
208// the digit 1 requires Shift. Our keycode fallback handles this.
209func TestAZERTYSpecificScenario(t *testing.T) {
210	// Scenario: User on French AZERTY keyboard presses Alt + physical "1" key
211	//
212	// What happens:
213	// 1. GTK receives: keyval=ampersand (0x26), keycode=10, modifiers=Alt
214	// 2. keyval lookup for "Alt+&" fails (no such shortcut defined)
215	// 3. Keycode fallback: keycode 10 with Alt modifier
216	// 4. KeycodeToTabAction[10] = ActionSwitchTabIndex1
217	// 5. Tab 1 is activated
218
219	// The keycode for physical "1" on ALL layouts
220	physicalKey1Keycode := uint(10)
221
222	// Verify our mapping handles this
223	action, ok := KeycodeToTabAction[physicalKey1Keycode]
224	if !ok {
225		t.Fatal("keycode 10 should be mapped for AZERTY support")
226	}
227	if action != ActionSwitchTabIndex1 {
228		t.Errorf("keycode 10 should map to ActionSwitchTabIndex1, got %s", action)
229	}
230}
231
232// TestNonNumberRowKeycodes verifies that non-number-row keycodes don't accidentally match.
233func TestNonNumberRowKeycodes(t *testing.T) {
234	// Some keycodes that should NOT be in our map
235	invalidKeycodes := []uint{
236		0,  // Invalid
237		1,  // Escape
238		9,  // Before number row
239		20, // After number row (minus key)
240		24, // Q key
241		38, // A key
242		50, // Shift
243		64, // Alt
244	}
245
246	for _, keycode := range invalidKeycodes {
247		if _, ok := KeycodeToTabAction[keycode]; ok {
248			t.Errorf("keycode %d should not be in KeycodeToTabAction map", keycode)
249		}
250	}
251}
252
253// TestKeycodeToDigitIndex verifies the keycode to digit index conversion.
254func TestKeycodeToDigitIndex(t *testing.T) {
255	tests := []struct {
256		keycode       uint
257		expectedIndex int
258		expectedOk    bool
259	}{
260		{KeycodeDigit1, 0, true},
261		{KeycodeDigit2, 1, true},
262		{KeycodeDigit3, 2, true},
263		{KeycodeDigit4, 3, true},
264		{KeycodeDigit5, 4, true},
265		{KeycodeDigit6, 5, true},
266		{KeycodeDigit7, 6, true},
267		{KeycodeDigit8, 7, true},
268		{KeycodeDigit9, 8, true},
269		{KeycodeDigit0, 9, true},
270		// Invalid keycodes
271		{0, -1, false},
272		{9, -1, false},  // Before number row
273		{20, -1, false}, // After number row
274		{64, -1, false}, // Alt key
275	}
276
277	for _, tt := range tests {
278		t.Run(fmt.Sprintf("keycode_%d", tt.keycode), func(t *testing.T) {
279			index, ok := KeycodeToDigitIndex(tt.keycode)
280			if ok != tt.expectedOk {
281				t.Errorf("KeycodeToDigitIndex(%d) ok = %v, want %v", tt.keycode, ok, tt.expectedOk)
282			}
283			if index != tt.expectedIndex {
284				t.Errorf("KeycodeToDigitIndex(%d) index = %d, want %d", tt.keycode, index, tt.expectedIndex)
285			}
286		})
287	}
288}
289
290// TestKeycodeToDigitIndexForOmnibox verifies the function works for omnibox Ctrl+1-9/0 shortcuts.
291func TestKeycodeToDigitIndexForOmnibox(t *testing.T) {
292	// Simulate omnibox usage: Ctrl+1 should select first item (index 0)
293	// Ctrl+0 should select 10th item (index 9)
294
295	// Physical "1" key -> index 0 (first omnibox result)
296	index, ok := KeycodeToDigitIndex(KeycodeDigit1)
297	if !ok || index != 0 {
298		t.Errorf("Ctrl+1 should map to index 0, got index=%d ok=%v", index, ok)
299	}
300
301	// Physical "0" key -> index 9 (10th omnibox result)
302	index, ok = KeycodeToDigitIndex(KeycodeDigit0)
303	if !ok || index != 9 {
304		t.Errorf("Ctrl+0 should map to index 9, got index=%d ok=%v", index, ok)
305	}
306}

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 8

Tags 42

↑↓ moveenter openesc close