Skip to content

Commit 4319830

Browse files
authored
refactor: split key labels in layout files (@fehmer) (monkeytypegame#6527)
1 parent 9b26793 commit 4319830

File tree

224 files changed

+11370
-6805
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

224 files changed

+11370
-6805
lines changed

docs/LAYOUTS.md

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,74 @@ The contents of the file should be as follows:
2121
"keymapShowTopRow": false,
2222
"type": "ansi",
2323
"keys": {
24-
"row1": ["`~", "1!", "2@", "3#", "4$", "5%", "6^", "7&", "8*", "9(", "0)", "-_", "=+"],
25-
"row2": ["qQ", "wW", "eE", "rR", "tT", "yY", "uU", "iI", "oO", "pP", "[{", "]}", "\\|"],
26-
"row3": ["aA", "sS", "dD", "fF", "gG", "hH", "jJ", "kK", "lL", ";:", "'\""],
27-
"row4": ["zZ", "xX", "cC", "vV", "bB", "nN", "mM", ",<", ".>", "/?"],
28-
"row5": [" "]
24+
"row1": [
25+
["`", "~"],
26+
["1", "!"],
27+
["2", "@"],
28+
["3", "#"],
29+
["4", "$"],
30+
["5", "%"],
31+
["6", "^"],
32+
["7", "&"],
33+
["8", "*"],
34+
["9", "("],
35+
["0", ")"],
36+
["-", "_"],
37+
["=", "+"]
38+
],
39+
"row2": [
40+
["q", "Q"],
41+
["w", "W"],
42+
["e", "E"],
43+
["r", "R"],
44+
["t", "T"],
45+
["y", "Y"],
46+
["u", "U"],
47+
["i", "I"],
48+
["o", "O"],
49+
["p", "P"],
50+
["[", "{"],
51+
["]", "}"],
52+
["\\", "|"]
53+
],
54+
"row3": [
55+
["a", "A"],
56+
["s", "S"],
57+
["d", "D"],
58+
["f", "F"],
59+
["g", "G"],
60+
["h", "H"],
61+
["j", "J"],
62+
["k", "K"],
63+
["l", "L"],
64+
[";", ":"],
65+
["'", "\""]
66+
],
67+
"row4": [
68+
["z", "Z"],
69+
["x", "X"],
70+
["c", "C"],
71+
["v", "V"],
72+
["b", "B"],
73+
["n", "N"],
74+
["m", "M"],
75+
[",", "<"],
76+
[".", ">"],
77+
["/", "?"]
78+
],
79+
"row5": [[" "]]
2980
}
3081
}
3182

83+
3284
```
3385

3486
It is recommended that you familiarize yourselves with JSON before adding a layout.
3587

3688
`keymapShowTopRow` indicates whether to always show the first row of the layout.
3789
`type` can be `ansi` or `iso`.
3890

39-
In `keys` you need to specify `row1` to `row5`. Add the keys within the row as string. The string can have up to four character. The character define unshifted, shifted, alt-gr and shifted alt-gr character in this order. For example `eE€` defines `e` on regular key press, `E` if `shift` is held and `` if `alt-gr` is held.
91+
In `keys` you need to specify `row1` to `row5`. Add the keys within the row as string-array. The string-array can have up to four character. The character define unshifted, shifted, alt-gr and shifted alt-gr character in this order. For example `["e","E","€"]` defines `e` on regular key press, `E` if `shift` is held and `` if `alt-gr` is held.
4092

4193
**Note:** Quote and backslash characters need to be escaped: `\"` and `\\`.
4294

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { readFileSync } from "fs";
2+
import { layoutKeyToKeycode } from "../../src/ts/utils/key-converter";
3+
4+
const isoDvorak = JSON.parse(
5+
readFileSync(
6+
import.meta.dirname + "/../../static/layouts/swedish_dvorak.json",
7+
"utf-8"
8+
)
9+
);
10+
const dvorak = JSON.parse(
11+
readFileSync(
12+
import.meta.dirname + "/../../static/layouts/dvorak.json",
13+
"utf-8"
14+
)
15+
);
16+
17+
describe("key-converter", () => {
18+
describe("layoutKeyToKeycode", () => {
19+
it("handles unknown key", () => {
20+
const keycode = layoutKeyToKeycode("🤷", isoDvorak);
21+
22+
expect(keycode).toBeUndefined();
23+
});
24+
it("handles iso backslash", () => {
25+
const keycode = layoutKeyToKeycode("*", isoDvorak);
26+
27+
expect(keycode).toEqual("Backslash");
28+
});
29+
it("handles iso IntlBackslash", () => {
30+
const keycode = layoutKeyToKeycode("<", isoDvorak);
31+
32+
expect(keycode).toEqual("IntlBackslash");
33+
});
34+
it("handles iso row4", () => {
35+
const keycode = layoutKeyToKeycode("q", isoDvorak);
36+
37+
expect(keycode).toEqual("KeyX");
38+
});
39+
it("handles ansi", () => {
40+
const keycode = layoutKeyToKeycode("q", dvorak);
41+
42+
expect(keycode).toEqual("KeyX");
43+
});
44+
});
45+
});

frontend/scripts/json-validation.cjs

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,18 @@ function validateOthers() {
133133
return reject(new Error(challengesValidator.errors[0].message));
134134
}
135135

136+
const charDefinitionSchema = {
137+
type: "array",
138+
minItems: 1,
139+
maxItems: 4,
140+
items: { type: "string", minLength: 1, maxLength: 1 },
141+
};
142+
const charDefinitionSchemaRow5 = {
143+
type: "array",
144+
minItems: 1,
145+
maxItems: 2,
146+
items: { type: "string", minLength: 1, maxLength: 1 },
147+
};
136148
//layouts
137149
const layoutsSchema = {
138150
ansi: {
@@ -145,31 +157,31 @@ function validateOthers() {
145157
properties: {
146158
row1: {
147159
type: "array",
148-
items: { type: "string", minLength: 1, maxLength: 4 },
160+
items: charDefinitionSchema,
149161
minItems: 13,
150162
maxItems: 13,
151163
},
152164
row2: {
153165
type: "array",
154-
items: { type: "string", minLength: 1, maxLength: 4 },
166+
items: charDefinitionSchema,
155167
minItems: 13,
156168
maxItems: 13,
157169
},
158170
row3: {
159171
type: "array",
160-
items: { type: "string", minLength: 1, maxLength: 4 },
172+
items: charDefinitionSchema,
161173
minItems: 11,
162174
maxItems: 11,
163175
},
164176
row4: {
165177
type: "array",
166-
items: { type: "string", minLength: 1, maxLength: 4 },
178+
items: charDefinitionSchema,
167179
minItems: 10,
168180
maxItems: 10,
169181
},
170182
row5: {
171183
type: "array",
172-
items: { type: "string", minLength: 1, maxLength: 2 },
184+
items: charDefinitionSchemaRow5,
173185
minItems: 1,
174186
maxItems: 2,
175187
},
@@ -189,31 +201,31 @@ function validateOthers() {
189201
properties: {
190202
row1: {
191203
type: "array",
192-
items: { type: "string", minLength: 1, maxLength: 4 },
204+
items: charDefinitionSchema,
193205
minItems: 13,
194206
maxItems: 13,
195207
},
196208
row2: {
197209
type: "array",
198-
items: { type: "string", minLength: 1, maxLength: 4 },
210+
items: charDefinitionSchema,
199211
minItems: 12,
200212
maxItems: 12,
201213
},
202214
row3: {
203215
type: "array",
204-
items: { type: "string", minLength: 1, maxLength: 4 },
216+
items: charDefinitionSchema,
205217
minItems: 12,
206218
maxItems: 12,
207219
},
208220
row4: {
209221
type: "array",
210-
items: { type: "string", minLength: 1, maxLength: 4 },
222+
items: charDefinitionSchema,
211223
minItems: 11,
212224
maxItems: 11,
213225
},
214226
row5: {
215227
type: "array",
216-
items: { type: "string", minLength: 1, maxLength: 2 },
228+
items: charDefinitionSchemaRow5,
217229
minItems: 1,
218230
maxItems: 2,
219231
},

frontend/src/ts/elements/keymap.ts

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,45 @@ import * as ShiftTracker from "../test/shift-tracker";
1414
import * as AltTracker from "../test/alt-tracker";
1515
import * as KeyConverter from "../utils/key-converter";
1616
import { getActiveFunboxNames } from "../test/funbox/list";
17+
import { areSortedArraysEqual } from "../utils/arrays";
18+
19+
export const keyDataDelimiter = "~~";
1720

1821
const stenoKeys: JSONData.Layout = {
1922
keymapShowTopRow: true,
2023
type: "matrix",
2124
keys: {
2225
row1: [],
23-
row2: ["sS", "tT", "pP", "hH", "**", "fF", "pP", "lL", "tT", "dD"],
24-
row3: ["sS", "kK", "wW", "rR", "**", "rR", "bB", "gG", "sS", "zZ"],
25-
row4: ["aA", "oO", "eE", "uU"],
26+
row2: [
27+
["s", "S"],
28+
["t", "T"],
29+
["p", "P"],
30+
["h", "H"],
31+
["*", "*"],
32+
["f", "F"],
33+
["p", "P"],
34+
["l", "L"],
35+
["t", "T"],
36+
["d", "D"],
37+
],
38+
row3: [
39+
["s", "S"],
40+
["k", "K"],
41+
["w", "W"],
42+
["r", "R"],
43+
["*", "*"],
44+
["r", "R"],
45+
["b", "B"],
46+
["g", "G"],
47+
["s", "S"],
48+
["z", "Z"],
49+
],
50+
row4: [
51+
["a", "A"],
52+
["o", "O"],
53+
["e", "E"],
54+
["u", "U"],
55+
],
2656
row5: [],
2757
},
2858
};
@@ -115,7 +145,7 @@ export function show(): void {
115145
function buildRow(options: {
116146
layoutData: JSONData.Layout;
117147
rowId: string;
118-
rowKeys: string[];
148+
rowKeys: string[][];
119149
layoutNameDisplayString: string;
120150
showTopRow: boolean;
121151
isMatrix: boolean;
@@ -189,30 +219,37 @@ function buildRow(options: {
189219
* It is just created for simplicity in the for loop below.
190220
* */
191221
// If only one space, add another
192-
if (rowKeys.length === 1 && rowKeys[0] === " ") {
193-
rowKeys[1] = rowKeys[0];
222+
const isRowEmpty = (row: string[] | undefined): boolean =>
223+
areSortedArraysEqual(row ?? [], [" "]);
224+
225+
if (rowKeys.length === 1 && isRowEmpty(rowKeys[0])) {
226+
rowKeys[1] = rowKeys[0] ?? [];
194227
}
195228
// If only one alpha, add one space and place it on the left
196-
if (rowKeys.length === 1 && rowKeys[0] !== " ") {
197-
rowKeys[1] = " ";
229+
if (rowKeys.length === 1 && !isRowEmpty(rowKeys[0])) {
230+
rowKeys[1] = [" "];
198231
rowKeys.reverse();
199232
}
200233
// If two alphas equal, replace one with a space on the left
201-
if (rowKeys.length > 1 && rowKeys[0] !== " " && rowKeys[0] === rowKeys[1]) {
202-
rowKeys[0] = " ";
234+
if (
235+
rowKeys.length > 1 &&
236+
!isRowEmpty(rowKeys[0]) &&
237+
areSortedArraysEqual(rowKeys[0] as string[], rowKeys[1] as string[])
238+
) {
239+
rowKeys[0] = [" "];
203240
}
204-
const alphas = (v: string): boolean => v !== " ";
241+
const alphas = (v: string[]): boolean => v.some((key) => key !== " ");
205242
hasAlphas = rowKeys.some(alphas);
206243

207244
keysHtml += "<div></div>";
208245

209246
for (let keyId = 0; keyId < rowKeys.length; keyId++) {
210-
const key = rowKeys[keyId] as string;
247+
const key = rowKeys[keyId] as string[];
211248
let keyDisplay = key[0] as string;
212249
if (Config.keymapLegendStyle === "uppercase") {
213250
keyDisplay = keyDisplay.toUpperCase();
214251
}
215-
const keyVisualValue = key.replace('"', "&quot;");
252+
const keyVisualValue = key.map((it) => it.replace('"', "&quot;"));
216253
// these are used to keep grid layout but magically hide keys using opacity:
217254
let side = keyId < 1 ? "left" : "right";
218255
// we won't use this trick for alternate layouts, unless Alice (for rotation):
@@ -221,7 +258,7 @@ function buildRow(options: {
221258
keysHtml += `<div class="keymapSplitSpacer"></div>`;
222259
r5Grid += "-";
223260
}
224-
if (keyVisualValue === " ") {
261+
if (isRowEmpty(keyVisualValue)) {
225262
keysHtml += `<div class="keymapKey keySpace layoutIndicator ${side}">
226263
<div class="letter" ${letterStyle}>${layoutDisplay}</div>
227264
</div>`;
@@ -256,7 +293,7 @@ function buildRow(options: {
256293
continue;
257294
}
258295

259-
const key = rowKeys[keyId] as string;
296+
const key = rowKeys[keyId] as string[];
260297
const bump = rowId === "row3" && (keyId === 3 || keyId === 6);
261298
let keyDisplay = key[0] as string;
262299
let letterStyle = "";
@@ -277,10 +314,11 @@ function buildRow(options: {
277314
hide = ` invisible`;
278315
}
279316

280-
const keyElement = `<div class="keymapKey${hide}" data-key="${key.replace(
281-
'"',
282-
"&quot;"
283-
)}"><span class="letter" ${letterStyle}>${keyDisplay}</span>${
317+
const keyElement = `<div class="keymapKey${hide}" data-key="${key
318+
.map((it) => it.replace('"', "&quot;"))
319+
.join(
320+
keyDataDelimiter
321+
)}"><span class="letter" ${letterStyle}>${keyDisplay}</span>${
284322
bump ? "<div class='bump'></div>" : ""
285323
}</div>`;
286324

@@ -486,7 +524,9 @@ async function updateLegends(): Promise<void> {
486524
}
487525
) as HTMLElement[];
488526

489-
const layoutKeys = keymapKeys.map((el) => el.dataset["key"]);
527+
const layoutKeys = keymapKeys.map((el) =>
528+
el.dataset["key"]?.split(keyDataDelimiter)
529+
);
490530
if (layoutKeys.includes(undefined)) return;
491531

492532
const keys = keymapKeys.map((el) => el.childNodes[0]);
@@ -508,7 +548,7 @@ async function updateLegends(): Promise<void> {
508548
}
509549

510550
for (let i = 0; i < layoutKeys.length; i++) {
511-
const layoutKey = layoutKeys[i] as string;
551+
const layoutKey = layoutKeys[i] as string[];
512552
const key = keys[i];
513553
const lowerCaseCharacter = layoutKey[0];
514554
const upperCaseCharacter = layoutKey[1];

frontend/src/ts/modals/word-filter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import { Language } from "@monkeytype/contracts/schemas/languages";
1414

1515
type FilterPreset = {
1616
display: string;
17-
getIncludeString: (layout: JSONData.Layout) => string[];
18-
getExcludeString: (layout: JSONData.Layout) => string[];
17+
getIncludeString: (layout: JSONData.Layout) => string[][];
18+
getExcludeString: (layout: JSONData.Layout) => string[][];
1919
};
2020

2121
const presets: Record<string, FilterPreset> = {

0 commit comments

Comments
 (0)