Skip to content
167 changes: 167 additions & 0 deletions apps/desktop/src/lib/trpc/routers/settings/font-settings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { describe, expect, it } from "bun:test";
import {
setFontSettingsSchema,
transformFontSettings,
} from "./font-settings.utils";

describe("font settings validation", () => {
describe("font size validation (range 10-24)", () => {
it("accepts font size at minimum (10)", () => {
const result = setFontSettingsSchema.safeParse({
terminalFontSize: 10,
});
expect(result.success).toBe(true);
});

it("accepts font size at maximum (24)", () => {
const result = setFontSettingsSchema.safeParse({
editorFontSize: 24,
});
expect(result.success).toBe(true);
});

it("accepts font size in the middle of range", () => {
const result = setFontSettingsSchema.safeParse({
terminalFontSize: 14,
editorFontSize: 16,
});
expect(result.success).toBe(true);
});

it("rejects font size below minimum (< 10)", () => {
const result = setFontSettingsSchema.safeParse({
terminalFontSize: 9,
});
expect(result.success).toBe(false);
});

it("rejects font size of 0", () => {
const result = setFontSettingsSchema.safeParse({
editorFontSize: 0,
});
expect(result.success).toBe(false);
});

it("rejects font size above maximum (> 24)", () => {
const result = setFontSettingsSchema.safeParse({
terminalFontSize: 25,
});
expect(result.success).toBe(false);
});

it("rejects very large font size", () => {
const result = setFontSettingsSchema.safeParse({
editorFontSize: 100,
});
expect(result.success).toBe(false);
});

it("rejects non-integer font sizes", () => {
const result = setFontSettingsSchema.safeParse({
terminalFontSize: 14.5,
});
expect(result.success).toBe(false);
});

it("accepts null font size (reset)", () => {
const result = setFontSettingsSchema.safeParse({
terminalFontSize: null,
editorFontSize: null,
});
expect(result.success).toBe(true);
});
});

describe("font family trimming", () => {
it("trims whitespace from font family", () => {
const input = setFontSettingsSchema.parse({
terminalFontFamily: " JetBrains Mono ",
});
const result = transformFontSettings(input);
expect(result.terminalFontFamily).toBe("JetBrains Mono");
});

it("trims whitespace from editor font family", () => {
const input = setFontSettingsSchema.parse({
editorFontFamily: " Fira Code ",
});
const result = transformFontSettings(input);
expect(result.editorFontFamily).toBe("Fira Code");
});

it("accepts valid font families without modification", () => {
const input = setFontSettingsSchema.parse({
terminalFontFamily: "JetBrains Mono",
editorFontFamily: "Fira Code",
});
const result = transformFontSettings(input);
expect(result.terminalFontFamily).toBe("JetBrains Mono");
expect(result.editorFontFamily).toBe("Fira Code");
});

it("accepts common monospace fonts", () => {
const fonts = [
"JetBrains Mono",
"Fira Code",
"Source Code Pro",
"Cascadia Code",
"IBM Plex Mono",
"Hack",
"Inconsolata",
];

for (const font of fonts) {
const input = setFontSettingsSchema.parse({
terminalFontFamily: font,
});
const result = transformFontSettings(input);
expect(result.terminalFontFamily).toBe(font);
}
});
});

describe("empty string as null (reset)", () => {
it("treats empty string font family as null", () => {
const input = setFontSettingsSchema.parse({
terminalFontFamily: "",
});
const result = transformFontSettings(input);
expect(result.terminalFontFamily).toBeNull();
});

it("treats whitespace-only font family as null", () => {
const input = setFontSettingsSchema.parse({
editorFontFamily: " ",
});
const result = transformFontSettings(input);
expect(result.editorFontFamily).toBeNull();
});

it("treats null font family as null", () => {
const input = setFontSettingsSchema.parse({
terminalFontFamily: null,
});
const result = transformFontSettings(input);
expect(result.terminalFontFamily).toBeNull();
});
});

describe("partial updates", () => {
it("only includes provided fields in the result", () => {
const input = setFontSettingsSchema.parse({
terminalFontFamily: "Fira Code",
});
const result = transformFontSettings(input);
expect(result.terminalFontFamily).toBe("Fira Code");
expect(result).not.toHaveProperty("editorFontFamily");
expect(result).not.toHaveProperty("terminalFontSize");
expect(result).not.toHaveProperty("editorFontSize");
});

it("accepts empty input (no changes)", () => {
const input = setFontSettingsSchema.parse({});
const result = transformFontSettings(input);
expect(Object.keys(result)).toHaveLength(0);
});
});
});
31 changes: 31 additions & 0 deletions apps/desktop/src/lib/trpc/routers/settings/font-settings.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { z } from "zod";

export const setFontSettingsSchema = z.object({
terminalFontFamily: z.string().max(500).nullable().optional(),
terminalFontSize: z.number().int().min(10).max(24).nullable().optional(),
editorFontFamily: z.string().max(500).nullable().optional(),
editorFontSize: z.number().int().min(10).max(24).nullable().optional(),
});

export type SetFontSettingsInput = z.infer<typeof setFontSettingsSchema>;

export function transformFontSettings(
input: SetFontSettingsInput,
): Record<string, string | number | null> {
const set: Record<string, string | number | null> = {};

if (input.terminalFontFamily !== undefined) {
set.terminalFontFamily = input.terminalFontFamily?.trim() || null;
}
if (input.terminalFontSize !== undefined) {
set.terminalFontSize = input.terminalFontSize;
}
if (input.editorFontFamily !== undefined) {
set.editorFontFamily = input.editorFontFamily?.trim() || null;
}
if (input.editorFontSize !== undefined) {
set.editorFontSize = input.editorFontSize;
}

return set;
}
35 changes: 35 additions & 0 deletions apps/desktop/src/lib/trpc/routers/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import { DEFAULT_RINGTONE_ID, RINGTONES } from "shared/ringtones";
import { z } from "zod";
import { publicProcedure, router } from "../..";
import { getGitAuthorName, getGitHubUsername } from "../workspaces/utils/git";
import {
setFontSettingsSchema,
transformFontSettings,
} from "./font-settings.utils";

const VALID_RINGTONE_IDS = RINGTONES.map((r) => r.id);

Expand Down Expand Up @@ -495,6 +499,37 @@ export const createSettingsRouter = () => {
return { success: true };
}),

getFontSettings: publicProcedure.query(() => {
const row = getSettings();
return {
terminalFontFamily: row.terminalFontFamily ?? null,
terminalFontSize: row.terminalFontSize ?? null,
editorFontFamily: row.editorFontFamily ?? null,
editorFontSize: row.editorFontSize ?? null,
};
}),

setFontSettings: publicProcedure
.input(setFontSettingsSchema)
.mutation(({ input }) => {
const set = transformFontSettings(input);

if (Object.keys(set).length === 0) {
return { success: true };
}

localDb
.insert(settings)
.values({ id: 1, ...set })
.onConflictDoUpdate({
target: settings.id,
set,
})
.run();

return { success: true };
}),

// TODO: remove telemetry procedures once telemetry_enabled column is dropped
getTelemetryEnabled: publicProcedure.query(() => {
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
import type React from "react";
import { createContext, useContext, useEffect, useState } from "react";
import { createContext, useContext, useEffect, useMemo, useState } from "react";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { useMonacoTheme } from "renderer/stores/theme";

self.MonacoEnvironment = {
Expand Down Expand Up @@ -127,6 +128,31 @@ export const MONACO_EDITOR_OPTIONS = {
},
};

export function useMonacoEditorOptions() {
const { data: fontSettings } = electronTrpc.settings.getFontSettings.useQuery(
undefined,
{
staleTime: 30_000,
},
);

return useMemo(() => {
if (!fontSettings) return MONACO_EDITOR_OPTIONS;
const fontSize =
fontSettings.editorFontSize ?? MONACO_EDITOR_OPTIONS.fontSize;
return {
...MONACO_EDITOR_OPTIONS,
...(fontSettings.editorFontFamily && {
fontFamily: fontSettings.editorFontFamily,
}),
...(fontSettings.editorFontSize != null && {
fontSize,
lineHeight: Math.round(fontSize * 1.5),
}),
};
}, [fontSettings]);
}

export function registerSaveAction(
editor: monaco.editor.IStandaloneCodeEditor,
onSave: () => void,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export {
monaco,
registerSaveAction,
SUPERSET_THEME,
useMonacoEditorOptions,
useMonacoReady,
} from "./MonacoProvider";
Loading
Loading