refactor(config): config metadata (@miodec) (#6753)

Create config metadata object
Move all the special code on config change to config listeners
Create a generic set function which will work with the metadata object
to update any config key
Update all setters to use the generic set.
(Later probably only use the generic settter and remove all the specific
ones)
Also orders config groups and config schema.

---------

Co-authored-by: Christian Fehmer <cfe@sexy-developer.com>
This commit is contained in:
Jack 2025-07-21 13:50:33 +02:00 committed by GitHub
parent db9e54f794
commit 92790f3682
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 1737 additions and 1733 deletions

View file

@ -100,8 +100,8 @@ describe("ConfigController", () => {
expect(body).toStrictEqual({
message: "Invalid request data schema",
validationErrors: [
`"autoSwitchTheme" Expected boolean, received string`,
`"confidenceMode" Invalid enum value. Expected 'off' | 'on' | 'max', received 'pretty'`,
`"autoSwitchTheme" Expected boolean, received string`,
],
});

View file

@ -249,8 +249,8 @@ describe("PresetController", () => {
expect(body).toStrictEqual({
message: "Invalid request data schema",
validationErrors: [
`"config.autoSwitchTheme" Expected boolean, received string`,
`"config.confidenceMode" Invalid enum value. Expected 'off' | 'on' | 'max', received 'pretty'`,
`"config.autoSwitchTheme" Expected boolean, received string`,
`"config" Unrecognized key(s) in object: 'extra'`,
`Unrecognized key(s) in object: '_id', 'extra'`,
],
@ -427,9 +427,9 @@ describe("PresetController", () => {
expect(body).toStrictEqual({
message: "Invalid request data schema",
validationErrors: [
`"settingGroups.0" Invalid enum value. Expected 'test' | 'behavior' | 'input' | 'sound' | 'caret' | 'appearance' | 'theme' | 'hideElements' | 'ads' | 'hidden', received 'mappers'`,
`"config.autoSwitchTheme" Expected boolean, received string`,
`"settingGroups.0" Invalid enum value. Expected 'test' | 'behavior' | 'input' | 'sound' | 'caret' | 'appearance' | 'theme' | 'hideElements' | 'hidden' | 'ads', received 'mappers'`,
`"config.confidenceMode" Invalid enum value. Expected 'off' | 'on' | 'max', received 'pretty'`,
`"config.autoSwitchTheme" Expected boolean, received string`,
`"config" Unrecognized key(s) in object: 'extra'`,
`Unrecognized key(s) in object: 'extra'`,
],

View file

@ -1,12 +1,499 @@
import * as Config from "../../src/ts/config";
import * as Misc from "../../src/ts/utils/misc";
import {
CustomThemeColors,
FunboxName,
ConfigKey,
Config as ConfigType,
} from "@monkeytype/contracts/schemas/configs";
import { randomBytes } from "crypto";
import { vi } from "vitest";
import * as FunboxValidation from "../../src/ts/test/funbox/funbox-validation";
import * as ConfigValidation from "../../src/ts/config-validation";
import * as ConfigEvent from "../../src/ts/observables/config-event";
import * as DB from "../../src/ts/db";
import * as AccountButton from "../../src/ts/elements/account-button";
import * as Notifications from "../../src/ts/elements/notifications";
type TestsByConfig<T> = Partial<{
[K in keyof ConfigType]: (T & { value: ConfigType[K] })[];
}>;
const { configMetadata, replaceConfig, getConfig } = Config.__testing;
describe("Config", () => {
const isDevEnvironmentMock = vi.spyOn(Misc, "isDevEnvironment");
beforeEach(() => isDevEnvironmentMock.mockReset());
describe("configMeta", () => {
afterAll(() => {
replaceConfig({});
vi.resetModules();
});
it("should have changeRequiresRestart defined", () => {
const configsRequiringRestarts = Object.entries(configMetadata)
.filter(([_key, value]) => value.changeRequiresRestart === true)
.map(([key]) => key)
.sort();
expect(configsRequiringRestarts).toEqual(
[
"punctuation",
"numbers",
"words",
"time",
"mode",
"quoteLength",
"language",
"difficulty",
"minWpmCustomSpeed",
"minWpm",
"minAcc",
"minAccCustom",
"minBurst",
"minBurstCustomSpeed",
"britishEnglish",
"funbox",
"customLayoutfluid",
"strictSpace",
"stopOnError",
"lazyMode",
"layout",
"codeUnindentOnBackspace",
].sort()
);
});
it("should have triggerResize defined", () => {
const configsWithTriggeResize = Object.entries(configMetadata)
.filter(([_key, value]) => value.triggerResize === true)
.map(([key]) => key)
.sort();
expect(configsWithTriggeResize).toEqual(
["fontSize", "keymapSize", "maxLineWidth", "tapeMode"].sort()
);
});
it("should throw if config key in not found in metadata", () => {
expect(() => {
Config.genericSet("nonExistentKey" as ConfigKey, true);
}).toThrowError(
`Config metadata for key "nonExistentKey" is not defined.`
);
});
describe("overrideValue", () => {
const testCases: TestsByConfig<{
given?: Partial<ConfigType>;
expected: Partial<ConfigType>;
}> = {
punctuation: [
{ value: true, expected: { punctuation: true } },
{
value: true,
given: { mode: "quote" },
expected: { punctuation: false },
},
],
numbers: [
{ value: true, expected: { numbers: true } },
{
value: true,
given: { mode: "quote" },
expected: { numbers: false },
},
],
customLayoutfluid: [
{
value: ["qwerty", "qwerty", "qwertz"],
expected: { customLayoutfluid: ["qwerty", "qwertz"] },
},
],
customPolyglot: [
{
value: ["english", "polish", "english"],
expected: { customPolyglot: ["english", "polish"] },
},
],
keymapSize: [
{ value: 1, expected: { keymapSize: 1 } },
{ value: 1.234, expected: { keymapSize: 1.2 } },
{ value: 0.4, expected: { keymapSize: 0.5 } },
{ value: 3.6, expected: { keymapSize: 3.5 } },
],
customBackground: [
{
value: " https://example.com/test.jpg ",
expected: { customBackground: "https://example.com/test.jpg" },
},
],
accountChart: [
{
value: ["on", "off", "off", "off"],
expected: { accountChart: ["on", "off", "off", "off"] },
},
{
value: ["off", "off", "off", "off"],
given: { accountChart: ["on", "off", "off", "off"] },
expected: { accountChart: ["off", "on", "off", "off"] },
},
{
value: ["off", "off", "on", "on"],
given: { accountChart: ["off", "on", "off", "off"] },
expected: { accountChart: ["on", "off", "on", "on"] },
},
],
};
it.for(
Object.entries(testCases).flatMap(([key, value]) =>
value.flatMap((it) => ({ key: key as ConfigKey, ...it }))
)
)(
`$key value=$value given=$given expect=$expected`,
({ key, value, given, expected }) => {
//GIVEN
replaceConfig(given ?? {});
//WHEN
Config.genericSet(key, value as any);
//THEN
expect(getConfig()).toMatchObject(expected);
}
);
});
describe("isBlocked", () => {
const testCases: TestsByConfig<{
given?: Partial<ConfigType>;
fail?: true;
}> = {
funbox: [
{
value: "gibberish" as any,
given: { mode: "quote" },
fail: true,
},
],
showAllLines: [
{ value: true, given: { tapeMode: "off" } },
{ value: false, given: { tapeMode: "word" } },
{ value: true, given: { tapeMode: "word" }, fail: true },
],
};
it.for(
Object.entries(testCases).flatMap(([key, value]) =>
value.flatMap((it) => ({ key: key as ConfigKey, ...it }))
)
)(
`$key value=$value given=$given fail=$fail`,
({ key, value, given, fail }) => {
//GIVEN
replaceConfig(given ?? {});
//WHEN
const applied = Config.genericSet(key, value as any);
//THEN
expect(applied).toEqual(!fail);
}
);
});
describe("overrideConfig", () => {
const testCases: TestsByConfig<{
given: Partial<ConfigType>;
expected?: Partial<ConfigType>;
}> = {
mode: [
{ value: "time", given: { numbers: true, punctuation: true } },
{
value: "custom",
given: { numbers: true, punctuation: true },
expected: { numbers: false, punctuation: false },
},
{
value: "quote",
given: { numbers: true, punctuation: true },
expected: { numbers: false, punctuation: false },
},
{
value: "zen",
given: { numbers: true, punctuation: true },
expected: { numbers: false, punctuation: false },
},
],
numbers: [{ value: false, given: { mode: "quote" } }],
freedomMode: [
{
value: false,
given: { confidenceMode: "on" },
expected: { confidenceMode: "on" },
},
{
value: true,
given: { confidenceMode: "on" },
expected: { confidenceMode: "off" },
},
],
stopOnError: [
{
value: "off",
given: { confidenceMode: "on" },
expected: { confidenceMode: "on" },
},
{
value: "word",
given: { confidenceMode: "on" },
expected: { confidenceMode: "off" },
},
],
confidenceMode: [
{
value: "off",
given: { freedomMode: true, stopOnError: "word" },
expected: { freedomMode: true, stopOnError: "word" },
},
{
value: "on",
given: { freedomMode: true, stopOnError: "word" },
expected: { freedomMode: false, stopOnError: "off" },
},
],
tapeMode: [
{
value: "off",
given: { showAllLines: true },
expected: { showAllLines: true },
},
{
value: "letter",
given: { showAllLines: true },
expected: { showAllLines: false },
},
],
theme: [
{
value: "8008",
given: { customTheme: true },
expected: { customTheme: false },
},
],
};
it.for(
Object.entries(testCases).flatMap(([key, value]) =>
value.flatMap((it) => ({ key: key as ConfigKey, ...it }))
)
)(
`$key value=$value given=$given expected=$expected`,
({ key, value, given, expected }) => {
//GIVEN
replaceConfig(given);
//WHEN
Config.genericSet(key, value as any);
//THEN
expect(getConfig()).toMatchObject(expected ?? {});
}
);
});
describe("test with mocks", () => {
const canSetConfigWithCurrentFunboxesMock = vi.spyOn(
FunboxValidation,
"canSetConfigWithCurrentFunboxes"
);
const isConfigValueValidMock = vi.spyOn(
ConfigValidation,
"isConfigValueValid"
);
const dispatchConfigEventMock = vi.spyOn(ConfigEvent, "dispatch");
const dbSaveConfigMock = vi.spyOn(DB, "saveConfig");
const accountButtonLoadingMock = vi.spyOn(AccountButton, "loading");
const notificationAddMock = vi.spyOn(Notifications, "add");
const miscReloadAfterMock = vi.spyOn(Misc, "reloadAfter");
const mocks = [
canSetConfigWithCurrentFunboxesMock,
isConfigValueValidMock,
dispatchConfigEventMock,
dbSaveConfigMock,
accountButtonLoadingMock,
notificationAddMock,
miscReloadAfterMock,
];
beforeEach(async () => {
vi.useFakeTimers();
mocks.forEach((it) => it.mockReset());
vi.mock("../../src/ts/test/test-state", () => ({
isActive: true,
}));
isConfigValueValidMock.mockReturnValue(true);
canSetConfigWithCurrentFunboxesMock.mockReturnValue(true);
dbSaveConfigMock.mockResolvedValue();
});
afterAll(() => {
mocks.forEach((it) => it.mockRestore());
vi.useRealTimers();
});
it("cannot set if funbox disallows", () => {
//GIVEN
canSetConfigWithCurrentFunboxesMock.mockReturnValue(false);
//WHEN / THEN
expect(Config.genericSet("numbers", true)).toBe(false);
});
it("fails if config is invalid", () => {
//GIVEN
isConfigValueValidMock.mockReturnValue(false);
//WHEN / THEN
expect(Config.genericSet("numbers", "off" as any)).toBe(false);
});
it("dispatches event on set", () => {
//GIVEN
replaceConfig({ numbers: false });
//WHEN
Config.genericSet("numbers", true, true);
//THEN
expect(dispatchConfigEventMock).toHaveBeenCalledWith(
"numbers",
true,
true,
false
);
});
it("saves to localstorage if nosave=false", async () => {
//GIVEN
replaceConfig({ numbers: false });
//WHEN
Config.genericSet("numbers", true);
//THEN
//wait for debounce
await vi.advanceTimersByTimeAsync(2500);
//show loading
expect(accountButtonLoadingMock).toHaveBeenNthCalledWith(1, true);
//save
expect(dbSaveConfigMock).toHaveBeenCalledWith({ numbers: true });
//hide loading
expect(accountButtonLoadingMock).toHaveBeenNthCalledWith(2, false);
//send event
expect(dispatchConfigEventMock).toHaveBeenCalledWith(
"saveToLocalStorage",
expect.stringContaining("numbers")
);
});
it("does not save to localstorage if nosave=true", async () => {
//GIVEN
replaceConfig({ numbers: false });
//WHEN
Config.genericSet("numbers", true, true);
//THEN
//wait for debounce
await vi.advanceTimersByTimeAsync(2500);
expect(accountButtonLoadingMock).not.toHaveBeenCalled();
expect(dbSaveConfigMock).not.toHaveBeenCalled();
expect(dispatchConfigEventMock).not.toHaveBeenCalledWith(
"saveToLocalStorage",
expect.any(String)
);
});
it("calls afterSet", () => {
//GIVEN
isDevEnvironmentMock.mockReturnValue(false);
replaceConfig({ ads: "off" });
//WHEN
Config.genericSet("ads", "sellout");
//THEN
expect(notificationAddMock).toHaveBeenCalledWith(
"Ad settings changed. Refreshing...",
0
);
expect(miscReloadAfterMock).toHaveBeenCalledWith(3);
});
it("fails if test is active and funbox no_quit", () => {
//GIVEN
replaceConfig({ funbox: ["no_quit"], numbers: false });
//WHEN
expect(Config.genericSet("numbers", true, true)).toBe(false);
//THEN
expect(notificationAddMock).toHaveBeenCalledWith(
"No quit funbox is active. Please finish the test.",
0,
{
important: true,
}
);
});
it("sends configEvents for overrideConfigs", () => {
//GIVEN
replaceConfig({
confidenceMode: "off",
freedomMode: true,
stopOnError: "letter",
});
//WHEN
Config.genericSet("confidenceMode", "max");
//THEN
expect(dispatchConfigEventMock).toHaveBeenCalledWith(
"freedomMode",
false,
true,
true
);
expect(dispatchConfigEventMock).toHaveBeenCalledWith(
"stopOnError",
"off",
true,
"letter"
);
expect(dispatchConfigEventMock).toHaveBeenCalledWith(
"confidenceMode",
"max",
false,
"off"
);
});
});
});
it("setMode", () => {
expect(Config.setMode("zen")).toBe(true);
expect(Config.setMode("invalid" as any)).toBe(false);
@ -33,7 +520,7 @@ describe("Config", () => {
it("setAccountChart", () => {
expect(Config.setAccountChart(["on", "off", "off", "on"])).toBe(true);
//arrays not having 4 values will get [on, on, on, on] as default
expect(Config.setAccountChart(["on", "off"] as any)).toBe(true);
expect(Config.setAccountChart(["on", "off"] as any)).toBe(false);
expect(Config.setAccountChart(["on", "off", "on", "true"] as any)).toBe(
false
);
@ -199,13 +686,13 @@ describe("Config", () => {
//invalid values being "auto-fixed"
expect(Config.setKeymapSize(0)).toBe(true);
expect(Config.default.keymapSize).toBe(0.5);
expect(getConfig().keymapSize).toBe(0.5);
expect(Config.setKeymapSize(4)).toBe(true);
expect(Config.default.keymapSize).toBe(3.5);
expect(getConfig().keymapSize).toBe(3.5);
expect(Config.setKeymapSize(1.25)).toBe(true);
expect(Config.default.keymapSize).toBe(1.3);
expect(getConfig().keymapSize).toBe(1.3);
expect(Config.setKeymapSize(1.24)).toBe(true);
expect(Config.default.keymapSize).toBe(1.2);
expect(getConfig().keymapSize).toBe(1.2);
});
it("setCustomBackgroundSize", () => {
expect(Config.setCustomBackgroundSize("contain")).toBe(true);
@ -214,8 +701,10 @@ describe("Config", () => {
});
it("setCustomBackgroundFilter", () => {
expect(Config.setCustomBackgroundFilter([0, 1, 2, 3])).toBe(true);
//gets converted
expect(Config.setCustomBackgroundFilter([0, 1, 2, 3, 4] as any)).toBe(true);
expect(Config.setCustomBackgroundFilter([0, 1, 2, 3, 4] as any)).toBe(
false
);
expect(Config.setCustomBackgroundFilter([] as any)).toBe(false);
expect(Config.setCustomBackgroundFilter(["invalid"] as any)).toBe(false);
expect(Config.setCustomBackgroundFilter([1, 2, 3, 4, 5, 6] as any)).toBe(
@ -231,9 +720,7 @@ describe("Config", () => {
it("setCustomThemeColors", () => {
expect(Config.setCustomThemeColors(customThemeColors(10))).toBe(true);
//gets converted
expect(Config.setCustomThemeColors(customThemeColors(9))).toBe(true);
expect(Config.setCustomThemeColors(customThemeColors(9))).toBe(false);
expect(Config.setCustomThemeColors([] as any)).toBe(false);
expect(Config.setCustomThemeColors(["invalid"] as any)).toBe(false);
expect(Config.setCustomThemeColors(customThemeColors(5))).toBe(false);
@ -258,7 +745,7 @@ describe("Config", () => {
});
it("setAccountChart", () => {
expect(Config.setAccountChart(["on", "off", "off", "on"])).toBe(true);
expect(Config.setAccountChart(["on", "off"] as any)).toBe(true);
expect(Config.setAccountChart(["on", "off"] as any)).toBe(false);
expect(Config.setAccountChart(["on", "off", "on", "true"] as any)).toBe(
false
);
@ -358,8 +845,6 @@ describe("Config", () => {
expect(Config.setMinAccCustom(0)).toBe(true);
expect(Config.setMinAccCustom(1)).toBe(true);
expect(Config.setMinAccCustom(11.11)).toBe(true);
//gets converted
expect(Config.setMinAccCustom(120)).toBe(true);
expect(Config.setMinAccCustom("invalid" as any)).toBe(false);
expect(Config.setMinAccCustom(-1)).toBe(false);
@ -376,19 +861,12 @@ describe("Config", () => {
expect(Config.setTimeConfig(0)).toBe(true);
expect(Config.setTimeConfig(1)).toBe(true);
//gets converted
expect(Config.setTimeConfig("invalid" as any)).toBe(true);
expect(Config.setTimeConfig(-1)).toBe(true);
expect(Config.setTimeConfig(11.11)).toBe(false);
});
it("setWordCount", () => {
expect(Config.setWordCount(0)).toBe(true);
expect(Config.setWordCount(1)).toBe(true);
//gets converted
expect(Config.setWordCount(-1)).toBe(true);
expect(Config.setWordCount("invalid" as any)).toBe(false);
expect(Config.setWordCount(11.11)).toBe(false);
});
@ -486,12 +964,14 @@ describe("Config", () => {
expect(Config.setCustomBackground("invalid")).toBe(false);
});
it("setQuoteLength", () => {
expect(Config.setQuoteLength(0)).toBe(true);
expect(Config.setQuoteLength(-3)).toBe(true);
expect(Config.setQuoteLength(3)).toBe(true);
expect(Config.setQuoteLength([0])).toBe(true);
expect(Config.setQuoteLength([-3])).toBe(true);
expect(Config.setQuoteLength([3])).toBe(true);
expect(Config.setQuoteLength(-4 as any)).toBe(false);
expect(Config.setQuoteLength(4 as any)).toBe(false);
expect(Config.setQuoteLength(3 as any)).toBe(false);
expect(Config.setQuoteLength(2 as any)).toBe(false);
expect(Config.setQuoteLength([0, -3, 2])).toBe(true);

View file

@ -119,7 +119,7 @@ describe("url-handler", () => {
//THEN
expect(setModeMock).toHaveBeenCalledWith("quote", true);
expect(setQuoteLengthMock).toHaveBeenCalledWith(-2, false);
expect(setQuoteLengthMock).toHaveBeenCalledWith([-2], false);
expect(setSelectedQuoteIdMock).toHaveBeenCalledWith(512);
expect(restartTestMock).toHaveBeenCalled();
});

View file

@ -19,7 +19,7 @@ const commands: Command[] = [
configValue: [0, 1, 2, 3],
exec: (): void => {
UpdateConfig.setMode("quote");
UpdateConfig.setQuoteLength([0, 1, 2, 3]);
UpdateConfig.setQuoteLengthAll();
TestLogic.restart();
},
},
@ -30,7 +30,7 @@ const commands: Command[] = [
configValueMode: "include",
exec: (): void => {
UpdateConfig.setMode("quote");
UpdateConfig.setQuoteLength(0);
UpdateConfig.setQuoteLength([0]);
TestLogic.restart();
},
},
@ -41,7 +41,7 @@ const commands: Command[] = [
configValueMode: "include",
exec: (): void => {
UpdateConfig.setMode("quote");
UpdateConfig.setQuoteLength(1);
UpdateConfig.setQuoteLength([1]);
TestLogic.restart();
},
},
@ -52,7 +52,7 @@ const commands: Command[] = [
configValueMode: "include",
exec: (): void => {
UpdateConfig.setMode("quote");
UpdateConfig.setQuoteLength(2);
UpdateConfig.setQuoteLength([2]);
TestLogic.restart();
},
},
@ -63,7 +63,7 @@ const commands: Command[] = [
configValueMode: "include",
exec: (): void => {
UpdateConfig.setMode("quote");
UpdateConfig.setQuoteLength(3);
UpdateConfig.setQuoteLength([3]);
TestLogic.restart();
},
},
@ -77,7 +77,7 @@ const commands: Command[] = [
},
exec: (): void => {
UpdateConfig.setMode("quote");
UpdateConfig.setQuoteLength(-3);
UpdateConfig.setQuoteLength([-3]);
TestLogic.restart();
},
},

File diff suppressed because it is too large Load diff

View file

@ -415,7 +415,36 @@ window
}
});
let ignoreConfigEvent = false;
ConfigEvent.subscribe(async (eventKey, eventValue, nosave) => {
if (eventKey === "fullConfigChange") {
ignoreConfigEvent = true;
}
if (eventKey === "fullConfigChangeFinished") {
ignoreConfigEvent = false;
await clearRandom();
await clearPreview(false);
if (Config.autoSwitchTheme) {
if (window.matchMedia?.("(prefers-color-scheme: dark)").matches) {
await set(Config.themeDark, true);
} else {
await set(Config.themeLight, true);
}
} else {
if (Config.customTheme) {
await set("custom");
} else {
await set(Config.theme);
}
}
}
// this is here to prevent calling set / preview multiple times during a full config loading
// once the full config is loaded, we can apply everything once
if (ignoreConfigEvent) return;
if (eventKey === "randomTheme") {
void changeThemeList();
}
@ -430,23 +459,6 @@ ConfigEvent.subscribe(async (eventKey, eventValue, nosave) => {
await clearPreview(false);
await set(eventValue as string);
}
if (eventKey === "setThemes") {
await clearRandom();
await clearPreview(false);
if (Config.autoSwitchTheme) {
if (window.matchMedia?.("(prefers-color-scheme: dark)").matches) {
await set(Config.themeDark, true);
} else {
await set(Config.themeLight, true);
}
} else {
if (eventValue as boolean) {
await set("custom");
} else {
await set(Config.theme);
}
}
}
if (eventKey === "randomTheme" && eventValue === "off") await clearRandom();
if (eventKey === "customBackground") applyCustomBackground();
if (eventKey === "customBackgroundSize") applyCustomBackgroundSize();

View file

@ -930,7 +930,7 @@ export async function updateLbMemory<M extends Mode>(
}
}
export async function saveConfig(config: Config): Promise<void> {
export async function saveConfig(config: Partial<Config>): Promise<void> {
if (isAuthenticated()) {
const response = await Ape.configs.save({ body: config });
if (response.status !== 200) {

View file

@ -595,8 +595,40 @@ ConfigEvent.subscribe((eventKey, newValue) => {
void refresh();
}
if (eventKey === "keymapMode") {
$(".activeKey").removeClass("activeKey");
$(".keymapKey").attr("style", "");
newValue === "off" ? hide() : show();
}
if (eventKey === "keymapSize") {
$("#keymap").css("zoom", newValue as string);
}
if (eventKey === "keymapLegendStyle") {
let style = newValue as string;
// Remove existing styles
const keymapLegendStyles = ["lowercase", "uppercase", "blank", "dynamic"];
keymapLegendStyles.forEach((name) => {
$(".keymapLegendStyle").removeClass(name);
});
style = style || "lowercase";
// Mutate the keymap in the DOM, if it exists.
// 1. Remove everything
$(".keymapKey > .letter").css("display", "");
$(".keymapKey > .letter").css("text-transform", "");
// 2. Append special styles onto the DOM elements
if (style === "uppercase") {
$(".keymapKey > .letter").css("text-transform", "capitalize");
}
if (style === "blank") {
$(".keymapKey > .letter").css("display", "none");
}
// Update and save to cookie for persistence
$(".keymapLegendStyle").addClass(style);
}
});
KeymapEvent.subscribe((mode, key, correct) => {

View file

@ -6,7 +6,10 @@ import * as CustomTestDurationPopup from "./custom-test-duration";
import * as QuoteSearchModal from "./quote-search";
import * as CustomTextPopup from "./custom-text";
import AnimatedModal from "../utils/animated-modal";
import { QuoteLength } from "@monkeytype/contracts/schemas/configs";
import {
QuoteLength,
QuoteLengthConfig,
} from "@monkeytype/contracts/schemas/configs";
import { Mode } from "@monkeytype/contracts/schemas/shared";
function update(): void {
@ -126,22 +129,25 @@ async function setup(modalEl: HTMLElement): Promise<void> {
for (const button of quoteGroupButtons) {
button.addEventListener("click", (e) => {
const target = e.currentTarget as HTMLElement;
const len = parseInt(target.getAttribute("data-quoteLength") ?? "0", 10);
const len = parseInt(
target.getAttribute("data-quoteLength") ?? "0",
10
) as QuoteLength;
if (len === -2) {
void QuoteSearchModal.show({
modalChain: modal,
});
} else {
let newVal: number[] | number = len;
if (len === -1) {
newVal = [0, 1, 2, 3];
let arr: QuoteLengthConfig = [];
if ((e as MouseEvent).shiftKey) {
arr = [...Config.quoteLength, len];
} else {
arr = [len];
}
UpdateConfig.setQuoteLength(
newVal as QuoteLength | QuoteLength[],
false,
(e as MouseEvent).shiftKey
);
UpdateConfig.setQuoteLength(arr, false);
ManualRestart.set();
TestLogic.restart();
}

View file

@ -21,7 +21,6 @@ import * as TestState from "../test/test-state";
import AnimatedModal, { ShowOptions } from "../utils/animated-modal";
import * as TestLogic from "../test/test-logic";
import { createErrorMessage } from "../utils/misc";
import { QuoteLength } from "@monkeytype/contracts/schemas/configs";
const searchServiceCache: Record<string, SearchService<Quote>> = {};
@ -326,7 +325,7 @@ function apply(val: number): void {
);
}
if (val !== null && !isNaN(val) && val >= 0) {
UpdateConfig.setQuoteLength(-2 as QuoteLength, false);
UpdateConfig.setQuoteLength([-2], false);
TestState.setSelectedQuoteId(val);
ManualRestart.set();
} else {

View file

@ -39,6 +39,7 @@ import { findLineByLeastSquares } from "../utils/numbers";
import defaultResultFilters from "../constants/default-result-filters";
import { SnapshotResult } from "../constants/default-snapshot";
import Ape from "../ape";
import { AccountChart } from "@monkeytype/contracts/schemas/configs";
let filterDebug = false;
//toggle filterdebug
@ -1122,25 +1123,25 @@ function sortAndRefreshHistory(
}
$(".pageAccount button.toggleResultsOnChart").on("click", () => {
const newValue = Config.accountChart;
const newValue = [...Config.accountChart] as AccountChart;
newValue[0] = newValue[0] === "on" ? "off" : "on";
UpdateConfig.setAccountChart(newValue);
});
$(".pageAccount button.toggleAccuracyOnChart").on("click", () => {
const newValue = Config.accountChart;
const newValue = [...Config.accountChart] as AccountChart;
newValue[1] = newValue[1] === "on" ? "off" : "on";
UpdateConfig.setAccountChart(newValue);
});
$(".pageAccount button.toggleAverage10OnChart").on("click", () => {
const newValue = Config.accountChart;
const newValue = [...Config.accountChart] as AccountChart;
newValue[2] = newValue[2] === "on" ? "off" : "on";
UpdateConfig.setAccountChart(newValue);
});
$(".pageAccount button.toggleAverage100OnChart").on("click", () => {
const newValue = Config.accountChart;
const newValue = [...Config.accountChart] as AccountChart;
newValue[3] = newValue[3] === "on" ? "off" : "on";
UpdateConfig.setAccountChart(newValue);
});

View file

@ -270,11 +270,26 @@ export async function updatePosition(noAnim = false): Promise<void> {
}
}
function updateStyle(): void {
caret.style.width = "";
caret.classList.remove(
...["off", "default", "underline", "outline", "block", "carrot", "banana"]
);
caret.classList.add(Config.caretStyle);
}
subscribe((eventKey) => {
if (eventKey === "caretStyle") {
caret.style.width = "";
updateStyle();
void updatePosition(true);
}
if (eventKey === "smoothCaret") {
if (Config.smoothCaret === "off") {
caret.style.animationName = "caretFlashHard";
} else {
caret.style.animationName = "caretFlashSmooth";
}
}
});
export function show(noAnim = false): void {

View file

@ -322,6 +322,21 @@ export function start(): void {
void update(performance.now() + (settings?.spc ?? 0) * 1000);
}
function updateStyle(): void {
const paceCaret = $("#paceCaret");
paceCaret.removeClass([
"off",
"default",
"underline",
"outline",
"block",
"carrot",
"banana",
]);
paceCaret.addClass(Config.paceCaretStyle);
}
ConfigEvent.subscribe((eventKey) => {
if (eventKey === "paceCaret") void init();
if (eventKey === "paceCaretStyle") updateStyle();
});

View file

@ -7,6 +7,7 @@ import Config from "../config";
import * as ConfigEvent from "../observables/config-event";
import * as ActivePage from "../states/active-page";
import { applyReducedMotion } from "../utils/misc";
import { areUnsortedArraysEqual } from "../utils/arrays";
export function show(): void {
$("#testConfig").removeClass("invisible");
@ -225,11 +226,18 @@ export function updateExtras(key: string, value: ConfigValue): void {
).addClass("active");
} else if (key === "quoteLength") {
$("#testConfig .quoteLength .textButton").removeClass("active");
(value as QuoteLength[]).forEach((ql) => {
$(
"#testConfig .quoteLength .textButton[quoteLength='" + ql + "']"
).addClass("active");
});
if (areUnsortedArraysEqual(value as QuoteLength[], [0, 1, 2, 3])) {
$("#testConfig .quoteLength .textButton[quoteLength='-1']").addClass(
"active"
);
} else {
(value as QuoteLength[]).forEach((ql) => {
$(
"#testConfig .quoteLength .textButton[quoteLength='" + ql + "']"
).addClass("active");
});
}
} else if (key === "numbers") {
if (value === false) {
$("#testConfig .numbersMode.textButton").removeClass("active");

View file

@ -58,7 +58,10 @@ import * as KeymapEvent from "../observables/keymap-event";
import * as LayoutfluidFunboxTimer from "../test/funbox/layoutfluid-funbox-timer";
import * as ArabicLazyMode from "../states/arabic-lazy-mode";
import Format from "../utils/format";
import { QuoteLength } from "@monkeytype/contracts/schemas/configs";
import {
QuoteLength,
QuoteLengthConfig,
} from "@monkeytype/contracts/schemas/configs";
import { Mode } from "@monkeytype/contracts/schemas/shared";
import {
CompletedEvent,
@ -461,7 +464,7 @@ export async function init(): Promise<void | null> {
if (Config.mode === "quote") {
if (Config.quoteLength.includes(-3) && !isAuthenticated()) {
UpdateConfig.setQuoteLength(-1);
UpdateConfig.setQuoteLengthAll();
}
}
@ -1442,14 +1445,20 @@ $(".pageTest").on("click", "#testConfig .time .textButton", (e) => {
$(".pageTest").on("click", "#testConfig .quoteLength .textButton", (e) => {
if (TestUI.testRestarting) return;
let len: QuoteLength | QuoteLength[] = parseInt(
const len = parseInt(
$(e.currentTarget).attr("quoteLength") ?? "1"
) as QuoteLength;
if (len !== -2) {
if (len === -1) {
len = [0, 1, 2, 3];
let arr: QuoteLengthConfig = [];
if (e.shiftKey) {
arr = [...Config.quoteLength, len];
} else {
arr = [len];
}
if (UpdateConfig.setQuoteLength(len, false, e.shiftKey)) {
if (UpdateConfig.setQuoteLength(arr, false)) {
ManualRestart.set();
restart();
}

View file

@ -1756,4 +1756,7 @@ ConfigEvent.subscribe((key, value) => {
if (key === "timerColor") {
updateLiveStatsColor(value as TimerColor);
}
if (key === "showOutOfFocusWarning" && value === false) {
OutOfFocus.hide();
}
});

View file

@ -525,7 +525,7 @@ async function getQuoteWordList(
TestState.selectedQuoteId
);
if (targetQuote === undefined) {
UpdateConfig.setQuoteLength(-1);
UpdateConfig.setQuoteLengthAll();
throw new WordGenError(
`Quote ${TestState.selectedQuoteId} does not exist`
);
@ -536,14 +536,14 @@ async function getQuoteWordList(
Config.language
);
if (randomQuote === null) {
UpdateConfig.setQuoteLength(-1);
UpdateConfig.setQuoteLengthAll();
throw new WordGenError("No favorite quotes found");
}
rq = randomQuote;
} else {
const randomQuote = QuotesController.getRandomQuote();
if (randomQuote === null) {
UpdateConfig.setQuoteLength(-1);
UpdateConfig.setQuoteLengthAll();
throw new WordGenError("No quotes found for selected quote length");
}
rq = randomQuote;

View file

@ -117,6 +117,28 @@ $(window).on("resize", () => {
debouncedEvent();
});
ConfigEvent.subscribe((eventKey) => {
ConfigEvent.subscribe((eventKey, value) => {
if (eventKey === "quickRestart") updateKeytips();
if (eventKey === "showKeyTips") {
if (Config.showKeyTips) {
$("footer .keyTips").removeClass("hidden");
} else {
$("footer .keyTips").addClass("hidden");
}
}
if (eventKey === "fontSize") {
$("#caret, #paceCaret, #liveStatsMini, #typingTest, #wordsInput").css(
"fontSize",
value + "rem"
);
}
if (eventKey === "fontFamily") {
document.documentElement.style.setProperty(
"--font",
`"${(value as string).replace(
/_/g,
" "
)}", "Roboto Mono", "Vazirmatn", monospace`
);
}
});

View file

@ -156,5 +156,47 @@ export function replaceLegacyValues(
configObj.fontSize = newValue;
}
if (
Array.isArray(configObj.accountChart) &&
configObj.accountChart.length !== 4
) {
configObj.accountChart = ["on", "on", "on", "on"];
}
if (
typeof configObj.minAccCustom === "number" &&
configObj.minAccCustom > 100
) {
configObj.minAccCustom = 100;
}
if (
Array.isArray(configObj.customThemeColors) &&
//@ts-expect-error legacy configs
configObj.customThemeColors.length === 9
) {
// migrate existing configs missing sub alt color
const colors = configObj.customThemeColors;
colors.splice(4, 0, "#000000");
configObj.customThemeColors = colors;
}
if (
Array.isArray(configObj.customBackgroundFilter) &&
//@ts-expect-error legacy configs
configObj.customBackgroundFilter.length === 5
) {
const arr = configObj.customBackgroundFilter;
configObj.customBackgroundFilter = [arr[0], arr[1], arr[2], arr[3]];
}
if (typeof configObj.quoteLength === "number") {
if (configObj.quoteLength === -1) {
configObj.quoteLength = [0, 1, 2, 3];
} else {
configObj.quoteLength = [configObj.quoteLength];
}
}
return configObj;
}

View file

@ -188,7 +188,7 @@ export function loadTestSettingsFromUrl(getOverride?: string): void {
} else if (mode === "words") {
UpdateConfig.setWordCount(parseInt(de[1], 10), true);
} else if (mode === "quote") {
UpdateConfig.setQuoteLength(-2, false);
UpdateConfig.setQuoteLength([-2], false);
TestState.setSelectedQuoteId(parseInt(de[1], 10));
ManualRestart.set();
}

View file

@ -13,7 +13,6 @@ export type QuickRestart = z.infer<typeof QuickRestartSchema>;
export const QuoteLengthSchema = z.union([
z.literal(-3),
z.literal(-2),
z.literal(-1),
z.literal(0),
z.literal(1),
z.literal(2),
@ -21,7 +20,19 @@ export const QuoteLengthSchema = z.union([
]);
export type QuoteLength = z.infer<typeof QuoteLengthSchema>;
export const QuoteLengthConfigSchema = z.array(QuoteLengthSchema);
export const QuoteLengthConfigSchema = z
.array(QuoteLengthSchema)
.describe(
[
"|value|description|\n|-|-|",
"|-3|Favorite quotes|",
"|-2|Quote search|",
"|0|Short quotes|",
"|1|Medium quotes|",
"|2|Long quotes|",
"|3|Thicc quotes|",
].join("\n")
);
export type QuoteLengthConfig = z.infer<typeof QuoteLengthConfigSchema>;
export const CaretStyleSchema = z.enum([
@ -346,18 +357,7 @@ export type CustomBackground = z.infer<typeof CustomBackgroundSchema>;
export const ConfigSchema = z
.object({
theme: ThemeNameSchema,
themeLight: ThemeNameSchema,
themeDark: ThemeNameSchema,
autoSwitchTheme: z.boolean(),
customTheme: z.boolean(),
//customThemeId: token().nonnegative().max(24),
customThemeColors: CustomThemeColorsSchema,
favThemes: FavThemesSchema,
showKeyTips: z.boolean(),
smoothCaret: SmoothCaretSchema,
codeUnindentOnBackspace: z.boolean(),
quickRestart: QuickRestartSchema,
// test
punctuation: z.boolean(),
numbers: z.boolean(),
words: WordCountSchema,
@ -365,76 +365,105 @@ export const ConfigSchema = z
mode: Shared.ModeSchema,
quoteLength: QuoteLengthConfigSchema,
language: LanguageSchema,
fontSize: FontSizeSchema,
freedomMode: z.boolean(),
burstHeatmap: z.boolean(),
// behavior
difficulty: DifficultySchema,
quickRestart: QuickRestartSchema,
repeatQuotes: RepeatQuotesSchema,
blindMode: z.boolean(),
quickEnd: z.boolean(),
caretStyle: CaretStyleSchema,
paceCaretStyle: CaretStyleSchema,
flipTestColors: z.boolean(),
layout: LayoutSchema,
alwaysShowWordsHistory: z.boolean(),
singleListCommandLine: SingleListCommandLineSchema,
minWpm: MinimumWordsPerMinuteSchema,
minWpmCustomSpeed: MinWpmCustomSpeedSchema,
minAcc: MinimumAccuracySchema,
minAccCustom: MinimumAccuracyCustomSchema,
minBurst: MinimumBurstSchema,
minBurstCustomSpeed: MinimumBurstCustomSpeedSchema,
britishEnglish: z.boolean(),
funbox: FunboxSchema,
customLayoutfluid: CustomLayoutFluidSchema,
customPolyglot: CustomPolyglotSchema,
// input
freedomMode: z.boolean(),
strictSpace: z.boolean(),
oppositeShiftMode: OppositeShiftModeSchema,
stopOnError: StopOnErrorSchema,
confidenceMode: ConfidenceModeSchema,
quickEnd: z.boolean(),
indicateTypos: IndicateTyposSchema,
hideExtraLetters: z.boolean(),
lazyMode: z.boolean(),
layout: LayoutSchema,
codeUnindentOnBackspace: z.boolean(),
// sound
soundVolume: SoundVolumeSchema,
playSoundOnClick: PlaySoundOnClickSchema,
playSoundOnError: PlaySoundOnErrorSchema,
// caret
smoothCaret: SmoothCaretSchema,
caretStyle: CaretStyleSchema,
paceCaret: PaceCaretSchema,
paceCaretCustomSpeed: PaceCaretCustomSpeedSchema,
paceCaretStyle: CaretStyleSchema,
repeatedPace: z.boolean(),
// appearance
timerStyle: TimerStyleSchema,
liveSpeedStyle: LiveSpeedAccBurstStyleSchema,
liveAccStyle: LiveSpeedAccBurstStyleSchema,
liveBurstStyle: LiveSpeedAccBurstStyleSchema,
colorfulMode: z.boolean(),
randomTheme: RandomThemeSchema,
timerColor: TimerColorSchema,
timerOpacity: TimerOpacitySchema,
stopOnError: StopOnErrorSchema,
showAllLines: z.boolean(),
keymapMode: KeymapModeSchema,
keymapStyle: KeymapStyleSchema,
keymapLegendStyle: KeymapLegendStyleSchema,
keymapLayout: KeymapLayoutSchema,
keymapShowTopRow: KeymapShowTopRowSchema,
keymapSize: KeymapSizeSchema,
fontFamily: FontFamilySchema,
smoothLineScroll: z.boolean(),
alwaysShowDecimalPlaces: z.boolean(),
alwaysShowWordsHistory: z.boolean(),
singleListCommandLine: SingleListCommandLineSchema,
capsLockWarning: z.boolean(),
playSoundOnError: PlaySoundOnErrorSchema,
playSoundOnClick: PlaySoundOnClickSchema,
soundVolume: SoundVolumeSchema,
startGraphsAtZero: z.boolean(),
showOutOfFocusWarning: z.boolean(),
paceCaret: PaceCaretSchema,
paceCaretCustomSpeed: PaceCaretCustomSpeedSchema,
repeatedPace: z.boolean(),
accountChart: AccountChartSchema,
minWpm: MinimumWordsPerMinuteSchema,
minWpmCustomSpeed: MinWpmCustomSpeedSchema,
highlightMode: HighlightModeSchema,
tapeMode: TapeModeSchema,
tapeMargin: TapeMarginSchema,
smoothLineScroll: z.boolean(),
showAllLines: z.boolean(),
alwaysShowDecimalPlaces: z.boolean(),
typingSpeedUnit: TypingSpeedUnitSchema,
ads: AdsSchema,
hideExtraLetters: z.boolean(),
strictSpace: z.boolean(),
minAcc: MinimumAccuracySchema,
minAccCustom: MinimumAccuracyCustomSchema,
monkey: z.boolean(),
repeatQuotes: RepeatQuotesSchema,
oppositeShiftMode: OppositeShiftModeSchema,
startGraphsAtZero: z.boolean(),
maxLineWidth: MaxLineWidthSchema,
fontSize: FontSizeSchema,
fontFamily: FontFamilySchema,
keymapMode: KeymapModeSchema,
keymapLayout: KeymapLayoutSchema,
keymapStyle: KeymapStyleSchema,
keymapLegendStyle: KeymapLegendStyleSchema,
keymapShowTopRow: KeymapShowTopRowSchema,
keymapSize: KeymapSizeSchema,
// theme
flipTestColors: z.boolean(),
colorfulMode: z.boolean(),
customBackground: CustomBackgroundSchema,
customBackgroundSize: CustomBackgroundSizeSchema,
customBackgroundFilter: CustomBackgroundFilterSchema,
customLayoutfluid: CustomLayoutFluidSchema,
monkeyPowerLevel: MonkeyPowerLevelSchema,
minBurst: MinimumBurstSchema,
minBurstCustomSpeed: MinimumBurstCustomSpeedSchema,
burstHeatmap: z.boolean(),
britishEnglish: z.boolean(),
lazyMode: z.boolean(),
autoSwitchTheme: z.boolean(),
themeLight: ThemeNameSchema,
themeDark: ThemeNameSchema,
randomTheme: RandomThemeSchema,
favThemes: FavThemesSchema,
theme: ThemeNameSchema,
customTheme: z.boolean(),
customThemeColors: CustomThemeColorsSchema,
// hide elements
showKeyTips: z.boolean(),
showOutOfFocusWarning: z.boolean(),
capsLockWarning: z.boolean(),
showAverage: ShowAverageSchema,
maxLineWidth: MaxLineWidthSchema,
customPolyglot: CustomPolyglotSchema,
// other (hidden)
accountChart: AccountChartSchema,
monkey: z.boolean(),
monkeyPowerLevel: MonkeyPowerLevelSchema,
// ads
ads: AdsSchema,
} satisfies Record<string, ZodSchema>)
.strict();
@ -455,24 +484,14 @@ export const ConfigGroupNameSchema = z.enum([
"appearance",
"theme",
"hideElements",
"ads",
"hidden",
"ads",
]);
export type ConfigGroupName = z.infer<typeof ConfigGroupNameSchema>;
export const ConfigGroupsLiteral = {
theme: "theme",
themeLight: "theme",
themeDark: "theme",
autoSwitchTheme: "theme",
customTheme: "theme",
customThemeColors: "theme",
favThemes: "theme",
showKeyTips: "hideElements",
smoothCaret: "caret",
codeUnindentOnBackspace: "input",
quickRestart: "behavior",
//test
punctuation: "test",
numbers: "test",
words: "test",
@ -480,76 +499,105 @@ export const ConfigGroupsLiteral = {
mode: "test",
quoteLength: "test",
language: "test",
fontSize: "appearance",
freedomMode: "input",
burstHeatmap: "test",
//behavior
difficulty: "behavior",
quickRestart: "behavior",
repeatQuotes: "behavior",
blindMode: "behavior",
quickEnd: "input",
caretStyle: "caret",
paceCaretStyle: "caret",
flipTestColors: "theme",
layout: "input",
funbox: "behavior",
alwaysShowWordsHistory: "behavior",
singleListCommandLine: "behavior",
minWpm: "behavior",
minWpmCustomSpeed: "behavior",
minAcc: "behavior",
minAccCustom: "behavior",
minBurst: "behavior",
minBurstCustomSpeed: "behavior",
britishEnglish: "behavior",
funbox: "behavior", //todo: maybe move to test?
customLayoutfluid: "behavior",
customPolyglot: "behavior",
//input
freedomMode: "input",
strictSpace: "input",
oppositeShiftMode: "input",
stopOnError: "input",
confidenceMode: "input",
quickEnd: "input",
indicateTypos: "input",
hideExtraLetters: "input",
lazyMode: "input",
layout: "input",
codeUnindentOnBackspace: "input",
//sound
soundVolume: "sound",
playSoundOnClick: "sound",
playSoundOnError: "sound",
//caret
smoothCaret: "caret",
caretStyle: "caret",
paceCaret: "caret",
paceCaretCustomSpeed: "caret",
paceCaretStyle: "caret",
repeatedPace: "caret",
//appearance
timerStyle: "appearance",
liveSpeedStyle: "appearance",
liveAccStyle: "appearance",
liveBurstStyle: "appearance",
colorfulMode: "theme",
randomTheme: "theme",
timerColor: "appearance",
timerOpacity: "appearance",
stopOnError: "input",
showAllLines: "appearance",
keymapMode: "appearance",
keymapStyle: "appearance",
keymapLegendStyle: "appearance",
keymapLayout: "appearance",
keymapShowTopRow: "appearance",
keymapSize: "appearance",
fontFamily: "appearance",
smoothLineScroll: "appearance",
alwaysShowDecimalPlaces: "appearance",
alwaysShowWordsHistory: "behavior",
singleListCommandLine: "behavior",
capsLockWarning: "hideElements",
playSoundOnError: "sound",
playSoundOnClick: "sound",
soundVolume: "sound",
startGraphsAtZero: "appearance",
showOutOfFocusWarning: "hideElements",
paceCaret: "caret",
paceCaretCustomSpeed: "caret",
repeatedPace: "caret",
accountChart: "hidden",
minWpm: "behavior",
minWpmCustomSpeed: "behavior",
highlightMode: "appearance",
tapeMode: "appearance",
tapeMargin: "appearance",
smoothLineScroll: "appearance",
showAllLines: "appearance",
alwaysShowDecimalPlaces: "appearance",
typingSpeedUnit: "appearance",
ads: "ads",
hideExtraLetters: "input",
strictSpace: "input",
minAcc: "behavior",
minAccCustom: "behavior",
monkey: "hidden",
repeatQuotes: "behavior",
oppositeShiftMode: "input",
startGraphsAtZero: "appearance",
maxLineWidth: "appearance",
fontSize: "appearance",
fontFamily: "appearance",
keymapMode: "appearance",
keymapLayout: "appearance",
keymapStyle: "appearance",
keymapLegendStyle: "appearance",
keymapShowTopRow: "appearance",
keymapSize: "appearance",
//theme
flipTestColors: "theme",
colorfulMode: "theme",
customBackground: "theme",
customBackgroundSize: "theme",
customBackgroundFilter: "theme",
customLayoutfluid: "behavior",
monkeyPowerLevel: "hidden",
minBurst: "behavior",
minBurstCustomSpeed: "behavior",
burstHeatmap: "test",
britishEnglish: "behavior",
lazyMode: "input",
autoSwitchTheme: "theme",
themeLight: "theme",
themeDark: "theme",
randomTheme: "theme",
favThemes: "theme",
theme: "theme",
customTheme: "theme",
customThemeColors: "theme",
//hide elements
showKeyTips: "hideElements",
showOutOfFocusWarning: "hideElements",
capsLockWarning: "hideElements",
showAverage: "hideElements",
maxLineWidth: "appearance",
customPolyglot: "behavior",
//other
accountChart: "hidden",
monkey: "hidden",
monkeyPowerLevel: "hidden",
//ads
ads: "ads",
} as const satisfies Record<ConfigKey, ConfigGroupName>;
export type ConfigGroups = typeof ConfigGroupsLiteral;