From 306d6bc5c8fa298b5605a2c55e25e9d54413597b Mon Sep 17 00:00:00 2001 From: Miodec Date: Wed, 28 Oct 2026 10:01:14 +0100 Subject: [PATCH 1/6] refactor: merge commandline metadata into config metadata (C5) (@miodec) Each config key's metadata gets a required `commandline` field (`null` with a reason when the key has no generated command). Side effects (test restarts, sound/font previews) move to `commandline/config-actions` to avoid importing the setters' dependents from config metadata. Command icons become `fa: FaObject`. Generated commands are identical to master (ids, labels, icons, options, exec/hover side effects for all 79 keys). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NZNRfGDYsGYMUTp2zYUTrv --- docs/specs/solid-monkeytype/roadmap.md | 2 +- frontend/__tests__/commandline/util.spec.ts | 27 +- .../input/helpers/fail-or-finish.spec.ts | 11 +- .../ts/commandline/commandline-metadata.ts | 780 ------------------ frontend/src/ts/commandline/config-actions.ts | 131 +++ frontend/src/ts/commandline/lists.ts | 38 +- .../lists/add-or-remove-theme-to-favorites.ts | 5 +- .../ts/commandline/lists/background-filter.ts | 10 +- frontend/src/ts/commandline/lists/bail-out.ts | 2 +- .../ts/commandline/lists/custom-background.ts | 6 +- .../commandline/lists/custom-themes-list.ts | 2 +- .../src/ts/commandline/lists/font-family.ts | 8 +- frontend/src/ts/commandline/lists/funbox.ts | 2 +- .../ts/commandline/lists/load-challenge.ts | 2 +- .../src/ts/commandline/lists/min-burst.ts | 2 +- .../src/ts/commandline/lists/navigation.ts | 14 +- frontend/src/ts/commandline/lists/presets.ts | 4 +- .../ts/commandline/lists/quote-favorites.ts | 4 +- .../src/ts/commandline/lists/result-screen.ts | 14 +- frontend/src/ts/commandline/lists/tags.ts | 6 +- frontend/src/ts/commandline/lists/themes.ts | 2 +- frontend/src/ts/commandline/types.ts | 4 +- frontend/src/ts/commandline/util.ts | 62 +- .../ts/components/modals/CommandlineModal.tsx | 47 +- frontend/src/ts/config/metadata.tsx | 714 +++++++++++++++- 25 files changed, 974 insertions(+), 925 deletions(-) delete mode 100644 frontend/src/ts/commandline/commandline-metadata.ts create mode 100644 frontend/src/ts/commandline/config-actions.ts diff --git a/docs/specs/solid-monkeytype/roadmap.md b/docs/specs/solid-monkeytype/roadmap.md index 0d9d88404a8d..7588485aa799 100644 --- a/docs/specs/solid-monkeytype/roadmap.md +++ b/docs/specs/solid-monkeytype/roadmap.md @@ -102,7 +102,7 @@ Serial; each step needs the previous one. Verify each in the browser (`__mt.chec | [ ] | C2 | S · commit each | Replace config-bus subscribers outside the test with effects on store keys: theme controller, sound controller, quotes controller, challenge controller, commandline theme list | C1 | One PR each, all parallel | | [ ] | C3 | M · commit per area; PR for test modules | Move mutable `Config.x` reads to the store, by area: format singleton; commandline lists; controllers; input; test modules | C1 | Parallel by area; the test-module part follows T3/T6 where they overlap | | [ ] | C4 | L · PR | Delete the mutable config object, the config event bus and the full-config-change events; whole-config apply becomes one batched reconcile (the `batch` already exists since C1); convert remaining test subscribers (pace caret, tts, practise words, test-logic) to effects | C2, C3, T6, L2 | Last step of C | -| [ ] | C5 | M · PR | Merge commandline metadata into config metadata; command icons become icon-component types | none | Can be L-track work | +| [x] | C5 | M · PR | Merge commandline metadata into config metadata; command icons become icon-component types | none | Done in PR (link follows): each config key's metadata has a required `commandline` (`null` with a reason for keys set elsewhere or with hand-built lists); `CommandlineConfigKey` is derived from it. Side effects (`afterExec` restarts, sound/font `hover` previews) moved to `commandline/config-actions.ts`, since the modules they call import the setters; its restarts go with T6b. Labels moved unchanged (TODO to unify them with `optionsMetadata`). `Command.icon`/`iconType` → `fa: FaObject`; the modal's text-icon branch was dead and is gone | ## Track U: user data (parallel with T and C) diff --git a/frontend/__tests__/commandline/util.spec.ts b/frontend/__tests__/commandline/util.spec.ts index fe3d9cde0808..70325668328c 100644 --- a/frontend/__tests__/commandline/util.spec.ts +++ b/frontend/__tests__/commandline/util.spec.ts @@ -3,9 +3,12 @@ import { describe, it, expect, afterAll, vi } from "vitest"; import * as Util from "../../src/ts/commandline/util"; -import type { CommandlineConfigMetadata } from "../../src/ts/commandline/commandline-metadata"; +import type { CommandlineConfigActions } from "../../src/ts/commandline/config-actions"; import type { ConfigKey } from "@monkeytype/schemas/configs"; -import type { ConfigMetadata } from "../../src/ts/config/metadata"; +import type { + CommandlineMetadata, + ConfigMetadata, +} from "../../src/ts/config/metadata"; import { z, ZodSchema } from "zod"; import { Command } from "../../src/ts/commandline/types"; @@ -13,8 +16,8 @@ const buildCommandForConfigKey = Util.__testing._buildCommandForConfigKey; describe("CommandlineUtils", () => { vi.mock("../../src/ts/config/metadata", () => ({ configMetadata: [] })); - vi.mock("../../src/ts/commandline/commandline-metadata", () => ({ - commandlineConfigMetadata: [], + vi.mock("../../src/ts/commandline/config-actions", () => ({ + commandlineConfigActions: {}, })); afterAll(() => { @@ -151,10 +154,10 @@ describe("CommandlineUtils", () => { input: { display: "custom test...", inputValueConvert: Number, - afterExec, alias: "alias", }, }, + actions: { input: { afterExec } }, configMeta: { fa: { icon: "fa-keyboard", @@ -178,7 +181,7 @@ describe("CommandlineUtils", () => { defaultValue: expect.anything(), alias: "alias", input: true, - icon: "fa-keyboard", + fa: { icon: "fa-keyboard" }, exec: expect.anything(), hover: undefined, configValue: undefined, @@ -200,10 +203,10 @@ describe("CommandlineUtils", () => { cmdMeta: { input: { display: "custom test...", - afterExec, alias: "alias", }, }, + actions: { input: { afterExec } }, configMeta: { fa: { icon: "fa-keyboard", @@ -219,7 +222,7 @@ describe("CommandlineUtils", () => { display: "custom test...", alias: "alias", input: true, - icon: "fa-keyboard", + fa: { icon: "fa-keyboard" }, }), ); }); @@ -409,18 +412,20 @@ describe("CommandlineUtils", () => { function buildCommand({ cmdMeta, configMeta, + actions, schema, key, }: { - cmdMeta: Partial>; + cmdMeta: Partial>; configMeta?: Partial>; + actions?: CommandlineConfigActions; schema?: ZodSchema; key?: K; }): Command { return buildCommandForConfigKey( key ?? ("" as any), - configMeta ?? ({} as any), - cmdMeta as any, + { ...configMeta, commandline: cmdMeta } as any, + actions, schema ?? z.string(), ); } diff --git a/frontend/__tests__/input/helpers/fail-or-finish.spec.ts b/frontend/__tests__/input/helpers/fail-or-finish.spec.ts index 14de4f3dba2c..0963a12ecf2f 100644 --- a/frontend/__tests__/input/helpers/fail-or-finish.spec.ts +++ b/frontend/__tests__/input/helpers/fail-or-finish.spec.ts @@ -19,9 +19,14 @@ vi.mock("../../../src/ts/utils/misc", async (importOriginal) => { }; }); -vi.mock("../../../src/ts/utils/strings", () => ({ - isSpace: vi.fn(), -})); +vi.mock("../../../src/ts/utils/strings", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + isSpace: vi.fn(), + }; +}); describe("checkIfFailedDueToMinBurst", () => { beforeEach(() => { diff --git a/frontend/src/ts/commandline/commandline-metadata.ts b/frontend/src/ts/commandline/commandline-metadata.ts deleted file mode 100644 index dc41e3c623bb..000000000000 --- a/frontend/src/ts/commandline/commandline-metadata.ts +++ /dev/null @@ -1,780 +0,0 @@ -import * as ConfigSchemas from "@monkeytype/schemas/configs"; -import * as SoundController from "../controllers/sound-controller"; -import * as TestLogic from "../test/test-logic"; -import { - getLanguageDisplayString, - replaceUnderscoresWithSpaces, -} from "../utils/strings"; - -import { areUnsortedArraysEqual } from "../utils/arrays"; -import { Config } from "../config/store"; -import { get as getTypingSpeedUnit } from "../utils/typing-speed-units"; -import { isAuthenticated } from "../states/core"; -import { getRoutePage } from "../states/router"; -import { Fonts } from "../constants/fonts"; -import { KnownFontName } from "@monkeytype/schemas/fonts"; -import { setFontPreview } from "../states/font"; -import { Validation } from "../types/validation"; -import { typedKeys } from "@monkeytype/util/objects"; - -//TODO: remove display property and instead use optionsMetadata from configMetadata -// eventually this file should be fully merged into config metadata, probably under the 'commandline' property - -type ConfigKeysWithoutCommands = - | "minWpmCustomSpeed" - | "minAccCustom" - | "minBurstCustomSpeed" - | "accountChart" - | "customThemeColors" - | "favThemes" - | "paceCaretCustomSpeed" - | "autoSwitchTheme" - | "themeLight" - | "themeDark" - | "burstHeatmap"; - -type SkippedConfigKeys = - | "minBurst" //this is skipped for now because it has 2 nested inputs; - | "customBackgroundFilter" //this is skipped for now because it has 4 nested inputs; - | "theme" //themes are sorted by color and also affected by config.favThemes - | "funbox"; //is using a special non schema command at the top to clear funboxes - -type CommandlineConfigKey = keyof Omit< - ConfigSchemas.Config, - ConfigKeysWithoutCommands | SkippedConfigKeys ->; - -export type CommandlineConfigMetadataObject = { - // oxlint-disable-next-line no-explicit-any - [K in CommandlineConfigKey]: CommandlineConfigMetadata; -}; - -export type InputProps = { - alias?: string; - display?: string; - afterExec?: (value: ConfigSchemas.Config[T]) => void; - defaultValue?: () => string; - /** - * default value for missing validation is `{schema:true}` - */ - validation?: Omit, "schema"> & { - schema?: true; - }; - hover?: () => void; - configValue?: ConfigSchemas.Config[T]; - inputValueConvert: ConfigSchemas.Config[T] extends string - ? ((val: string) => string) | undefined - : (val: string) => ConfigSchemas.Config[T]; -}; - -export type SecondaryInputProps = { - secondKey: T; -} & InputProps; - -export type CommandlineConfigMetadata< - T extends keyof ConfigSchemas.Config, - T2 extends keyof ConfigSchemas.Config, -> = { - alias?: string; - display?: string; - isVisible?: boolean; - // oxlint-disable-next-line no-generated-empty-object-type -- {} means "no input" - input?: InputProps | SecondaryInputProps | Record; - subgroup?: SubgroupProps; -}; - -export type SubgroupProps = { - alias?: (value: ConfigSchemas.Config[T]) => string; - display?: (value: ConfigSchemas.Config[T]) => string; - configValueMode?: (value: ConfigSchemas.Config[T]) => "include" | undefined; - isVisible?: (value: ConfigSchemas.Config[T]) => boolean; - isAvailable?: (value: ConfigSchemas.Config[T]) => (() => boolean) | undefined; - customData?: ( - value: ConfigSchemas.Config[T], - ) => Record; - hover?: (value: ConfigSchemas.Config[T]) => void; - afterExec?: (value: ConfigSchemas.Config[T]) => void; - options: "fromSchema" | ConfigSchemas.Config[T][]; -}; - -export const commandlineConfigMetadata: CommandlineConfigMetadataObject = { - //test - punctuation: { - subgroup: { - options: "fromSchema", - afterExec: () => { - void TestLogic.restartTest(); - }, - }, - }, - numbers: { - subgroup: { - options: "fromSchema", - afterExec: () => { - void TestLogic.restartTest(); - }, - }, - }, - words: { - alias: "words", - subgroup: { - options: [10, 25, 50, 100], - afterExec: () => { - void TestLogic.restartTest(); - }, - }, - input: { - inputValueConvert: Number, - - afterExec: () => { - void TestLogic.restartTest(); - }, - }, - }, - time: { - subgroup: { - options: [15, 30, 60, 120], - afterExec: () => { - void TestLogic.restartTest(); - }, - }, - input: { - inputValueConvert: Number, - afterExec: () => { - void TestLogic.restartTest(); - }, - }, - }, - mode: { - subgroup: { - options: "fromSchema", - afterExec: () => { - void TestLogic.restartTest(); - }, - }, - }, - quoteLength: { - alias: "quotes", - subgroup: { - options: [[0, 1, 2, 3], [0], [1], [2], [3], [-3]], - configValueMode: () => "include", - isAvailable: (value) => { - if (value[0] === -3) { - return isAuthenticated; - } - return undefined; - }, - display: (value) => { - if (areUnsortedArraysEqual(value, [0, 1, 2, 3])) { - return "all"; - } - - const map: Record = { - 0: "short", - 1: "medium", - 2: "long", - 3: "thicc", - "-3": "favorite", - }; - - return map[value[0] as number] as string; - }, - afterExec: () => { - void TestLogic.restartTest(); - }, - }, - }, - language: { - subgroup: { - options: "fromSchema", - display: (value) => { - return getLanguageDisplayString(value); - }, - }, - }, - //behavior - difficulty: { - subgroup: { - options: "fromSchema", - }, - }, - quickRestart: { - subgroup: { - options: "fromSchema", - }, - }, - repeatQuotes: { - subgroup: { - options: "fromSchema", - }, - }, - resultSaving: { - subgroup: { - options: "fromSchema", - alias: (val) => (val ? "enabled" : "disabled"), - }, - alias: "results practice incognito", - }, - blindMode: { - subgroup: { - options: "fromSchema", - }, - }, - alwaysShowWordsHistory: { - subgroup: { - options: "fromSchema", - }, - }, - singleListCommandLine: { - subgroup: { - options: "fromSchema", - }, - }, - minWpm: { - display: "Minimum speed...", - alias: "wpm", - subgroup: { - options: ["off"], - }, - input: { - configValue: "custom", - display: "custom...", - secondKey: "minWpmCustomSpeed", - inputValueConvert: (value) => { - let newVal = Number(value); - newVal = getTypingSpeedUnit(Config.typingSpeedUnit).toWpm(newVal); - return newVal; - }, - }, - }, - minAcc: { - display: "Minimum accuracy...", - subgroup: { - options: ["off"], - }, - input: { - configValue: "custom", - display: "custom...", - secondKey: "minAccCustom", - inputValueConvert: (value) => { - return Number(value); - }, - }, - }, - // minBurst: null, - britishEnglish: { - subgroup: { - options: "fromSchema", - afterExec: () => { - void TestLogic.restartTest(); - }, - }, - }, - // funbox: null, - customLayoutfluid: { - input: { - defaultValue: () => { - return Config.customLayoutfluid.join(" "); - }, - inputValueConvert: (val) => - val.trim().split(" ") as ConfigSchemas.CustomLayoutFluid, - }, - }, - customPolyglot: { - input: { - defaultValue: () => { - return Config.customPolyglot.join(" "); - }, - inputValueConvert: (val) => - val.trim().split(" ") as ConfigSchemas.CustomPolyglot, - afterExec: () => { - if (getRoutePage() === "test") { - void TestLogic.restartTest(); - } - }, - }, - }, - //input - freedomMode: { - subgroup: { - options: "fromSchema", - }, - }, - strictSpace: { - subgroup: { - options: "fromSchema", - }, - }, - oppositeShiftMode: { - subgroup: { - options: "fromSchema", - }, - }, - stopOnError: { - subgroup: { - options: "fromSchema", - }, - }, - deleteOnError: { - subgroup: { - options: "fromSchema", - display: (deleteOnError) => deleteOnError.replace(/_/g, " "), - }, - }, - confidenceMode: { - subgroup: { - options: "fromSchema", - }, - }, - quickEnd: { - subgroup: { - options: "fromSchema", - }, - }, - indicateTypos: { - subgroup: { - options: "fromSchema", - }, - }, - compositionDisplay: { - subgroup: { - options: "fromSchema", - }, - }, - hideExtraLetters: { - subgroup: { - options: "fromSchema", - }, - }, - lazyMode: { - subgroup: { - options: "fromSchema", - afterExec: () => void TestLogic.restartTest(), - }, - }, - layout: { - subgroup: { - options: "fromSchema", - display: (layout) => - layout === "default" ? "off" : layout.replace(/_/g, " "), - afterExec: () => void TestLogic.restartTest(), - }, - }, - codeUnindentOnBackspace: { - subgroup: { - options: "fromSchema", - }, - }, - //sound - soundVolume: { - subgroup: { - options: [0.1, 0.5, 1], - display: (val) => - new Map([ - [0.1, "quiet"], - [0.5, "medium"], - [1.0, "loud"], - ]).get(val) ?? "custom...", - afterExec: () => SoundController.playClick, - }, - input: { - inputValueConvert: Number, - }, - }, - playSoundOnClick: { - alias: "play", - display: "Sound on click...", - subgroup: { - options: "fromSchema", - display: (value) => { - const map: Record = { - off: "off", - "1": "click", - "2": "beep", - "3": "pop", - "4": "nk creams", - "5": "typewriter", - "6": "osu", - "7": "hitmarker", - "8": "sine", - "9": "sawtooth", - "10": "square", - "11": "triangle", - "12": "pentatonic", - "13": "wholetone", - "14": "fist fight", - "15": "rubber keys", - "16": "fart", - "17": "akko lavenders", - "18": "cherrymx black abs", - "19": "cherrymx black pbt", - "20": "cherrymx blue abs", - "21": "cherrymx blue pbt", - "22": "cherrymx brown pbt", - "23": "kalih box white", - "24": "razer green", - "25": "tealios v2", - "26": "trust gxt", - }; - return map[value]; - }, - hover: (value) => { - if (value === "off") { - return; - } - void SoundController.previewClick(value); - }, - afterExec: () => { - void SoundController.playClick(); - }, - }, - }, - playSoundOnError: { - alias: "play", - display: "Sound on error...", - subgroup: { - options: "fromSchema", - display: (value) => { - const map: Record = { - off: "off", - "1": "damage", - "2": "triangle", - "3": "square", - "4": "punch miss", - }; - return map[value]; - }, - hover: (value) => { - if (value === "off") { - return; - } - void SoundController.previewError(value); - }, - afterExec: () => { - void SoundController.playError(); - }, - }, - }, - playTimeWarning: { - alias: "sound", - subgroup: { - options: "fromSchema", - display: (value) => { - if (value === "off") { - return "off"; - } - return `${value} second${value !== "1" ? "s" : ""}`; - }, - afterExec: (value) => { - if (value !== "off") { - void SoundController.playTimeWarning(); - } - }, - hover: (value) => { - if (value !== "off") { - void SoundController.playTimeWarning(); - } - }, - }, - }, - //caret - smoothCaret: { - subgroup: { - options: "fromSchema", - }, - }, - caretStyle: { - subgroup: { - options: "fromSchema", - isVisible: (value) => !["banana", "carrot", "monkey"].includes(value), - }, - }, - paceCaret: { - display: "Pace caret mode...", - subgroup: { - options: ["off", "pb", "tagPb", "last", "average", "daily"], - afterExec: () => { - void TestLogic.restartTest(); - }, - }, - input: { - secondKey: "paceCaretCustomSpeed", - configValue: "custom", - inputValueConvert: (value) => { - let newVal = Number(value); - newVal = getTypingSpeedUnit(Config.typingSpeedUnit).toWpm(newVal); - return newVal; - }, - afterExec: () => { - void TestLogic.restartTest(); - }, - }, - }, - repeatedPace: { - subgroup: { - options: "fromSchema", - }, - }, - paceCaretStyle: { - subgroup: { - options: "fromSchema", - isVisible: (value) => !["banana", "carrot", "monkey"].includes(value), - }, - }, - - //appearence - liveSpeedStyle: { - subgroup: { - options: "fromSchema", - }, - alias: "wpm", - }, - liveAccStyle: { - subgroup: { - options: "fromSchema", - }, - alias: "wpm", - }, - liveBurstStyle: { - subgroup: { - options: "fromSchema", - }, - alias: "wpm", - }, - timerStyle: { - subgroup: { - options: "fromSchema", - display: replaceUnderscoresWithSpaces, - }, - alias: "timer", - }, - timerColor: { - display: "Live progress color...", - alias: "timer speed wpm burst acc", - subgroup: { - options: "fromSchema", - alias: () => "timer", - }, - }, - timerOpacity: { - display: "Live progress opacity...", - alias: "timer speed wpm burst acc", - subgroup: { - options: "fromSchema", - alias: () => "timer", - }, - }, - highlightMode: { - subgroup: { - options: "fromSchema", - display: replaceUnderscoresWithSpaces, - }, - }, - typedEffect: { - subgroup: { - options: "fromSchema", - }, - }, - tapeMode: { - subgroup: { - options: "fromSchema", - }, - }, - tapeMargin: { - input: { - inputValueConvert: Number, - }, - }, - smoothLineScroll: { - subgroup: { - options: "fromSchema", - }, - }, - showAllLines: { - subgroup: { - options: "fromSchema", - }, - }, - typingSpeedUnit: { - subgroup: { - options: "fromSchema", - isVisible: (val) => val !== "wph", - }, - }, - alwaysShowDecimalPlaces: { - subgroup: { - options: "fromSchema", - }, - }, - startGraphsAtZero: { - subgroup: { - options: "fromSchema", - }, - }, - maxLineWidth: { - input: { - alias: "page", - inputValueConvert: Number, - }, - }, - fontSize: { - input: { - inputValueConvert: Number, - }, - }, - fontFamily: { - subgroup: { - options: typedKeys(Fonts).sort((a, b) => - (Fonts[a]?.display ?? a.replace(/_/g, " ")).localeCompare( - Fonts[b]?.display ?? b.replace(/_/g, " "), - ), - ), - display: (name) => - Fonts[name as KnownFontName]?.display ?? name.replaceAll(/_/g, " "), - customData: (name) => { - const fontConfig = Fonts[name as KnownFontName]; - if (fontConfig === undefined) return {}; - return { - name: name.replaceAll(/_/g, " "), - isSystem: fontConfig.systemFont === true, - display: fontConfig.display, - } as Record; - }, - hover: (name) => setFontPreview(name), - }, - }, - keymapMode: { - alias: "keyboard", - subgroup: { - options: "fromSchema", - alias: (val) => (val === "react" ? "flash" : ""), - }, - }, - keymapStyle: { - subgroup: { - options: "fromSchema", - display: replaceUnderscoresWithSpaces, - }, - alias: "keyboard", - }, - keymapLegendStyle: { - subgroup: { - options: "fromSchema", - }, - alias: "keyboard", - }, - keymapSize: { - input: { - alias: "keyboard", - inputValueConvert: Number, - }, - }, - keymapLayout: { - alias: "keyboard", - subgroup: { - options: "fromSchema", - alias: (val) => (val === "overrideSync" ? "default" : ""), - display: (layout) => - layout === "overrideSync" ? "emulator sync" : layout.replace(/_/g, " "), - afterExec: () => void TestLogic.restartTest(), - }, - }, - keymapKeys: { - subgroup: { - options: "fromSchema", - display: replaceUnderscoresWithSpaces, - }, - alias: "keyboard", - }, - - //themes - customTheme: { - subgroup: { - options: "fromSchema", - }, - }, - flipTestColors: { - subgroup: { - options: "fromSchema", - }, - }, - colorfulMode: { - subgroup: { - options: "fromSchema", - }, - }, - customBackground: { - input: {}, - }, - customBackgroundSize: { - subgroup: { - options: "fromSchema", - }, - }, - randomTheme: { - subgroup: { - options: "fromSchema", - isAvailable: (value) => - value === "custom" ? isAuthenticated : undefined, - }, - }, - - //showhide - showKeyTips: { - subgroup: { - options: "fromSchema", - }, - display: "Key tips...", - }, - showOutOfFocusWarning: { - subgroup: { - options: "fromSchema", - }, - display: "Out of focus warning...", - }, - capsLockWarning: { - subgroup: { - options: "fromSchema", - }, - }, - showAverage: { - subgroup: { - options: "fromSchema", - }, - }, - showPb: { - subgroup: { - options: "fromSchema", - }, - alias: "pb", - }, - monkeyPowerLevel: { - alias: "powermode", - isVisible: false, - subgroup: { - options: "fromSchema", - display: (value) => { - const map: Record = { - off: "off", - "1": "mellow", - "2": "high", - "3": "ultra", - "4": "over 9000", - }; - return map[value]; - }, - }, - }, - monkey: { - subgroup: { - options: "fromSchema", - }, - }, - - //danger zone - ads: { - subgroup: { - options: "fromSchema", - }, - }, -}; diff --git a/frontend/src/ts/commandline/config-actions.ts b/frontend/src/ts/commandline/config-actions.ts new file mode 100644 index 000000000000..8c87a67e4f7b --- /dev/null +++ b/frontend/src/ts/commandline/config-actions.ts @@ -0,0 +1,131 @@ +import * as ConfigSchemas from "@monkeytype/schemas/configs"; +import * as SoundController from "../controllers/sound-controller"; +import * as TestLogic from "../test/test-logic"; +import { getRoutePage } from "../states/router"; +import { setFontPreview } from "../states/font"; + +// side effects of config commands, kept out of config metadata because the +// modules they call import the config setters (a circular import otherwise). +// the test restarts go away once the engine restarts on `changeRequiresRestart` + +export type CommandlineConfigActions = { + subgroup?: { + afterExec?: (value: ConfigSchemas.Config[K]) => void; + hover?: (value: ConfigSchemas.Config[K]) => void; + }; + input?: { + afterExec?: () => void; + hover?: () => void; + }; +}; + +type CommandlineConfigActionsObject = { + [K in keyof ConfigSchemas.Config]?: CommandlineConfigActions; +}; + +const restart = (): void => { + void TestLogic.restartTest(); +}; + +export const commandlineConfigActions: CommandlineConfigActionsObject = { + //test + punctuation: { + subgroup: { afterExec: restart }, + }, + numbers: { + subgroup: { afterExec: restart }, + }, + words: { + subgroup: { afterExec: restart }, + input: { afterExec: restart }, + }, + time: { + subgroup: { afterExec: restart }, + input: { afterExec: restart }, + }, + mode: { + subgroup: { afterExec: restart }, + }, + quoteLength: { + subgroup: { afterExec: restart }, + }, + britishEnglish: { + subgroup: { afterExec: restart }, + }, + customPolyglot: { + input: { + afterExec: () => { + if (getRoutePage() === "test") { + restart(); + } + }, + }, + }, + //input + lazyMode: { + subgroup: { afterExec: restart }, + }, + layout: { + subgroup: { afterExec: restart }, + }, + //sound + soundVolume: { + subgroup: { + afterExec: () => SoundController.playClick, + }, + }, + playSoundOnClick: { + subgroup: { + hover: (value) => { + if (value === "off") { + return; + } + void SoundController.previewClick(value); + }, + afterExec: () => { + void SoundController.playClick(); + }, + }, + }, + playSoundOnError: { + subgroup: { + hover: (value) => { + if (value === "off") { + return; + } + void SoundController.previewError(value); + }, + afterExec: () => { + void SoundController.playError(); + }, + }, + }, + playTimeWarning: { + subgroup: { + afterExec: (value) => { + if (value !== "off") { + void SoundController.playTimeWarning(); + } + }, + hover: (value) => { + if (value !== "off") { + void SoundController.playTimeWarning(); + } + }, + }, + }, + //caret + paceCaret: { + subgroup: { afterExec: restart }, + input: { afterExec: restart }, + }, + //appearance + fontFamily: { + subgroup: { + hover: (name) => setFontPreview(name), + }, + }, + keymapLayout: { + subgroup: { afterExec: restart }, + }, +}; diff --git a/frontend/src/ts/commandline/lists.ts b/frontend/src/ts/commandline/lists.ts index 2ed23b7eaa6a..ab788b2bfe4a 100644 --- a/frontend/src/ts/commandline/lists.ts +++ b/frontend/src/ts/commandline/lists.ts @@ -25,7 +25,7 @@ import { } from "../states/notifications"; import { Command, CommandlineListKey, CommandsSubgroup } from "./types"; import { buildCommandForConfigKey } from "./util"; -import { CommandlineConfigMetadataObject } from "./commandline-metadata"; +import { CommandlineConfigKey } from "../config/metadata"; import { isAuthAvailable, signOut } from "../firebase"; import { isAuthenticated } from "../states/core"; import { ConfigKey } from "@monkeytype/schemas/configs"; @@ -58,7 +58,7 @@ export const commands: CommandsSubgroup = { { id: "changeCustomModeText", display: "Change custom text", - icon: "fa-align-left", + fa: { icon: "fa-align-left" }, exec: (): void => { showModal("CustomText"); }, @@ -66,7 +66,7 @@ export const commands: CommandsSubgroup = { { id: "viewQuoteSearchPopup", display: "Search for quotes", - icon: "fa-search", + fa: { icon: "fa-search" }, exec: (): void => { setConfig("mode", "quote"); showModal("QuoteSearch"); @@ -78,7 +78,7 @@ export const commands: CommandsSubgroup = { { id: "shareTestSettings", display: "Share test settings", - icon: "fa-share", + fa: { icon: "fa-share" }, exec: (): void => { showModal("ShareTestSettings"); }, @@ -188,7 +188,7 @@ export const commands: CommandsSubgroup = { { id: "randomizeTheme", display: "Next random theme", - icon: "fa-random", + fa: { icon: "fa-random" }, exec: async (): Promise => randomizeTheme(), available: (): boolean => { return Config.randomTheme !== "off"; @@ -215,7 +215,7 @@ export const commands: CommandsSubgroup = { { id: "importSettingsJSON", display: "Import settings JSON", - icon: "fa-cog", + fa: { icon: "fa-cog" }, alias: "import config", input: true, exec: async ({ input }): Promise => { @@ -226,7 +226,7 @@ export const commands: CommandsSubgroup = { { id: "exportSettingsJSON", display: "Export settings JSON", - icon: "fa-cog", + fa: { icon: "fa-cog" }, alias: "export config", input: true, defaultValue: (): string => { @@ -236,7 +236,7 @@ export const commands: CommandsSubgroup = { { id: "clearNotifications", display: "Clear all notifications", - icon: "fa-trash-alt", + fa: { icon: "fa-trash-alt" }, alias: "dismiss", exec: async (): Promise => { clearAllNotifications(); @@ -245,7 +245,7 @@ export const commands: CommandsSubgroup = { { id: "clearSwCache", display: "Clear SW cache", - icon: "fa-cog", + fa: { icon: "fa-cog" }, exec: async (): Promise => { const clist = await caches.keys(); for (const name of clist) { @@ -257,7 +257,7 @@ export const commands: CommandsSubgroup = { { id: "getSwCache", display: "Get SW cache", - icon: "fa-cog", + fa: { icon: "fa-cog" }, exec: async (): Promise => { alert(await caches.keys()); }, @@ -266,7 +266,7 @@ export const commands: CommandsSubgroup = { id: "copyResultStats", display: "Copy last event log (result data)", alias: "stats events", - icon: "fa-cog", + fa: { icon: "fa-cog" }, visible: false, available: (): boolean => { return getLastEventLog() !== null; @@ -285,7 +285,7 @@ export const commands: CommandsSubgroup = { { id: "fpsCounter", display: "FPS counter...", - icon: "fa-cog", + fa: { icon: "fa-cog" }, visible: false, subgroup: { title: "FPS counter...", @@ -293,7 +293,7 @@ export const commands: CommandsSubgroup = { { id: "startFpsCounter", display: "show", - icon: "fa-cog", + fa: { icon: "fa-cog" }, exec: (): void => { showFpsCounter(); }, @@ -301,7 +301,7 @@ export const commands: CommandsSubgroup = { { id: "stopFpsCounter", display: "hide", - icon: "fa-cog", + fa: { icon: "fa-cog" }, exec: (): void => { hideFpsCounter(); }, @@ -312,7 +312,7 @@ export const commands: CommandsSubgroup = { { id: "fixSkillIssue", display: "Fix skill issue", - icon: "fa-wrench", + fa: { icon: "fa-wrench" }, visible: false, exec: (): void => { fixSkillIssue(); @@ -321,7 +321,7 @@ export const commands: CommandsSubgroup = { { id: "joinDiscord", display: "Join the Discord server", - icon: "fa-users", + fa: { icon: "fa-users" }, exec: (): void => { window.open("https://discord.gg/monkeytype"); }, @@ -329,7 +329,7 @@ export const commands: CommandsSubgroup = { { id: "signOut", display: "Sign out", - icon: "fa-sign-out-alt", + fa: { icon: "fa-sign-out-alt" }, exec: (): void => { void signOut(); }, @@ -462,7 +462,7 @@ function buildSingleListCommands( singleListParentDisplay: parentCommandDisplay, singleListDisplayNoIcon, configKey: parentCommand.subgroup?.configKey, - icon: parentCommand.icon, + fa: parentCommand.fa, alias: newAlias, visible: (parentCommand.visible ?? true) && (command.visible ?? true), available: async (): Promise => { @@ -481,7 +481,7 @@ function buildSingleListCommands( } function buildCommands( - ...commands: (Command | keyof CommandlineConfigMetadataObject)[] + ...commands: (Command | CommandlineConfigKey)[] ): Command[] { return commands.map((it) => typeof it === "string" ? buildCommandForConfigKey(it) : it, diff --git a/frontend/src/ts/commandline/lists/add-or-remove-theme-to-favorites.ts b/frontend/src/ts/commandline/lists/add-or-remove-theme-to-favorites.ts index c687c693e142..59b7a8abca93 100644 --- a/frontend/src/ts/commandline/lists/add-or-remove-theme-to-favorites.ts +++ b/frontend/src/ts/commandline/lists/add-or-remove-theme-to-favorites.ts @@ -8,7 +8,7 @@ const commands: Command[] = [ { id: "addThemeToFavorite", display: "Add current theme to favorite", - icon: "fa-star", + fa: { icon: "fa-star" }, available: (): boolean => { return ( !Config.customTheme && @@ -26,8 +26,7 @@ const commands: Command[] = [ { id: "removeThemeFromFavorite", display: "Remove current theme from favorite", - icon: "fa-star", - iconType: "regular", + fa: { icon: "fa-star", variant: "regular" }, available: (): boolean => { return ( !Config.customTheme && diff --git a/frontend/src/ts/commandline/lists/background-filter.ts b/frontend/src/ts/commandline/lists/background-filter.ts index 01bd3ecfdc02..7629d3f546e0 100644 --- a/frontend/src/ts/commandline/lists/background-filter.ts +++ b/frontend/src/ts/commandline/lists/background-filter.ts @@ -9,7 +9,7 @@ const subgroup: CommandsSubgroup = { { id: "setCustomBackgroundBlur", display: "blur...", - icon: "fa-image", + fa: { icon: "fa-image" }, input: true, defaultValue: (): string => { return Config.customBackgroundFilter[0].toString(); @@ -24,7 +24,7 @@ const subgroup: CommandsSubgroup = { { id: "setCustomBackgroundBrightness", display: "brightness...", - icon: "fa-image", + fa: { icon: "fa-image" }, input: true, defaultValue: (): string => { return Config.customBackgroundFilter[1].toString(); @@ -39,7 +39,7 @@ const subgroup: CommandsSubgroup = { { id: "setCustomBackgroundSaturation", display: "saturation...", - icon: "fa-image", + fa: { icon: "fa-image" }, input: true, defaultValue: (): string => { return Config.customBackgroundFilter[2].toString(); @@ -54,7 +54,7 @@ const subgroup: CommandsSubgroup = { { id: "setCustomBackgroundOpacity", display: "opacity...", - icon: "fa-image", + fa: { icon: "fa-image" }, input: true, defaultValue: (): string => { return Config.customBackgroundFilter[3].toString(); @@ -73,7 +73,7 @@ const commands: Command[] = [ { id: "setCustomBackgroundFilter", display: "Custom background filter...", - icon: "fa-image", + fa: { icon: "fa-image" }, subgroup, }, ]; diff --git a/frontend/src/ts/commandline/lists/bail-out.ts b/frontend/src/ts/commandline/lists/bail-out.ts index f604d425e096..a2e27361df23 100644 --- a/frontend/src/ts/commandline/lists/bail-out.ts +++ b/frontend/src/ts/commandline/lists/bail-out.ts @@ -52,7 +52,7 @@ const commands: Command[] = [ { id: "bailOut", display: "Bail out...", - icon: "fa-running", + fa: { icon: "fa-running" }, subgroup, visible: false, available: (): boolean => { diff --git a/frontend/src/ts/commandline/lists/custom-background.ts b/frontend/src/ts/commandline/lists/custom-background.ts index b7db03d0a02c..bdd0e6092d91 100644 --- a/frontend/src/ts/commandline/lists/custom-background.ts +++ b/frontend/src/ts/commandline/lists/custom-background.ts @@ -10,7 +10,7 @@ const fromMeta = buildCommandForConfigKey("customBackground"); const customBackgroundCommand: Command = { id: "customBackground", display: "Custom background...", - icon: "fa-image", + fa: { icon: "fa-image" }, subgroup: { title: "Custom background...", list: [ @@ -18,7 +18,7 @@ const customBackgroundCommand: Command = { { id: "customLocalBackground", display: "Local background...", - icon: "fa-file-import fa-fw", + fa: { icon: "fa-file-import" }, alias: "upload background", available: async (): Promise => { return !(await FileStorage.hasFile("LocalBackgroundFile")); @@ -69,7 +69,7 @@ const customBackgroundCommand: Command = { { id: "removeCustomBackground", display: "Remove custom background", - icon: "fa-trash", + fa: { icon: "fa-trash" }, available: async (): Promise => { return ( (await FileStorage.hasFile("LocalBackgroundFile")) || diff --git a/frontend/src/ts/commandline/lists/custom-themes-list.ts b/frontend/src/ts/commandline/lists/custom-themes-list.ts index f5f9e574278b..4e09ec1f7eab 100644 --- a/frontend/src/ts/commandline/lists/custom-themes-list.ts +++ b/frontend/src/ts/commandline/lists/custom-themes-list.ts @@ -15,7 +15,7 @@ const commands: Command[] = [ { id: "setCustomThemeId", display: "Custom themes...", - icon: "fa-palette", + fa: { icon: "fa-palette" }, subgroup, available: (): boolean => { return isAuthenticated(); diff --git a/frontend/src/ts/commandline/lists/font-family.ts b/frontend/src/ts/commandline/lists/font-family.ts index 39b3c94f520a..022a418acce9 100644 --- a/frontend/src/ts/commandline/lists/font-family.ts +++ b/frontend/src/ts/commandline/lists/font-family.ts @@ -13,7 +13,7 @@ if (fromMeta.subgroup) { fromMeta.subgroup.list.push({ id: "customFont", display: "Custom font...", - icon: "fa-font", + fa: { icon: "fa-font" }, alias: "custom font options", subgroup: { title: "Custom font...", @@ -21,7 +21,7 @@ if (fromMeta.subgroup) { withValidation({ id: "customFontName", display: "Custom name...", - icon: "fa-font", + fa: { icon: "fa-font" }, alias: "custom font name", input: true, validation: { @@ -39,7 +39,7 @@ if (fromMeta.subgroup) { { id: "customLocalFont", display: "Local font...", - icon: "fa-file-import fa-fw", + fa: { icon: "fa-file-import" }, alias: "upload font", available: async (): Promise => { return !(await FileStorage.hasFile("LocalFontFamilyFile")); @@ -95,7 +95,7 @@ if (fromMeta.subgroup) { { id: "removeLocalFont", display: "Remove local font", - icon: "fa-trash", + fa: { icon: "fa-trash" }, alias: "remove font", available: async (): Promise => { return await FileStorage.hasFile("LocalFontFamilyFile"); diff --git a/frontend/src/ts/commandline/lists/funbox.ts b/frontend/src/ts/commandline/lists/funbox.ts index 3fa0f0a56540..50ad30268655 100644 --- a/frontend/src/ts/commandline/lists/funbox.ts +++ b/frontend/src/ts/commandline/lists/funbox.ts @@ -50,7 +50,7 @@ const commands: Command[] = [ id: "changeFunbox", display: "Funbox...", alias: "fun box", - icon: "fa-gamepad", + fa: { icon: "fa-gamepad" }, subgroup, }, ]; diff --git a/frontend/src/ts/commandline/lists/load-challenge.ts b/frontend/src/ts/commandline/lists/load-challenge.ts index b0bbf0657525..97f5012a08b6 100644 --- a/frontend/src/ts/commandline/lists/load-challenge.ts +++ b/frontend/src/ts/commandline/lists/load-challenge.ts @@ -26,7 +26,7 @@ const commands: Command[] = [ { id: "loadChallenge", display: "Load challenge...", - icon: "fa-award", + fa: { icon: "fa-award" }, subgroup, }, ]; diff --git a/frontend/src/ts/commandline/lists/min-burst.ts b/frontend/src/ts/commandline/lists/min-burst.ts index 1152e476c68e..a2cabac11c1d 100644 --- a/frontend/src/ts/commandline/lists/min-burst.ts +++ b/frontend/src/ts/commandline/lists/min-burst.ts @@ -51,7 +51,7 @@ const commands: Command[] = [ id: "changeMinBurst", display: "Minimum word burst...", alias: "minimum", - icon: "fa-bomb", + fa: { icon: "fa-bomb" }, subgroup, }, ]; diff --git a/frontend/src/ts/commandline/lists/navigation.ts b/frontend/src/ts/commandline/lists/navigation.ts index cfb0952c316a..46f5d92896f5 100644 --- a/frontend/src/ts/commandline/lists/navigation.ts +++ b/frontend/src/ts/commandline/lists/navigation.ts @@ -11,7 +11,7 @@ const commands: Command[] = [ id: "viewTypingPage", display: "View Typing Page", alias: "navigate go to start begin type test", - icon: "fa-keyboard", + fa: { icon: "fa-keyboard" }, exec: (): void => { void navigate({ to: "/" }); }, @@ -20,7 +20,7 @@ const commands: Command[] = [ id: "viewLeaderboards", display: "View Leaderboards", alias: "navigate go to", - icon: "fa-crown", + fa: { icon: "fa-crown" }, exec: (): void => { void navigate({ to: "/leaderboards" }); }, @@ -29,7 +29,7 @@ const commands: Command[] = [ id: "viewAbout", display: "View About Page", alias: "navigate go to", - icon: "fa-info", + fa: { icon: "fa-info" }, exec: (): void => { void navigate({ to: "/about" }); }, @@ -38,7 +38,7 @@ const commands: Command[] = [ id: "viewSettings", display: "View Settings Page", alias: "navigate go to", - icon: "fa-cog", + fa: { icon: "fa-cog" }, exec: (): void => { void navigate({ to: "/settings" }); }, @@ -48,7 +48,7 @@ const commands: Command[] = [ id: "viewAccount", display: "View Account Page", alias: "navigate go to stats", - icon: "fa-user", + fa: { icon: "fa-user" }, exec: (): void => { void navigate({ to: isAuthenticated() ? "/account" : "/login" }); }, @@ -57,7 +57,7 @@ const commands: Command[] = [ id: "searchProfile", display: "Search for a profile", alias: "profile user search find lookup", - icon: "fa-search", + fa: { icon: "fa-search" }, input: true, validation: { schema: UserNameWithoutFilterSchema, @@ -80,7 +80,7 @@ const commands: Command[] = [ { id: "toggleFullscreen", display: "Toggle Fullscreen", - icon: "fa-expand", + fa: { icon: "fa-expand" }, exec: (): void => { toggleFullscreen(); }, diff --git a/frontend/src/ts/commandline/lists/presets.ts b/frontend/src/ts/commandline/lists/presets.ts index 017d0000d99c..db7c53346cbc 100644 --- a/frontend/src/ts/commandline/lists/presets.ts +++ b/frontend/src/ts/commandline/lists/presets.ts @@ -17,7 +17,7 @@ const commands: Command[] = [ visible: false, id: "applyPreset", display: "Presets...", - icon: "fa-sliders-h", + fa: { icon: "fa-sliders-h" }, subgroup, available: (): boolean => { return isAuthenticated(); @@ -41,7 +41,7 @@ function update(): void { subgroup.list.push({ id: "createPreset", display: "Create preset", - icon: "fa-plus", + fa: { icon: "fa-plus" }, exec: (): void => { showModal("AddPresetModal"); }, diff --git a/frontend/src/ts/commandline/lists/quote-favorites.ts b/frontend/src/ts/commandline/lists/quote-favorites.ts index 6ef9752a4506..de34fa1c8660 100644 --- a/frontend/src/ts/commandline/lists/quote-favorites.ts +++ b/frontend/src/ts/commandline/lists/quote-favorites.ts @@ -13,7 +13,7 @@ const commands: Command[] = [ { id: "addQuoteToFavorite", display: "Add current quote to favorite", - icon: "fa-heart", + fa: { icon: "fa-heart" }, available: (): boolean => { const quote = getCurrentQuote(); return ( @@ -41,7 +41,7 @@ const commands: Command[] = [ { id: "removeQuoteFromFavorite", display: "Remove current quote from favorite", - icon: "fa-heart-broken", + fa: { icon: "fa-heart-broken" }, available: (): boolean => { const quote = getCurrentQuote(); return ( diff --git a/frontend/src/ts/commandline/lists/result-screen.ts b/frontend/src/ts/commandline/lists/result-screen.ts index b7e3b8766d50..e1314e919c4f 100644 --- a/frontend/src/ts/commandline/lists/result-screen.ts +++ b/frontend/src/ts/commandline/lists/result-screen.ts @@ -65,7 +65,7 @@ const commands: Command[] = [ id: "nextTest", display: "Next test", alias: "restart start begin type test typing", - icon: "fa-chevron-right", + fa: { icon: "fa-chevron-right" }, available: (): boolean => { return isTestFinished(); }, @@ -76,7 +76,7 @@ const commands: Command[] = [ { id: "repeatTest", display: "Repeat test", - icon: "fa-sync-alt", + fa: { icon: "fa-sync-alt" }, exec: (): void => { void TestLogic.restartTest({ withSameWordset: true, @@ -89,7 +89,7 @@ const commands: Command[] = [ { id: "practiseWords", display: "Practice words...", - icon: "fa-exclamation-triangle", + fa: { icon: "fa-exclamation-triangle" }, subgroup: practiceSubgroup, available: (): boolean => { return isTestFinished(); @@ -98,7 +98,7 @@ const commands: Command[] = [ { id: "toggleWordHistory", display: "Toggle word history", - icon: "fa-align-left", + fa: { icon: "fa-align-left" }, exec: (): void => { toggleResultWords(); }, @@ -109,7 +109,7 @@ const commands: Command[] = [ { id: "copyScreenshot", display: "Copy screenshot to clipboard", - icon: "fa-copy", + fa: { icon: "fa-copy" }, alias: "copy image clipboard", exec: (): void => { setTimeout(() => { @@ -123,7 +123,7 @@ const commands: Command[] = [ { id: "downloadScreenshot", display: "Download screenshot", - icon: "fa-download", + fa: { icon: "fa-download" }, alias: "save image download file", exec: (): void => { setTimeout(async () => { @@ -137,7 +137,7 @@ const commands: Command[] = [ { id: "copyWordsToClipboard", display: "Copy words to clipboard", - icon: "fa-copy", + fa: { icon: "fa-copy" }, exec: (): void => { const eventLog = getLastEventLog(); if (eventLog === null) { diff --git a/frontend/src/ts/commandline/lists/tags.ts b/frontend/src/ts/commandline/lists/tags.ts index f63ee1b1a3a1..a70e5e48536b 100644 --- a/frontend/src/ts/commandline/lists/tags.ts +++ b/frontend/src/ts/commandline/lists/tags.ts @@ -21,7 +21,7 @@ const commands: Command[] = [ { id: "changeTags", display: "Tags...", - icon: "fa-tag", + fa: { icon: "fa-tag" }, subgroup, available: (): boolean => { return isAuthenticated(); @@ -37,7 +37,7 @@ function update(): void { subgroup.list.push({ id: "clearTags", display: `Clear tags`, - icon: "fa-times", + fa: { icon: "fa-times" }, sticky: true, exec: async (): Promise => { await clearActiveTags(); @@ -76,7 +76,7 @@ function update(): void { subgroup.list.push({ id: "createTag", display: "Create tag", - icon: "fa-plus", + fa: { icon: "fa-plus" }, shouldFocusTestUI: false, opensModal: true, exec: (): void => { diff --git a/frontend/src/ts/commandline/lists/themes.ts b/frontend/src/ts/commandline/lists/themes.ts index 157be86f1868..d6642defe5ff 100644 --- a/frontend/src/ts/commandline/lists/themes.ts +++ b/frontend/src/ts/commandline/lists/themes.ts @@ -60,7 +60,7 @@ const commands: Command[] = [ { id: "changeTheme", display: "Theme...", - icon: "fa-palette", + fa: { icon: "fa-palette" }, subgroup, }, ]; diff --git a/frontend/src/ts/commandline/types.ts b/frontend/src/ts/commandline/types.ts index a73ca6167fc9..7e056d45eba9 100644 --- a/frontend/src/ts/commandline/types.ts +++ b/frontend/src/ts/commandline/types.ts @@ -1,5 +1,6 @@ import { Config, ConfigKey } from "@monkeytype/schemas/configs"; import { Validation } from "../types/validation"; +import { FaObject } from "../types/font-awesome"; // this file is needed becauase otherwise it would produce a circular dependency @@ -15,8 +16,7 @@ export type Command = { singleListDisplayNoIcon?: string; subgroup?: CommandsSubgroup; found?: boolean; - icon?: string; - iconType?: "regular" | "solid"; + fa?: FaObject; sticky?: boolean; alias?: string; input?: boolean; diff --git a/frontend/src/ts/commandline/util.ts b/frontend/src/ts/commandline/util.ts index 75b123c6ecf0..8a928c2e38ee 100644 --- a/frontend/src/ts/commandline/util.ts +++ b/frontend/src/ts/commandline/util.ts @@ -1,14 +1,18 @@ import { Config } from "../config/store"; import { setConfig } from "../config/setters"; -import { ConfigMetadata, configMetadata } from "../config/metadata"; +import { + CommandlineConfigKey, + CommandlineInputMetadata, + CommandlineMetadata, + CommandlineSubgroupMetadata, + ConfigMetadata, + configMetadata, +} from "../config/metadata"; import { capitalizeFirstLetter } from "../utils/strings"; import { - CommandlineConfigMetadata, - commandlineConfigMetadata, - CommandlineConfigMetadataObject, - InputProps, - SubgroupProps, -} from "./commandline-metadata"; + CommandlineConfigActions, + commandlineConfigActions, +} from "./config-actions"; import { Command } from "./types"; import * as ConfigSchemas from "@monkeytype/schemas/configs"; import { ZodSchema, ZodFirstPartySchemaTypes } from "zod"; @@ -16,24 +20,22 @@ import { getOptions } from "../utils/zod"; export function buildCommandForConfigKey< // oxlint-disable-next-line no-unnecessary-type-parameters -- keeps key and its metadata types correlated - K extends keyof CommandlineConfigMetadataObject, + K extends CommandlineConfigKey, >(key: K): Command { const configMeta = configMetadata[key]; - const commandMeta = commandlineConfigMetadata[key]; + const actions = commandlineConfigActions[key]; const schema = ConfigSchemas.ConfigSchema.shape[key]; - return _buildCommandForConfigKey(key, configMeta, commandMeta, schema); + return _buildCommandForConfigKey(key, configMeta, actions, schema); } -function _buildCommandForConfigKey< - K extends keyof CommandlineConfigMetadataObject, ->( +function _buildCommandForConfigKey( key: K, configMeta: ConfigMetadata, - commandMeta: - | CommandlineConfigMetadata - | undefined, + actions: CommandlineConfigActions | undefined, schema: ZodSchema, ): Command { + const commandMeta: CommandlineMetadata | null | undefined = + configMeta.commandline; if (commandMeta === undefined || commandMeta === null) { throw new Error(`No commandline metadata found for config key "${key}".`); } @@ -46,6 +48,7 @@ function _buildCommandForConfigKey< commandMeta.display, commandMeta.alias, commandMeta.subgroup, + actions?.subgroup, configMeta, schema, ); @@ -57,7 +60,10 @@ function _buildCommandForConfigKey< const inputCommand = buildInputCommand({ key: "secondKey" in inputProps ? inputProps.secondKey : key, isPartOfSubgroup: "subgroup" in commandMeta, - inputProps: inputProps as InputProps, + inputProps: inputProps as CommandlineInputMetadata< + keyof ConfigSchemas.Config + >, + inputActions: actions?.input, configMeta: configMeta as unknown as ConfigMetadata< keyof ConfigSchemas.Config >, @@ -86,7 +92,8 @@ function buildCommandWithSubgroup( key: K, rootDisplay: string | undefined, rootAlias: string | undefined, - subgroupProps: SubgroupProps, + subgroupProps: CommandlineSubgroupMetadata, + subgroupActions: CommandlineConfigActions["subgroup"], configMeta: ConfigMetadata, schema: ZodSchema, ): Command { @@ -111,7 +118,7 @@ function buildCommandWithSubgroup( ); } const list = values.map((value) => - buildSubgroupCommand(key, value, subgroupProps), + buildSubgroupCommand(key, value, subgroupProps, subgroupActions), ); list.sort((a, b) => { @@ -123,7 +130,7 @@ function buildCommandWithSubgroup( return { id: `change${capitalizeFirstLetter(key)}`, display: display, - icon: configMeta?.fa?.icon ?? "fa-cog", + fa: configMeta?.fa ?? { icon: "fa-cog" }, subgroup: { title: display, configKey: key, @@ -137,15 +144,14 @@ function buildSubgroupCommand( key: keyof ConfigSchemas.Config, value: ConfigSchemas.Config[K], { - afterExec, - hover, display: commandDisplay, alias: commandAlias, configValueMode: commandConfigValueMode, isVisible: isCommandVisible, isAvailable: isCommandAvailable, customData: commandCustomData, - }: SubgroupProps, + }: CommandlineSubgroupMetadata, + { afterExec, hover }: CommandlineConfigActions["subgroup"] = {}, ): Command { const val = value; @@ -189,12 +195,14 @@ function buildInputCommand({ key, isPartOfSubgroup, inputProps, + inputActions, configMeta, schema, }: { key: K; isPartOfSubgroup: boolean; - inputProps?: InputProps; + inputProps?: CommandlineInputMetadata; + inputActions?: CommandlineConfigActions["input"]; configMeta: ConfigMetadata; schema?: ZodSchema; }): Command { @@ -217,15 +225,15 @@ function buildInputCommand({ display: displayString, alias: inputProps?.alias ?? undefined, input: true, - icon: configMeta?.fa?.icon ?? "fa-cog", + fa: configMeta?.fa ?? { icon: "fa-cog" }, //@ts-expect-error this is fine exec: ({ input }): void => { if (input === undefined) return; setConfig(key, input as ConfigSchemas.Config[K]); - inputProps?.afterExec?.(input as ConfigSchemas.Config[K]); + inputActions?.afterExec?.(); }, - hover: inputProps?.hover, + hover: inputActions?.hover, }; if (inputProps?.inputValueConvert !== undefined) { diff --git a/frontend/src/ts/components/modals/CommandlineModal.tsx b/frontend/src/ts/components/modals/CommandlineModal.tsx index 84c9fdadfe71..b962208f2169 100644 --- a/frontend/src/ts/components/modals/CommandlineModal.tsx +++ b/frontend/src/ts/components/modals/CommandlineModal.tsx @@ -57,11 +57,6 @@ const MAX_RENDERED_COMMANDS = 1000; type CommandlineMode = "search" | "input"; -/** `Command.icon` and `Command.iconType` are plain strings, so they need widening to the `Fa` props union. */ -function faProps(icon: string, variant?: "regular" | "solid"): FaObject { - return { icon, variant } as FaObject; -} - function isCommandActive( command: Command, subgroup: CommandsSubgroup, @@ -116,9 +111,6 @@ function CommandIcon(props: { props.command.configValue !== undefined) || props.command.active !== undefined); - const icon = (): string => props.command.icon ?? ""; - const isTextIcon = (): boolean => icon() !== "" && !icon().startsWith("fa-"); - return ( } > - - } - > -
- {icon()} -
-
+
); } @@ -195,7 +170,7 @@ export function CommandlineModal(): JSXElement { const [inputPlaceholder, setInputPlaceholder] = createSignal( null, ); - const [inputIcon, setInputIcon] = createSignal(null); + const [inputIcon, setInputIcon] = createSignal(null); const [warning, setWarning] = createSignal(null); const [isChecking, setIsChecking] = createSignal(false); const [hiddenCount, setHiddenCount] = createSignal(0); @@ -392,7 +367,7 @@ export function CommandlineModal(): JSXElement { setMode("input"); setInputCommand(command); setInputPlaceholder(command.display); - setInputIcon(command.icon ?? "fa-chevron-right"); + setInputIcon(command.fa ?? { icon: "fa-chevron-right" }); validation = undefined; createValidationHandler(command); applyInputValue(command.defaultValue?.() ?? "", true); @@ -778,11 +753,9 @@ export function CommandlineModal(): JSXElement { when={isChecking()} fallback={ diff --git a/frontend/src/ts/config/metadata.tsx b/frontend/src/ts/config/metadata.tsx index 58f977094238..d232d7cefb96 100644 --- a/frontend/src/ts/config/metadata.tsx +++ b/frontend/src/ts/config/metadata.tsx @@ -1,21 +1,32 @@ import { checkCompatibility } from "@monkeytype/funbox"; import * as ConfigSchemas from "@monkeytype/schemas/configs"; +import { KnownFontName } from "@monkeytype/schemas/fonts"; import { roundTo1 } from "@monkeytype/util/numbers"; +import { typedKeys } from "@monkeytype/util/objects"; import { JSXElement } from "solid-js"; import * as CustomThemes from "../collections/custom-themes"; import { getDefaultConfig } from "../constants/default-config"; +import { Fonts } from "../constants/fonts"; import { isAuthenticated } from "../states/core"; import { showNoticeNotification } from "../states/notifications"; import { FaObject } from "../types/font-awesome"; +import { Validation } from "../types/validation"; +import { areUnsortedArraysEqual } from "../utils/arrays"; import { isDevEnvironment } from "../utils/env"; import { reloadAfter } from "../utils/misc"; -import { capitalizeFirstLetter } from "../utils/strings"; +import { + capitalizeFirstLetter, + getLanguageDisplayString, + replaceUnderscoresWithSpaces, +} from "../utils/strings"; +import { get as getTypingSpeedUnit } from "../utils/typing-speed-units"; import { getOptions } from "../utils/zod"; import { canSetFunboxWithConfig, getFunboxUnsupportedReason, } from "./funbox-validation"; +import { Config } from "./store"; // type SetBlock = { // [K in keyof ConfigSchemas.Config]?: ConfigSchemas.Config[K][]; // }; @@ -30,6 +41,60 @@ export type OptionMetadata = { visible?: boolean; }; +export type CommandlineInputMetadata = { + alias?: string; + display?: string; + defaultValue?: () => string; + /** + * default value for missing validation is `{schema:true}` + */ + validation?: Omit, "schema"> & { + schema?: true; + }; + configValue?: ConfigSchemas.Config[K]; + inputValueConvert: ConfigSchemas.Config[K] extends string + ? ((val: string) => string) | undefined + : (val: string) => ConfigSchemas.Config[K]; +}; + +export type CommandlineSecondaryInputMetadata< + K extends keyof ConfigSchemas.Config, +> = { + secondKey: K; +} & CommandlineInputMetadata; + +export type CommandlineSubgroupMetadata = + { + alias?: (value: ConfigSchemas.Config[K]) => string; + display?: (value: ConfigSchemas.Config[K]) => string; + configValueMode?: (value: ConfigSchemas.Config[K]) => "include" | undefined; + isVisible?: (value: ConfigSchemas.Config[K]) => boolean; + isAvailable?: ( + value: ConfigSchemas.Config[K], + ) => (() => boolean) | undefined; + customData?: ( + value: ConfigSchemas.Config[K], + ) => Record; + options: "fromSchema" | ConfigSchemas.Config[K][]; + }; + +// side effects (afterExec, hover) live in commandline/config-actions +//TODO: the display strings duplicate optionsMetadata and displayString in places, unify them +export type CommandlineMetadata< + K extends keyof ConfigSchemas.Config, + K2 extends keyof ConfigSchemas.Config = keyof ConfigSchemas.Config, +> = { + alias?: string; + display?: string; + isVisible?: boolean; + input?: + | CommandlineInputMetadata + | CommandlineSecondaryInputMetadata + // oxlint-disable-next-line no-generated-empty-object-type -- {} means "no input" + | Record; + subgroup?: CommandlineSubgroupMetadata; +}; + export type ConfigMetadata = { /** * The config key that this metadata is for @@ -114,6 +179,11 @@ export type ConfigMetadata = { nosave: boolean; currentConfig: Readonly; }) => void; + /** + * Commandline command generated for this config key. `null` when the key has no generated command (set elsewhere, or a hand-built list). + */ + // oxlint-disable-next-line no-explicit-any -- any secondKey, each typed by its own value + commandline: CommandlineMetadata | null; }; export type ConfigMetadataObject = { @@ -139,7 +209,7 @@ const caretOptionsMetadata = { outline: {}, underline: {}, }; -export const configMetadata: ConfigMetadataObject = { +const configMetadataEntries = { // test punctuation: { key: "punctuation", @@ -154,6 +224,11 @@ export const configMetadata: ConfigMetadataObject = { } return value; }, + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, numbers: { key: "numbers", @@ -168,6 +243,11 @@ export const configMetadata: ConfigMetadataObject = { } return value; }, + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, words: { key: "words", @@ -183,6 +263,15 @@ export const configMetadata: ConfigMetadataObject = { } return {}; }, + commandline: { + alias: "words", + subgroup: { + options: [10, 25, 50, 100], + }, + input: { + inputValueConvert: Number, + }, + }, }, time: { key: "time", @@ -198,6 +287,14 @@ export const configMetadata: ConfigMetadataObject = { } return {}; }, + commandline: { + subgroup: { + options: [15, 30, 60, 120], + }, + input: { + inputValueConvert: Number, + }, + }, }, mode: { key: "mode", @@ -235,6 +332,11 @@ export const configMetadata: ConfigMetadataObject = { showNoticeNotification(`Pace caret will not work with zen mode.`); } }, + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, quoteLength: { key: "quoteLength", @@ -250,6 +352,34 @@ export const configMetadata: ConfigMetadataObject = { } return {}; }, + commandline: { + alias: "quotes", + subgroup: { + options: [[0, 1, 2, 3], [0], [1], [2], [3], [-3]], + configValueMode: () => "include", + isAvailable: (value) => { + if (value[0] === -3) { + return isAuthenticated; + } + return undefined; + }, + display: (value) => { + if (areUnsortedArraysEqual(value, [0, 1, 2, 3])) { + return "all"; + } + + const map: Record = { + 0: "short", + 1: "medium", + 2: "long", + 3: "thicc", + "-3": "favorite", + }; + + return map[value[0] as number] as string; + }, + }, + }, }, language: { key: "language", @@ -258,6 +388,14 @@ export const configMetadata: ConfigMetadataObject = { changeRequiresRestart: true, group: "test", description: "Change in which language you want to type.", + commandline: { + subgroup: { + options: "fromSchema", + display: (value) => { + return getLanguageDisplayString(value); + }, + }, + }, }, burstHeatmap: { key: "burstHeatmap", @@ -265,6 +403,8 @@ export const configMetadata: ConfigMetadataObject = { displayString: "word burst heatmap", changeRequiresRestart: false, group: "test", + // set on the result screen + commandline: null, }, // behavior @@ -275,6 +415,11 @@ export const configMetadata: ConfigMetadataObject = { group: "behavior", description: "Normal is the classic typing test experience. Expert fails the test if you submit (press space) an incorrect word. Master fails if you press a single incorrect key (meaning you have to achieve 100% accuracy).", + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, quickRestart: { key: "quickRestart", @@ -284,6 +429,11 @@ export const configMetadata: ConfigMetadataObject = { group: "behavior", description: 'Press tab, esc or enter to quickly restart the test, or to quickly jump to the test page. These options disable tab navigation on most parts of the website. Using the "esc" option will move opening the commandline to the tab key.', + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, repeatQuotes: { key: "repeatQuotes", @@ -293,6 +443,11 @@ export const configMetadata: ConfigMetadataObject = { group: "behavior", description: "This setting changes the restarting behavior when typing in quote mode. Changing it to 'typing' will repeat the quote if you restart while typing.", + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, resultSaving: { key: "resultSaving", @@ -302,6 +457,13 @@ export const configMetadata: ConfigMetadataObject = { group: "behavior", description: 'Set this setting to "off" in case you want to practice without saving new results to your account and affecting your statistics.', + commandline: { + subgroup: { + options: "fromSchema", + alias: (val) => (val ? "enabled" : "disabled"), + }, + alias: "results practice incognito", + }, }, blindMode: { key: "blindMode", @@ -318,6 +480,11 @@ export const configMetadata: ConfigMetadataObject = { group: "behavior", description: "No errors or incorrect words are highlighted. Helps you to focus on raw speed. If enabled, quick end is recommended.", + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, alwaysShowWordsHistory: { key: "alwaysShowWordsHistory", @@ -327,6 +494,11 @@ export const configMetadata: ConfigMetadataObject = { group: "behavior", description: "This option will automatically show the words history at the end of the test. Can cause slight lag with a lot of words.", + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, singleListCommandLine: { key: "singleListCommandLine", @@ -336,6 +508,11 @@ export const configMetadata: ConfigMetadataObject = { group: "behavior", description: "When enabled, it will show the command line with all commands in a single list instead of submenu arrangements. Selecting 'manual' will expose all commands only after typing >.", + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, minWpm: { key: "minWpm", @@ -345,6 +522,23 @@ export const configMetadata: ConfigMetadataObject = { group: "behavior", description: "Automatically fails a test if your speed falls below a threshold.", + commandline: { + display: "Minimum speed...", + alias: "wpm", + subgroup: { + options: ["off"], + }, + input: { + configValue: "custom", + display: "custom...", + secondKey: "minWpmCustomSpeed", + inputValueConvert: (value) => { + let newVal = Number(value); + newVal = getTypingSpeedUnit(Config.typingSpeedUnit).toWpm(newVal); + return newVal; + }, + }, + }, }, minWpmCustomSpeed: { key: "minWpmCustomSpeed", @@ -360,6 +554,8 @@ export const configMetadata: ConfigMetadataObject = { } return {}; }, + // set through the minWpm command's custom input + commandline: null, }, minAcc: { key: "minAcc", @@ -369,6 +565,20 @@ export const configMetadata: ConfigMetadataObject = { group: "behavior", description: "Automatically fails a test if your accuracy falls below a threshold.", + commandline: { + display: "Minimum accuracy...", + subgroup: { + options: ["off"], + }, + input: { + configValue: "custom", + display: "custom...", + secondKey: "minAccCustom", + inputValueConvert: (value) => { + return Number(value); + }, + }, + }, }, minAccCustom: { key: "minAccCustom", @@ -384,6 +594,8 @@ export const configMetadata: ConfigMetadataObject = { } return {}; }, + // set through the minAcc command's custom input + commandline: null, }, minBurst: { key: "minBurst", @@ -393,6 +605,8 @@ export const configMetadata: ConfigMetadataObject = { group: "behavior", description: "Automatically fails a test if your raw for a single word falls below this threshold. Selecting 'flex' allows for this threshold to automatically decrease for longer words.", + // hand-built list (min-burst): it has 2 nested inputs + commandline: null, }, minBurstCustomSpeed: { key: "minBurstCustomSpeed", @@ -400,6 +614,8 @@ export const configMetadata: ConfigMetadataObject = { displayString: "min word burst custom speed", changeRequiresRestart: true, group: "behavior", + // set through the min burst list's custom inputs + commandline: null, }, britishEnglish: { key: "britishEnglish", @@ -409,6 +625,11 @@ export const configMetadata: ConfigMetadataObject = { group: "behavior", description: "When enabled, the website will use the British spelling instead of American. Note that this might not replace all words correctly. If you find any issues, please let us know.", + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, funbox: { key: "funbox", @@ -447,6 +668,8 @@ export const configMetadata: ConfigMetadataObject = { return false; }, + // hand-built list (funbox): a special non schema command at the top clears funboxes + commandline: null, }, customLayoutfluid: { key: "customLayoutfluid", @@ -459,6 +682,15 @@ export const configMetadata: ConfigMetadataObject = { overrideValue: ({ value }) => { return Array.from(new Set(value)); }, + commandline: { + input: { + defaultValue: () => { + return Config.customLayoutfluid.join(" "); + }, + inputValueConvert: (val) => + val.trim().split(" ") as ConfigSchemas.CustomLayoutFluid, + }, + }, }, customPolyglot: { key: "customPolyglot", @@ -470,6 +702,15 @@ export const configMetadata: ConfigMetadataObject = { overrideValue: ({ value }) => { return Array.from(new Set(value)); }, + commandline: { + input: { + defaultValue: () => { + return Config.customPolyglot.join(" "); + }, + inputValueConvert: (val) => + val.trim().split(" ") as ConfigSchemas.CustomPolyglot, + }, + }, }, // input @@ -489,6 +730,11 @@ export const configMetadata: ConfigMetadataObject = { } return {}; }, + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, strictSpace: { key: "strictSpace", @@ -498,6 +744,11 @@ export const configMetadata: ConfigMetadataObject = { group: "input", description: "Pressing space at the beginning of a word will insert a space character when this mode is enabled.", + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, oppositeShiftMode: { key: "oppositeShiftMode", @@ -507,6 +758,11 @@ export const configMetadata: ConfigMetadataObject = { group: "input", description: 'This mode will force you to use opposite shift keys for shifting. Using an incorrect one will count as an error. This feature ignores keys in locations B, Y, and ^ because many people use the other hand for those keys. If you\'re using external software to emulate your layout (including QMK), you should use the "keymap" mode - the standard "on" will not work. This will enforce opposite shift based on the "keymap layout" setting.', + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, stopOnError: { key: "stopOnError", @@ -525,6 +781,11 @@ export const configMetadata: ConfigMetadataObject = { } return {}; }, + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, deleteOnError: { key: "deleteOnError", @@ -543,6 +804,12 @@ export const configMetadata: ConfigMetadataObject = { } return {}; }, + commandline: { + subgroup: { + options: "fromSchema", + display: (deleteOnError) => deleteOnError.replace(/_/g, " "), + }, + }, }, confidenceMode: { key: "confidenceMode", @@ -562,6 +829,11 @@ export const configMetadata: ConfigMetadataObject = { } return {}; }, + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, quickEnd: { key: "quickEnd", @@ -571,6 +843,11 @@ export const configMetadata: ConfigMetadataObject = { group: "input", description: "This only applies to the words mode - when enabled, the test will end as soon as the last word has been typed, even if it's incorrect. When disabled, you need to manually confirm the last incorrect entry with a space.", + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, indicateTypos: { key: "indicateTypos", @@ -580,6 +857,11 @@ export const configMetadata: ConfigMetadataObject = { group: "input", description: 'Shows typos that you\'ve made. "Below" shows what you typed below the letters, "replace" will replace the letters with the ones you typed and "both" will do the same as replace and below, but it will show the correct letters below your mistakes.', + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, compositionDisplay: { key: "compositionDisplay", @@ -589,6 +871,11 @@ export const configMetadata: ConfigMetadataObject = { group: "input", description: 'Change how composition is displayed. "off" will just underline the letter if composition is active. "below" will show the composed character below the test. "replace" will replace the letter in the test with the composed character.', + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, hideExtraLetters: { key: "hideExtraLetters", @@ -598,6 +885,11 @@ export const configMetadata: ConfigMetadataObject = { group: "input", description: "Hides extra letters. This will completely avoid words jumping lines (due to changing width), but might feel a bit confusing when you press a key and nothing happens.", + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, lazyMode: { key: "lazyMode", @@ -607,6 +899,11 @@ export const configMetadata: ConfigMetadataObject = { group: "input", description: "Replaces accents / diacritics / special characters with their normal letter equivalents.", + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, layout: { key: "layout", @@ -616,6 +913,13 @@ export const configMetadata: ConfigMetadataObject = { group: "input", description: "With this setting you can emulate other layouts. This setting is best kept off, as it can break things like dead keys and alt layers.", + commandline: { + subgroup: { + options: "fromSchema", + display: (layout) => + layout === "default" ? "off" : layout.replace(/_/g, " "), + }, + }, }, codeUnindentOnBackspace: { key: "codeUnindentOnBackspace", @@ -625,6 +929,11 @@ export const configMetadata: ConfigMetadataObject = { group: "input", description: "Automatically go back to the previous line when deleting line leading tab characters. Only works in code languages.", + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, // sound @@ -635,6 +944,20 @@ export const configMetadata: ConfigMetadataObject = { changeRequiresRestart: false, group: "sound", description: "Change the volume of the sound effects.", + commandline: { + subgroup: { + options: [0.1, 0.5, 1], + display: (val) => + new Map([ + [0.1, "quiet"], + [0.5, "medium"], + [1.0, "loud"], + ]).get(val) ?? "custom...", + }, + input: { + inputValueConvert: Number, + }, + }, }, playSoundOnClick: { key: "playSoundOnClick", @@ -672,6 +995,46 @@ export const configMetadata: ConfigMetadataObject = { changeRequiresRestart: false, group: "sound", description: "Plays a short sound when you press a key.", + commandline: { + alias: "play", + display: "Sound on click...", + subgroup: { + options: "fromSchema", + display: (value) => { + const map: Record = + { + off: "off", + "1": "click", + "2": "beep", + "3": "pop", + "4": "nk creams", + "5": "typewriter", + "6": "osu", + "7": "hitmarker", + "8": "sine", + "9": "sawtooth", + "10": "square", + "11": "triangle", + "12": "pentatonic", + "13": "wholetone", + "14": "fist fight", + "15": "rubber keys", + "16": "fart", + "17": "akko lavenders", + "18": "cherrymx black abs", + "19": "cherrymx black pbt", + "20": "cherrymx blue abs", + "21": "cherrymx blue pbt", + "22": "cherrymx brown pbt", + "23": "kalih box white", + "24": "razer green", + "25": "tealios v2", + "26": "trust gxt", + }; + return map[value]; + }, + }, + }, }, playSoundOnError: { key: "playSoundOnError", @@ -688,6 +1051,24 @@ export const configMetadata: ConfigMetadataObject = { group: "sound", description: "Plays a short sound if you press an incorrect key or press space too early.", + commandline: { + alias: "play", + display: "Sound on error...", + subgroup: { + options: "fromSchema", + display: (value) => { + const map: Record = + { + off: "off", + "1": "damage", + "2": "triangle", + "3": "square", + "4": "punch miss", + }; + return map[value]; + }, + }, + }, }, playTimeWarning: { key: "playTimeWarning", @@ -704,6 +1085,18 @@ export const configMetadata: ConfigMetadataObject = { group: "sound", description: "Play a short warning sound if you are close to the end of a timed test.", + commandline: { + alias: "sound", + subgroup: { + options: "fromSchema", + display: (value) => { + if (value === "off") { + return "off"; + } + return `${value} second${value !== "1" ? "s" : ""}`; + }, + }, + }, }, // caret @@ -714,6 +1107,11 @@ export const configMetadata: ConfigMetadataObject = { changeRequiresRestart: false, group: "caret", description: "The caret will move smoothly between letters and words.", + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, caretStyle: { key: "caretStyle", @@ -723,6 +1121,12 @@ export const configMetadata: ConfigMetadataObject = { group: "caret", description: "Change the style of the caret during the test.", optionsMetadata: caretOptionsMetadata, + commandline: { + subgroup: { + options: "fromSchema", + isVisible: (value) => !["banana", "carrot", "monkey"].includes(value), + }, + }, }, paceCaret: { key: "paceCaret", @@ -754,6 +1158,21 @@ export const configMetadata: ConfigMetadataObject = { } return false; }, + commandline: { + display: "Pace caret mode...", + subgroup: { + options: ["off", "pb", "tagPb", "last", "average", "daily"], + }, + input: { + secondKey: "paceCaretCustomSpeed", + configValue: "custom", + inputValueConvert: (value) => { + let newVal = Number(value); + newVal = getTypingSpeedUnit(Config.typingSpeedUnit).toWpm(newVal); + return newVal; + }, + }, + }, }, paceCaretCustomSpeed: { key: "paceCaretCustomSpeed", @@ -769,6 +1188,8 @@ export const configMetadata: ConfigMetadataObject = { } return {}; }, + // set through the paceCaret command's custom input + commandline: null, }, paceCaretStyle: { key: "paceCaretStyle", @@ -778,6 +1199,12 @@ export const configMetadata: ConfigMetadataObject = { group: "caret", description: "Change the style of the pace caret during the test.", optionsMetadata: caretOptionsMetadata, + commandline: { + subgroup: { + options: "fromSchema", + isVisible: (value) => !["banana", "carrot", "monkey"].includes(value), + }, + }, }, repeatedPace: { key: "repeatedPace", @@ -787,6 +1214,11 @@ export const configMetadata: ConfigMetadataObject = { group: "caret", description: "When repeating a test, a pace caret will automatically be enabled for one test with the speed of your previous test. It does not override the pace caret if it's already enabled.", + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, // appearance @@ -798,6 +1230,13 @@ export const configMetadata: ConfigMetadataObject = { group: "appearance", description: 'Change the style of the timer/word count during a test. "Flash" styles will briefly show the timer in timed modes every 15 seconds.', + commandline: { + subgroup: { + options: "fromSchema", + display: replaceUnderscoresWithSpaces, + }, + alias: "timer", + }, }, liveSpeedStyle: { key: "liveSpeedStyle", @@ -815,6 +1254,12 @@ export const configMetadata: ConfigMetadataObject = { } return {}; }, + commandline: { + subgroup: { + options: "fromSchema", + }, + alias: "wpm", + }, }, liveAccStyle: { key: "liveAccStyle", @@ -832,6 +1277,12 @@ export const configMetadata: ConfigMetadataObject = { } return {}; }, + commandline: { + subgroup: { + options: "fromSchema", + }, + alias: "wpm", + }, }, liveBurstStyle: { key: "liveBurstStyle", @@ -841,6 +1292,12 @@ export const configMetadata: ConfigMetadataObject = { group: "appearance", description: "Change the style of the live burst speed displayed during the test.", + commandline: { + subgroup: { + options: "fromSchema", + }, + alias: "wpm", + }, }, timerColor: { key: "timerColor", @@ -850,6 +1307,14 @@ export const configMetadata: ConfigMetadataObject = { group: "appearance", description: "Change the color of the progress, live speed, accuracy and burst text.", + commandline: { + display: "Live progress color...", + alias: "timer speed wpm burst acc", + subgroup: { + options: "fromSchema", + alias: () => "timer", + }, + }, }, timerOpacity: { key: "timerOpacity", @@ -859,6 +1324,14 @@ export const configMetadata: ConfigMetadataObject = { group: "appearance", description: "Change the opacity of the progress, live speed, burst and accuracy text.", + commandline: { + display: "Live progress opacity...", + alias: "timer speed wpm burst acc", + subgroup: { + options: "fromSchema", + alias: () => "timer", + }, + }, }, highlightMode: { key: "highlightMode", @@ -867,6 +1340,12 @@ export const configMetadata: ConfigMetadataObject = { changeRequiresRestart: false, group: "appearance", description: "Change what is highlighted during the test.", + commandline: { + subgroup: { + options: "fromSchema", + display: replaceUnderscoresWithSpaces, + }, + }, }, typedEffect: { key: "typedEffect", @@ -875,6 +1354,11 @@ export const configMetadata: ConfigMetadataObject = { changeRequiresRestart: false, group: "appearance", description: "Change how typed words are shown.", + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, tapeMode: { key: "tapeMode", @@ -893,6 +1377,11 @@ export const configMetadata: ConfigMetadataObject = { } return {}; }, + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, tapeMargin: { key: "tapeMargin", @@ -903,6 +1392,11 @@ export const configMetadata: ConfigMetadataObject = { group: "appearance", description: "When in tape mode, set the carets position from the left edge of the typing test as a percentage (for example, 50% centers it).", + commandline: { + input: { + inputValueConvert: Number, + }, + }, }, smoothLineScroll: { key: "smoothLineScroll", @@ -911,6 +1405,11 @@ export const configMetadata: ConfigMetadataObject = { changeRequiresRestart: false, group: "appearance", description: "When enabled, the line transition will be animated.", + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, showAllLines: { key: "showAllLines", @@ -927,6 +1426,11 @@ export const configMetadata: ConfigMetadataObject = { } return false; }, + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, alwaysShowDecimalPlaces: { key: "alwaysShowDecimalPlaces", @@ -938,6 +1442,11 @@ export const configMetadata: ConfigMetadataObject = { group: "appearance", description: "Always shows decimal places for values on the result page, without the need to hover over the stats.", + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, typingSpeedUnit: { key: "typingSpeedUnit", @@ -946,6 +1455,12 @@ export const configMetadata: ConfigMetadataObject = { changeRequiresRestart: false, group: "appearance", description: "Display typing speed in the specified unit.", + commandline: { + subgroup: { + options: "fromSchema", + isVisible: (val) => val !== "wph", + }, + }, }, startGraphsAtZero: { key: "startGraphsAtZero", @@ -955,6 +1470,11 @@ export const configMetadata: ConfigMetadataObject = { group: "appearance", description: "Force graph axis to always start at zero, no matter what the data is. Turning this off may exaggerate the value changes.", + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, maxLineWidth: { key: "maxLineWidth", @@ -965,6 +1485,12 @@ export const configMetadata: ConfigMetadataObject = { group: "appearance", description: "Change the maximum width of the typing test, measured in characters. Setting this to 0 will align the words to the edges of the content area.", + commandline: { + input: { + alias: "page", + inputValueConvert: Number, + }, + }, }, fontSize: { key: "fontSize", @@ -974,6 +1500,11 @@ export const configMetadata: ConfigMetadataObject = { displayString: "font size", group: "appearance", description: "Change the font size of the test words.", + commandline: { + input: { + inputValueConvert: Number, + }, + }, }, fontFamily: { key: "fontFamily", @@ -988,6 +1519,26 @@ export const configMetadata: ConfigMetadataObject = { displayString: "Helvetica", }, }, + commandline: { + subgroup: { + options: typedKeys(Fonts).sort((a, b) => + (Fonts[a]?.display ?? a.replace(/_/g, " ")).localeCompare( + Fonts[b]?.display ?? b.replace(/_/g, " "), + ), + ), + display: (name) => + Fonts[name as KnownFontName]?.display ?? name.replaceAll(/_/g, " "), + customData: (name) => { + const fontConfig = Fonts[name as KnownFontName]; + if (fontConfig === undefined) return {}; + return { + name: name.replaceAll(/_/g, " "), + isSystem: fontConfig.systemFont === true, + display: fontConfig.display, + } as Record; + }, + }, + }, }, keymapMode: { key: "keymapMode", @@ -997,6 +1548,13 @@ export const configMetadata: ConfigMetadataObject = { group: "appearance", description: "Displays your current layout while taking a test. React shows what you pressed and Next shows what you need to press next.", + commandline: { + alias: "keyboard", + subgroup: { + options: "fromSchema", + alias: (val) => (val === "react" ? "flash" : ""), + }, + }, }, keymapLayout: { key: "keymapLayout", @@ -1007,6 +1565,17 @@ export const configMetadata: ConfigMetadataObject = { description: "Controls which layout is displayed on the keymap.", overrideConfig: ({ currentConfig }) => currentConfig.keymapMode === "off" ? { keymapMode: "static" } : {}, + commandline: { + alias: "keyboard", + subgroup: { + options: "fromSchema", + alias: (val) => (val === "overrideSync" ? "default" : ""), + display: (layout) => + layout === "overrideSync" + ? "emulator sync" + : layout.replace(/_/g, " "), + }, + }, }, keymapStyle: { key: "keymapStyle", @@ -1016,6 +1585,13 @@ export const configMetadata: ConfigMetadataObject = { group: "appearance", overrideConfig: ({ currentConfig }) => currentConfig.keymapMode === "off" ? { keymapMode: "static" } : {}, + commandline: { + subgroup: { + options: "fromSchema", + display: replaceUnderscoresWithSpaces, + }, + alias: "keyboard", + }, }, keymapLegendStyle: { key: "keymapLegendStyle", @@ -1025,6 +1601,12 @@ export const configMetadata: ConfigMetadataObject = { group: "appearance", overrideConfig: ({ currentConfig }) => currentConfig.keymapMode === "off" ? { keymapMode: "static" } : {}, + commandline: { + subgroup: { + options: "fromSchema", + }, + alias: "keyboard", + }, }, keymapKeys: { key: "keymapKeys", @@ -1034,6 +1616,13 @@ export const configMetadata: ConfigMetadataObject = { group: "appearance", overrideConfig: ({ currentConfig }) => currentConfig.keymapMode === "off" ? { keymapMode: "static" } : {}, + commandline: { + subgroup: { + options: "fromSchema", + display: replaceUnderscoresWithSpaces, + }, + alias: "keyboard", + }, }, keymapSize: { key: "keymapSize", @@ -1050,6 +1639,12 @@ export const configMetadata: ConfigMetadataObject = { }, overrideConfig: ({ currentConfig }) => currentConfig.keymapMode === "off" ? { keymapMode: "static" } : {}, + commandline: { + input: { + alias: "keyboard", + inputValueConvert: Number, + }, + }, }, // theme @@ -1061,6 +1656,11 @@ export const configMetadata: ConfigMetadataObject = { group: "theme", description: "By default, typed text is brighter than the future text. When enabled, the colors will be flipped and the future text will be brighter than the already typed text.", + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, colorfulMode: { key: "colorfulMode", @@ -1070,6 +1670,11 @@ export const configMetadata: ConfigMetadataObject = { group: "theme", description: "When enabled, the test words will use the main color, instead of the text color, making the website more colorful.", + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, customBackground: { key: "customBackground", @@ -1082,6 +1687,9 @@ export const configMetadata: ConfigMetadataObject = { }, description: "Set an image url or local image to be a custom background image. Local image always take priority over the image url. Cover fits the image to cover the screen. Contain fits the image to be fully visible. Max fits the image corner to corner.", + commandline: { + input: {}, + }, }, customBackgroundSize: { key: "customBackgroundSize", @@ -1091,6 +1699,11 @@ export const configMetadata: ConfigMetadataObject = { group: "theme", description: "Set an image url or local image to be a custom background image. Cover fits the image to cover the screen. Contain fits the image to be fully visible. Max fits the image corner to corner.", + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, customBackgroundFilter: { key: "customBackgroundFilter", @@ -1099,6 +1712,8 @@ export const configMetadata: ConfigMetadataObject = { changeRequiresRestart: false, group: "theme", description: "Apply various effects to the custom background.", + // hand-built list (background-filter): it has 4 nested inputs + commandline: null, }, autoSwitchTheme: { key: "autoSwitchTheme", @@ -1108,6 +1723,8 @@ export const configMetadata: ConfigMetadataObject = { group: "theme", description: "Enabling this will automatically switch the theme between light and dark depending on the system theme.", + // set on the settings page + commandline: null, }, themeLight: { key: "themeLight", @@ -1115,6 +1732,8 @@ export const configMetadata: ConfigMetadataObject = { displayString: "theme light", changeRequiresRestart: false, group: "theme", + // set on the settings page + commandline: null, }, themeDark: { key: "themeDark", @@ -1122,6 +1741,8 @@ export const configMetadata: ConfigMetadataObject = { displayString: "theme dark", changeRequiresRestart: false, group: "theme", + // set on the settings page + commandline: null, }, randomTheme: { key: "randomTheme", @@ -1159,6 +1780,13 @@ export const configMetadata: ConfigMetadataObject = { } return false; }, + commandline: { + subgroup: { + options: "fromSchema", + isAvailable: (value) => + value === "custom" ? isAuthenticated : undefined, + }, + }, }, favThemes: { key: "favThemes", @@ -1166,6 +1794,8 @@ export const configMetadata: ConfigMetadataObject = { displayString: "favorite themes", changeRequiresRestart: false, group: "theme", + // set through the add/remove favorite theme commands + commandline: null, }, theme: { key: "theme", @@ -1179,6 +1809,8 @@ export const configMetadata: ConfigMetadataObject = { customTheme: false, }; }, + // hand-built list (themes): sorted by color and affected by config.favThemes + commandline: null, }, customTheme: { key: "customTheme", @@ -1186,6 +1818,11 @@ export const configMetadata: ConfigMetadataObject = { displayString: "custom theme", changeRequiresRestart: false, group: "theme", + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, customThemeColors: { key: "customThemeColors", @@ -1201,6 +1838,8 @@ export const configMetadata: ConfigMetadataObject = { return value; } }, + // set through the custom themes list and theme settings + commandline: null, }, // hide elements @@ -1215,6 +1854,12 @@ export const configMetadata: ConfigMetadataObject = { true: { displayString: "show" }, false: { displayString: "hide" }, }, + commandline: { + subgroup: { + options: "fromSchema", + }, + display: "Key tips...", + }, }, showOutOfFocusWarning: { key: "showOutOfFocusWarning", @@ -1228,6 +1873,12 @@ export const configMetadata: ConfigMetadataObject = { true: { displayString: "show" }, false: { displayString: "hide" }, }, + commandline: { + subgroup: { + options: "fromSchema", + }, + display: "Out of focus warning...", + }, }, capsLockWarning: { key: "capsLockWarning", @@ -1240,6 +1891,11 @@ export const configMetadata: ConfigMetadataObject = { true: { displayString: "show" }, false: { displayString: "hide" }, }, + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, showAverage: { key: "showAverage", @@ -1249,6 +1905,11 @@ export const configMetadata: ConfigMetadataObject = { group: "hideElements", description: "Displays your average speed and/or accuracy over the last 10 tests.", + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, showPb: { key: "showPb", @@ -1256,6 +1917,12 @@ export const configMetadata: ConfigMetadataObject = { displayString: "show personal best", changeRequiresRestart: false, group: "hideElements", + commandline: { + subgroup: { + options: "fromSchema", + }, + alias: "pb", + }, }, // other (hidden) @@ -1274,6 +1941,8 @@ export const configMetadata: ConfigMetadataObject = { } return value; }, + // set on the account page + commandline: null, }, monkey: { key: "monkey", @@ -1296,6 +1965,11 @@ export const configMetadata: ConfigMetadataObject = { } return {}; }, + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, monkeyPowerLevel: { key: "monkeyPowerLevel", @@ -1303,6 +1977,24 @@ export const configMetadata: ConfigMetadataObject = { displayString: "monkey power level", changeRequiresRestart: false, group: "hidden", + commandline: { + alias: "powermode", + isVisible: false, + subgroup: { + options: "fromSchema", + display: (value) => { + const map: Record = + { + off: "off", + "1": "mellow", + "2": "high", + "3": "ultra", + "4": "over 9000", + }; + return map[value]; + }, + }, + }, }, // ads @@ -1331,8 +2023,24 @@ export const configMetadata: ConfigMetadataObject = { showNoticeNotification("Ad settings changed. Refreshing..."); } }, + commandline: { + subgroup: { + options: "fromSchema", + }, + }, }, -}; +} satisfies ConfigMetadataObject; + +export const configMetadata: ConfigMetadataObject = configMetadataEntries; + +/** + * Config keys that have a generated commandline command + */ +export type CommandlineConfigKey = { + [ + K in keyof typeof configMetadataEntries + ]: (typeof configMetadataEntries)[K]["commandline"] extends null ? never : K; +}[keyof typeof configMetadataEntries]; // typed accessor for a single option's metadata, avoiding per-callsite casts export function getOptionMetadata( From 3224a65da451cec1e068b116fcbb38bbcc4868d2 Mon Sep 17 00:00:00 2001 From: Miodec Date: Wed, 28 Oct 2026 10:01:57 +0100 Subject: [PATCH 2/6] build: include tsx files in madge circular import checks (@miodec) `--extensions ts` skipped every .tsx module, so cycles through Solid components and config metadata went unchecked. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NZNRfGDYsGYMUTp2zYUTrv --- frontend/package.json | 4 ++-- packages/challenges/package.json | 2 +- packages/contracts/package.json | 2 +- packages/funbox/package.json | 2 +- packages/schemas/package.json | 2 +- packages/util/package.json | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 80bfb6ce964c..72315f1e80ba 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,10 +10,10 @@ "lint-json": "eslint static/**/*.json", "check-assets": "tsx ./scripts/check-assets.ts", "audit": "vite-bundle-visualizer", - "dep-graph": "madge -c -i \"dep-graph.png\" ./src/ts", + "dep-graph": "madge -c --extensions ts,tsx -i \"dep-graph.png\" ./src/ts", "ts-check": "tsc --noEmit", "build": "npm run madge && vite build", - "madge": " madge --circular --extensions ts ./src", + "madge": " madge --circular --extensions ts,tsx ./src", "start": "vite preview --port 3000", "dev": "vite dev", "test": "vitest run", diff --git a/packages/challenges/package.json b/packages/challenges/package.json index 716c38ea3b34..8f6c8c3352f6 100644 --- a/packages/challenges/package.json +++ b/packages/challenges/package.json @@ -12,7 +12,7 @@ "dev": "tsup-node --watch", "build": "npm run madge && tsup-node", "test": "vitest run", - "madge": " madge --circular --extensions ts ./src", + "madge": " madge --circular --extensions ts,tsx ./src", "ts-check": "tsc --noEmit", "lint": "oxlint . --type-aware --type-check", "george-mapping": "tsx ./scripts/challenge-roles" diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 8579c6ee71b8..428176dcbdd4 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -17,7 +17,7 @@ "dev": "tsup-node --watch", "build": "npm run madge && tsup-node", "test": "vitest run", - "madge": " madge --circular --extensions ts ./src", + "madge": " madge --circular --extensions ts,tsx ./src", "ts-check": "tsc --noEmit", "lint": "oxlint . --type-aware --type-check" }, diff --git a/packages/funbox/package.json b/packages/funbox/package.json index 9c418ad38f47..c0900548b65b 100644 --- a/packages/funbox/package.json +++ b/packages/funbox/package.json @@ -12,7 +12,7 @@ "dev": "tsup-node --watch", "build": "npm run madge && tsup-node", "test": "vitest run", - "madge": " madge --circular --extensions ts ./src", + "madge": " madge --circular --extensions ts,tsx ./src", "ts-check": "tsc --noEmit", "lint": "oxlint . --type-aware --type-check" }, diff --git a/packages/schemas/package.json b/packages/schemas/package.json index 7888eafc88be..84c7bbc51c71 100644 --- a/packages/schemas/package.json +++ b/packages/schemas/package.json @@ -17,7 +17,7 @@ "dev": "tsup-node --watch", "build": "npm run madge && tsup-node", "test": "vitest run", - "madge": " madge --circular --extensions ts ./src", + "madge": " madge --circular --extensions ts,tsx ./src", "ts-check": "tsc --noEmit", "lint": "oxlint . --type-aware --type-check" }, diff --git a/packages/util/package.json b/packages/util/package.json index 2a3f1464d40a..4c3bb945ae62 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -11,7 +11,7 @@ "dev": "tsup-node --watch", "build": "npm run madge && tsup-node", "test": "vitest run", - "madge": " madge --circular --extensions ts ./src", + "madge": " madge --circular --extensions ts,tsx ./src", "ts-check": "tsc --noEmit", "lint": "oxlint . --type-aware --type-check" }, From 1db9dfd73c885b5d283c0b5fb996f4553e379c19 Mon Sep 17 00:00:00 2001 From: Miodec Date: Wed, 28 Oct 2026 10:11:21 +0100 Subject: [PATCH 3/6] refactor: generated commands use the settings labels (C5) (@miodec) Option labels come from `optionsMetadata` via `optionLabel`, command names from `displayString`; commandline display overrides stay only where settings has no label to share (blind mode, quote length, sound volume, font family, keymap layout). Renamed labels are recorded in the spec's intended deviations; "minimum" and "progress" aliases keep the old names searchable. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NZNRfGDYsGYMUTp2zYUTrv --- docs/specs/solid-monkeytype/roadmap.md | 2 +- docs/specs/solid-monkeytype/spec.md | 6 ++ frontend/__tests__/commandline/util.spec.ts | 51 ++++++++- frontend/src/ts/commandline/util.ts | 27 ++--- frontend/src/ts/config/metadata.tsx | 114 ++++---------------- 5 files changed, 94 insertions(+), 106 deletions(-) diff --git a/docs/specs/solid-monkeytype/roadmap.md b/docs/specs/solid-monkeytype/roadmap.md index 7588485aa799..654356b3eae5 100644 --- a/docs/specs/solid-monkeytype/roadmap.md +++ b/docs/specs/solid-monkeytype/roadmap.md @@ -102,7 +102,7 @@ Serial; each step needs the previous one. Verify each in the browser (`__mt.chec | [ ] | C2 | S · commit each | Replace config-bus subscribers outside the test with effects on store keys: theme controller, sound controller, quotes controller, challenge controller, commandline theme list | C1 | One PR each, all parallel | | [ ] | C3 | M · commit per area; PR for test modules | Move mutable `Config.x` reads to the store, by area: format singleton; commandline lists; controllers; input; test modules | C1 | Parallel by area; the test-module part follows T3/T6 where they overlap | | [ ] | C4 | L · PR | Delete the mutable config object, the config event bus and the full-config-change events; whole-config apply becomes one batched reconcile (the `batch` already exists since C1); convert remaining test subscribers (pace caret, tts, practise words, test-logic) to effects | C2, C3, T6, L2 | Last step of C | -| [x] | C5 | M · PR | Merge commandline metadata into config metadata; command icons become icon-component types | none | Done in PR (link follows): each config key's metadata has a required `commandline` (`null` with a reason for keys set elsewhere or with hand-built lists); `CommandlineConfigKey` is derived from it. Side effects (`afterExec` restarts, sound/font `hover` previews) moved to `commandline/config-actions.ts`, since the modules they call import the setters; its restarts go with T6b. Labels moved unchanged (TODO to unify them with `optionsMetadata`). `Command.icon`/`iconType` → `fa: FaObject`; the modal's text-icon branch was dead and is gone | +| [x] | C5 | M · PR | Merge commandline metadata into config metadata; command icons become icon-component types | none | Done in PR (link follows): each config key's metadata has a required `commandline` (`null` with a reason for keys set elsewhere or with hand-built lists); `CommandlineConfigKey` is derived from it. Side effects (`afterExec` restarts, sound/font `hover` previews) moved to `commandline/config-actions.ts`, since the modules they call import the setters; its restarts go with T6b. Generated options use the settings labels (`optionLabel` from `optionsMetadata`), command names the settings `displayString`; renamed labels are in the spec's intended deviations (blind mode keeps on/off). `Command.icon`/`iconType` → `fa: FaObject`; the modal's text-icon branch was dead and is gone | ## Track U: user data (parallel with T and C) diff --git a/docs/specs/solid-monkeytype/spec.md b/docs/specs/solid-monkeytype/spec.md index 2ac9b7299dd8..760a670502b1 100644 --- a/docs/specs/solid-monkeytype/spec.md +++ b/docs/specs/solid-monkeytype/spec.md @@ -266,6 +266,12 @@ From config (C1): - A key that fails to apply during a whole-config apply holds its default in the store (it used to hold the rejected value). - A single setting change runs store effects before config event listeners. A whole-config apply runs them once, before failed keys are re-saved and before full-config-change finished (they used to run after it). +From commandline metadata (C5): generated config commands use the settings labels (`displayString`, `optionsMetadata`). + +- Renamed commands: Minimum speed → Min speed, Minimum accuracy → Min accuracy (both get a "minimum" alias), Sound on click/error → Play sound on click/error, Pace caret mode → Pace caret, Live progress color/opacity → Timer color/opacity (a "progress" alias), Key tips → Show key tips, Out of focus warning → Show out of focus warning. +- Renamed options: key tips, out of focus warning and caps lock warning show/hide (were on/off); error sound 4 "missed punch" (was "punch miss"); layout "default" (was "off"); random theme "favorite" (was "fav"); pace caret "tag pb" (was "tagPb"). +- Kept commandline labels: blind mode on/off (settings shows a blank "on" for button height), quote length, sound volume, font family and keymap layout (no settings label of their own to share). + ## Settled decisions (grilling, 2026-10-24) | # | Decision | diff --git a/frontend/__tests__/commandline/util.spec.ts b/frontend/__tests__/commandline/util.spec.ts index 70325668328c..e69176d6b001 100644 --- a/frontend/__tests__/commandline/util.spec.ts +++ b/frontend/__tests__/commandline/util.spec.ts @@ -15,7 +15,10 @@ import { Command } from "../../src/ts/commandline/types"; const buildCommandForConfigKey = Util.__testing._buildCommandForConfigKey; describe("CommandlineUtils", () => { - vi.mock("../../src/ts/config/metadata", () => ({ configMetadata: [] })); + vi.mock("../../src/ts/config/metadata", async (importOriginal) => ({ + ...(await importOriginal()), + configMetadata: [], + })); vi.mock("../../src/ts/commandline/config-actions", () => ({ commandlineConfigActions: {}, })); @@ -136,6 +139,52 @@ describe("CommandlineUtils", () => { expect(cmd.subgroup?.list[0]?.available).toBeUndefined(); expect(cmd.subgroup?.list[1]?.available?.()).toBe(true); }); + + it("uses the settings option labels", () => { + //GIVEN + const schema = z.enum(["off", "tag_pb", "fav"]); + + //WHEN + const cmd = buildCommand({ + cmdMeta: { subgroup: { options: "fromSchema" } }, + configMeta: { + optionsMetadata: { fav: { displayString: "favorite" } } as any, + }, + schema, + }); + + //THEN + expect(cmd.subgroup?.list.map((it) => it.display)).toEqual([ + "off", + "tag pb", + "favorite", + ]); + }); + + it("prefers the commandline display over the settings label", () => { + //GIVEN + const schema = z.boolean(); + + //WHEN + const cmd = buildCommand({ + cmdMeta: { + subgroup: { + options: "fromSchema", + display: (value) => (value === true ? "yes" : "no"), + }, + }, + configMeta: { + optionsMetadata: { true: { displayString: " " } } as any, + }, + schema, + }); + + //THEN + expect(cmd.subgroup?.list.map((it) => it.display)).toEqual([ + "no", + "yes", + ]); + }); }); describe("type subgroupWithInput", () => { diff --git a/frontend/src/ts/commandline/util.ts b/frontend/src/ts/commandline/util.ts index 8a928c2e38ee..6f498d397fc4 100644 --- a/frontend/src/ts/commandline/util.ts +++ b/frontend/src/ts/commandline/util.ts @@ -7,6 +7,8 @@ import { CommandlineSubgroupMetadata, ConfigMetadata, configMetadata, + OptionMetadata, + optionLabel, } from "../config/metadata"; import { capitalizeFirstLetter } from "../utils/strings"; import { @@ -118,7 +120,15 @@ function buildCommandWithSubgroup( ); } const list = values.map((value) => - buildSubgroupCommand(key, value, subgroupProps, subgroupActions), + buildSubgroupCommand( + key, + value, + subgroupProps, + subgroupActions, + ( + configMeta.optionsMetadata as Record | undefined + )?.[String(value)], + ), ); list.sort((a, b) => { @@ -152,20 +162,13 @@ function buildSubgroupCommand( customData: commandCustomData, }: CommandlineSubgroupMetadata, { afterExec, hover }: CommandlineConfigActions["subgroup"] = {}, + optionMeta?: OptionMetadata, ): Command { const val = value; - let displayString = commandDisplay?.(value); - - if (displayString === undefined) { - if (value === true) { - displayString = "on"; - } else if (value === false) { - displayString = "off"; - } else { - displayString = value.toString(); - } - } + // same label as the settings page unless the commandline overrides it + const displayString = + commandDisplay?.(value) ?? optionLabel(optionMeta, value); return { id: `set${capitalizeFirstLetter(key)}${capitalizeFirstLetter( diff --git a/frontend/src/ts/config/metadata.tsx b/frontend/src/ts/config/metadata.tsx index d232d7cefb96..a4bc830bd7bf 100644 --- a/frontend/src/ts/config/metadata.tsx +++ b/frontend/src/ts/config/metadata.tsx @@ -15,11 +15,7 @@ import { Validation } from "../types/validation"; import { areUnsortedArraysEqual } from "../utils/arrays"; import { isDevEnvironment } from "../utils/env"; import { reloadAfter } from "../utils/misc"; -import { - capitalizeFirstLetter, - getLanguageDisplayString, - replaceUnderscoresWithSpaces, -} from "../utils/strings"; +import { capitalizeFirstLetter } from "../utils/strings"; import { get as getTypingSpeedUnit } from "../utils/typing-speed-units"; import { getOptions } from "../utils/zod"; import { @@ -79,7 +75,6 @@ export type CommandlineSubgroupMetadata = }; // side effects (afterExec, hover) live in commandline/config-actions -//TODO: the display strings duplicate optionsMetadata and displayString in places, unify them export type CommandlineMetadata< K extends keyof ConfigSchemas.Config, K2 extends keyof ConfigSchemas.Config = keyof ConfigSchemas.Config, @@ -391,9 +386,6 @@ const configMetadataEntries = { commandline: { subgroup: { options: "fromSchema", - display: (value) => { - return getLanguageDisplayString(value); - }, }, }, }, @@ -483,6 +475,8 @@ const configMetadataEntries = { commandline: { subgroup: { options: "fromSchema", + // settings shows " " for "on" (button height), keep a readable label here + display: (value) => (value ? "on" : "off"), }, }, }, @@ -523,14 +517,12 @@ const configMetadataEntries = { description: "Automatically fails a test if your speed falls below a threshold.", commandline: { - display: "Minimum speed...", - alias: "wpm", + alias: "wpm minimum", subgroup: { options: ["off"], }, input: { configValue: "custom", - display: "custom...", secondKey: "minWpmCustomSpeed", inputValueConvert: (value) => { let newVal = Number(value); @@ -566,13 +558,12 @@ const configMetadataEntries = { description: "Automatically fails a test if your accuracy falls below a threshold.", commandline: { - display: "Minimum accuracy...", + alias: "minimum", subgroup: { options: ["off"], }, input: { configValue: "custom", - display: "custom...", secondKey: "minAccCustom", inputValueConvert: (value) => { return Number(value); @@ -807,7 +798,6 @@ const configMetadataEntries = { commandline: { subgroup: { options: "fromSchema", - display: (deleteOnError) => deleteOnError.replace(/_/g, " "), }, }, }, @@ -916,8 +906,6 @@ const configMetadataEntries = { commandline: { subgroup: { options: "fromSchema", - display: (layout) => - layout === "default" ? "off" : layout.replace(/_/g, " "), }, }, }, @@ -997,42 +985,8 @@ const configMetadataEntries = { description: "Plays a short sound when you press a key.", commandline: { alias: "play", - display: "Sound on click...", subgroup: { options: "fromSchema", - display: (value) => { - const map: Record = - { - off: "off", - "1": "click", - "2": "beep", - "3": "pop", - "4": "nk creams", - "5": "typewriter", - "6": "osu", - "7": "hitmarker", - "8": "sine", - "9": "sawtooth", - "10": "square", - "11": "triangle", - "12": "pentatonic", - "13": "wholetone", - "14": "fist fight", - "15": "rubber keys", - "16": "fart", - "17": "akko lavenders", - "18": "cherrymx black abs", - "19": "cherrymx black pbt", - "20": "cherrymx blue abs", - "21": "cherrymx blue pbt", - "22": "cherrymx brown pbt", - "23": "kalih box white", - "24": "razer green", - "25": "tealios v2", - "26": "trust gxt", - }; - return map[value]; - }, }, }, }, @@ -1053,20 +1007,8 @@ const configMetadataEntries = { "Plays a short sound if you press an incorrect key or press space too early.", commandline: { alias: "play", - display: "Sound on error...", subgroup: { options: "fromSchema", - display: (value) => { - const map: Record = - { - off: "off", - "1": "damage", - "2": "triangle", - "3": "square", - "4": "punch miss", - }; - return map[value]; - }, }, }, }, @@ -1089,12 +1031,6 @@ const configMetadataEntries = { alias: "sound", subgroup: { options: "fromSchema", - display: (value) => { - if (value === "off") { - return "off"; - } - return `${value} second${value !== "1" ? "s" : ""}`; - }, }, }, }, @@ -1159,7 +1095,6 @@ const configMetadataEntries = { return false; }, commandline: { - display: "Pace caret mode...", subgroup: { options: ["off", "pb", "tagPb", "last", "average", "daily"], }, @@ -1233,7 +1168,6 @@ const configMetadataEntries = { commandline: { subgroup: { options: "fromSchema", - display: replaceUnderscoresWithSpaces, }, alias: "timer", }, @@ -1308,8 +1242,7 @@ const configMetadataEntries = { description: "Change the color of the progress, live speed, accuracy and burst text.", commandline: { - display: "Live progress color...", - alias: "timer speed wpm burst acc", + alias: "timer speed wpm burst acc progress", subgroup: { options: "fromSchema", alias: () => "timer", @@ -1325,8 +1258,7 @@ const configMetadataEntries = { description: "Change the opacity of the progress, live speed, burst and accuracy text.", commandline: { - display: "Live progress opacity...", - alias: "timer speed wpm burst acc", + alias: "timer speed wpm burst acc progress", subgroup: { options: "fromSchema", alias: () => "timer", @@ -1343,7 +1275,6 @@ const configMetadataEntries = { commandline: { subgroup: { options: "fromSchema", - display: replaceUnderscoresWithSpaces, }, }, }, @@ -1588,7 +1519,6 @@ const configMetadataEntries = { commandline: { subgroup: { options: "fromSchema", - display: replaceUnderscoresWithSpaces, }, alias: "keyboard", }, @@ -1619,7 +1549,6 @@ const configMetadataEntries = { commandline: { subgroup: { options: "fromSchema", - display: replaceUnderscoresWithSpaces, }, alias: "keyboard", }, @@ -1858,7 +1787,6 @@ const configMetadataEntries = { subgroup: { options: "fromSchema", }, - display: "Key tips...", }, }, showOutOfFocusWarning: { @@ -1877,7 +1805,6 @@ const configMetadataEntries = { subgroup: { options: "fromSchema", }, - display: "Out of focus warning...", }, }, capsLockWarning: { @@ -1975,6 +1902,13 @@ const configMetadataEntries = { key: "monkeyPowerLevel", fa: { icon: "fa-egg" }, displayString: "monkey power level", + optionsMetadata: { + off: {}, + "1": { displayString: "mellow" }, + "2": { displayString: "high" }, + "3": { displayString: "ultra" }, + "4": { displayString: "over 9000" }, + }, changeRequiresRestart: false, group: "hidden", commandline: { @@ -1982,17 +1916,6 @@ const configMetadataEntries = { isVisible: false, subgroup: { options: "fromSchema", - display: (value) => { - const map: Record = - { - off: "off", - "1": "mellow", - "2": "high", - "3": "ultra", - "4": "over 9000", - }; - return map[value]; - }, }, }, }, @@ -2070,7 +1993,14 @@ export function getOptionLabel( key: K, option: ConfigSchemas.Config[K], ): string { - const optionMeta = getOptionMetadata(key, option); + return optionLabel(getOptionMetadata(key, option), option); +} + +// the label for an option given its metadata (settings and the commandline share it) +export function optionLabel( + optionMeta: OptionMetadata | undefined, + option: unknown, +): string { if (optionMeta?.displayString !== undefined) return optionMeta.displayString; if (option === true) return "on"; if (option === false) return "off"; From c0d0da843b08128487a3c27d6f48e94386f4ee77 Mon Sep 17 00:00:00 2001 From: Miodec Date: Wed, 28 Oct 2026 10:11:59 +0100 Subject: [PATCH 4/6] docs: link C5 PR in the roadmap (@miodec) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NZNRfGDYsGYMUTp2zYUTrv --- docs/specs/solid-monkeytype/roadmap.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/specs/solid-monkeytype/roadmap.md b/docs/specs/solid-monkeytype/roadmap.md index 654356b3eae5..076535f6ffd2 100644 --- a/docs/specs/solid-monkeytype/roadmap.md +++ b/docs/specs/solid-monkeytype/roadmap.md @@ -102,7 +102,7 @@ Serial; each step needs the previous one. Verify each in the browser (`__mt.chec | [ ] | C2 | S · commit each | Replace config-bus subscribers outside the test with effects on store keys: theme controller, sound controller, quotes controller, challenge controller, commandline theme list | C1 | One PR each, all parallel | | [ ] | C3 | M · commit per area; PR for test modules | Move mutable `Config.x` reads to the store, by area: format singleton; commandline lists; controllers; input; test modules | C1 | Parallel by area; the test-module part follows T3/T6 where they overlap | | [ ] | C4 | L · PR | Delete the mutable config object, the config event bus and the full-config-change events; whole-config apply becomes one batched reconcile (the `batch` already exists since C1); convert remaining test subscribers (pace caret, tts, practise words, test-logic) to effects | C2, C3, T6, L2 | Last step of C | -| [x] | C5 | M · PR | Merge commandline metadata into config metadata; command icons become icon-component types | none | Done in PR (link follows): each config key's metadata has a required `commandline` (`null` with a reason for keys set elsewhere or with hand-built lists); `CommandlineConfigKey` is derived from it. Side effects (`afterExec` restarts, sound/font `hover` previews) moved to `commandline/config-actions.ts`, since the modules they call import the setters; its restarts go with T6b. Generated options use the settings labels (`optionLabel` from `optionsMetadata`), command names the settings `displayString`; renamed labels are in the spec's intended deviations (blind mode keeps on/off). `Command.icon`/`iconType` → `fa: FaObject`; the modal's text-icon branch was dead and is gone | +| [x] | C5 | M · PR | Merge commandline metadata into config metadata; command icons become icon-component types | none | Done in [#8537](https://github.com/monkeytypegame/monkeytype/pull/8537): each config key's metadata has a required `commandline` (`null` with a reason for keys set elsewhere or with hand-built lists); `CommandlineConfigKey` is derived from it. Side effects (`afterExec` restarts, sound/font `hover` previews) moved to `commandline/config-actions.ts`, since the modules they call import the setters; its restarts go with T6b. Generated options use the settings labels (`optionLabel` from `optionsMetadata`), command names the settings `displayString`; renamed labels are in the spec's intended deviations (blind mode keeps on/off). `Command.icon`/`iconType` → `fa: FaObject`; the modal's text-icon branch was dead and is gone | ## Track U: user data (parallel with T and C) From 636dfad0cdd1e5fb19bd7f30f2b52c0e81f86133 Mon Sep 17 00:00:00 2001 From: Miodec Date: Wed, 28 Oct 2026 10:52:51 +0100 Subject: [PATCH 5/6] fixes --- frontend/src/ts/commandline/lists/min-burst.ts | 4 ++-- frontend/src/ts/config/metadata.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/ts/commandline/lists/min-burst.ts b/frontend/src/ts/commandline/lists/min-burst.ts index a2cabac11c1d..c9423824e394 100644 --- a/frontend/src/ts/commandline/lists/min-burst.ts +++ b/frontend/src/ts/commandline/lists/min-burst.ts @@ -4,7 +4,7 @@ import { get as getTypingSpeedUnit } from "../../utils/typing-speed-units"; import { Command, CommandsSubgroup } from "../types"; const subgroup: CommandsSubgroup = { - title: "Minimum word burst...", + title: "Min word burst...", configKey: "minBurst", list: [ { @@ -49,7 +49,7 @@ const subgroup: CommandsSubgroup = { const commands: Command[] = [ { id: "changeMinBurst", - display: "Minimum word burst...", + display: "Min word burst...", alias: "minimum", fa: { icon: "fa-bomb" }, subgroup, diff --git a/frontend/src/ts/config/metadata.tsx b/frontend/src/ts/config/metadata.tsx index a4bc830bd7bf..ada1d8b0d595 100644 --- a/frontend/src/ts/config/metadata.tsx +++ b/frontend/src/ts/config/metadata.tsx @@ -1236,7 +1236,7 @@ const configMetadataEntries = { timerColor: { key: "timerColor", fa: { icon: "fa-chart-pie" }, - displayString: "timer color", + displayString: "live progress color", changeRequiresRestart: false, group: "appearance", description: @@ -1252,7 +1252,7 @@ const configMetadataEntries = { timerOpacity: { key: "timerOpacity", fa: { icon: "fa-chart-pie" }, - displayString: "timer opacity", + displayString: "live progress opacity", changeRequiresRestart: false, group: "appearance", description: From 6072cd222bb582cdfeef26b6256cda37be55f6c0 Mon Sep 17 00:00:00 2001 From: Miodec Date: Wed, 28 Oct 2026 11:26:20 +0100 Subject: [PATCH 6/6] fix: play click preview after picking a sound volume (@miodec) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NZNRfGDYsGYMUTp2zYUTrv --- frontend/src/ts/commandline/config-actions.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/ts/commandline/config-actions.ts b/frontend/src/ts/commandline/config-actions.ts index 8c87a67e4f7b..a00f4c5a7907 100644 --- a/frontend/src/ts/commandline/config-actions.ts +++ b/frontend/src/ts/commandline/config-actions.ts @@ -71,7 +71,9 @@ export const commandlineConfigActions: CommandlineConfigActionsObject = { //sound soundVolume: { subgroup: { - afterExec: () => SoundController.playClick, + afterExec: () => { + void SoundController.playClick(); + }, }, }, playSoundOnClick: {