diff --git a/apps/desktop/src/lib/trpc/routers/settings/font-settings.test.ts b/apps/desktop/src/lib/trpc/routers/settings/font-settings.test.ts new file mode 100644 index 00000000000..2e843889141 --- /dev/null +++ b/apps/desktop/src/lib/trpc/routers/settings/font-settings.test.ts @@ -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); + }); + }); +}); diff --git a/apps/desktop/src/lib/trpc/routers/settings/font-settings.utils.ts b/apps/desktop/src/lib/trpc/routers/settings/font-settings.utils.ts new file mode 100644 index 00000000000..0b471592434 --- /dev/null +++ b/apps/desktop/src/lib/trpc/routers/settings/font-settings.utils.ts @@ -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; + +export function transformFontSettings( + input: SetFontSettingsInput, +): Record { + const set: Record = {}; + + 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; +} diff --git a/apps/desktop/src/lib/trpc/routers/settings/index.ts b/apps/desktop/src/lib/trpc/routers/settings/index.ts index 7b897347dc1..a4a0416c3eb 100644 --- a/apps/desktop/src/lib/trpc/routers/settings/index.ts +++ b/apps/desktop/src/lib/trpc/routers/settings/index.ts @@ -21,6 +21,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); @@ -569,6 +573,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; diff --git a/apps/desktop/src/renderer/providers/MonacoProvider/MonacoProvider.tsx b/apps/desktop/src/renderer/providers/MonacoProvider/MonacoProvider.tsx index 3264e271422..0876d05f6db 100644 --- a/apps/desktop/src/renderer/providers/MonacoProvider/MonacoProvider.tsx +++ b/apps/desktop/src/renderer/providers/MonacoProvider/MonacoProvider.tsx @@ -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 = { @@ -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, diff --git a/apps/desktop/src/renderer/providers/MonacoProvider/index.ts b/apps/desktop/src/renderer/providers/MonacoProvider/index.ts index 9c0a21f3940..65656ed95f7 100644 --- a/apps/desktop/src/renderer/providers/MonacoProvider/index.ts +++ b/apps/desktop/src/renderer/providers/MonacoProvider/index.ts @@ -4,5 +4,6 @@ export { monaco, registerSaveAction, SUPERSET_THEME, + useMonacoEditorOptions, useMonacoReady, } from "./MonacoProvider"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/AppearanceSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/AppearanceSettings.tsx index 7863aa7495c..1f4bb36a9db 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/AppearanceSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/AppearanceSettings.tsx @@ -1,27 +1,34 @@ -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@superset/ui/select"; -import { - type MarkdownStyle, - SYSTEM_THEME_ID, - useMarkdownStyle, - useSetMarkdownStyle, - useSetTheme, - useThemeId, - useThemeStore, -} from "renderer/stores"; -import { builtInThemes } from "shared/themes"; +import type { ReactNode } from "react"; import { isItemVisible, SETTING_ITEM_ID, type SettingItemId, } from "../../../utils/settings-search"; -import { SystemThemeCard } from "./components/SystemThemeCard"; -import { ThemeCard } from "./components/ThemeCard"; +import { CustomThemesSection } from "./components/CustomThemesSection"; +import { FontSettingSection } from "./components/FontSettingSection"; +import { MarkdownStyleSection } from "./components/MarkdownStyleSection"; +import { ThemeSection } from "./components/ThemeSection"; + +/** + * Renders a list of visible sections with automatic border separators. + * Each section is its own component that owns its data-fetching, + * so query resolutions in one section don't re-render others. + */ +function SectionList({ children }: { children: ReactNode[] }) { + const visibleChildren = children.filter(Boolean); + return ( +
+ {visibleChildren.map((child, i) => ( +
0 ? "pt-6 border-t mt-6" : ""} + > + {child} +
+ ))} +
+ ); +} interface AppearanceSettingsProps { visibleItems?: SettingItemId[] | null; @@ -36,19 +43,19 @@ export function AppearanceSettings({ visibleItems }: AppearanceSettingsProps) { SETTING_ITEM_ID.APPEARANCE_MARKDOWN, visibleItems, ); + const showEditorFont = isItemVisible( + SETTING_ITEM_ID.APPEARANCE_EDITOR_FONT, + visibleItems, + ); + const showTerminalFont = isItemVisible( + SETTING_ITEM_ID.APPEARANCE_TERMINAL_FONT, + visibleItems, + ); const showCustomThemes = isItemVisible( SETTING_ITEM_ID.APPEARANCE_CUSTOM_THEMES, visibleItems, ); - const activeThemeId = useThemeId(); - const setTheme = useSetTheme(); - const customThemes = useThemeStore((state) => state.customThemes); - const markdownStyle = useMarkdownStyle(); - const setMarkdownStyle = useSetMarkdownStyle(); - - const allThemes = [...builtInThemes, ...customThemes]; - return (
@@ -58,65 +65,17 @@ export function AppearanceSettings({ visibleItems }: AppearanceSettingsProps) {

-
- {/* Theme Section */} - {showTheme && ( -
-

Theme

-
- setTheme(SYSTEM_THEME_ID)} - /> - {allThemes.map((theme) => ( - setTheme(theme.id)} - /> - ))} -
-
+ + {showTheme && } + {showMarkdown && } + {showEditorFont && ( + )} - - {showMarkdown && ( -
-

Markdown Style

-

- Rendering style for markdown files when viewing rendered content -

- -

- Tufte style uses elegant serif typography inspired by Edward - Tufte's books -

-
- )} - - {showCustomThemes && ( -
-

Custom Themes

-

- Custom theme import coming soon. You'll be able to import JSON - theme files to create your own themes. -

-
+ {showTerminalFont && ( + )} -
+ {showCustomThemes && } +
); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/CustomThemesSection/CustomThemesSection.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/CustomThemesSection/CustomThemesSection.tsx new file mode 100644 index 00000000000..9494db8a8ec --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/CustomThemesSection/CustomThemesSection.tsx @@ -0,0 +1,11 @@ +export function CustomThemesSection() { + return ( +
+

Custom Themes

+

+ Custom theme import coming soon. You'll be able to import JSON theme + files to create your own themes. +

+
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/CustomThemesSection/index.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/CustomThemesSection/index.ts new file mode 100644 index 00000000000..1343be868d5 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/CustomThemesSection/index.ts @@ -0,0 +1 @@ +export { CustomThemesSection } from "./CustomThemesSection"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/FontPreview/FontPreview.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/FontPreview/FontPreview.tsx new file mode 100644 index 00000000000..df7c7b77f60 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/FontPreview/FontPreview.tsx @@ -0,0 +1,29 @@ +const FONT_PREVIEW_TEXT = + "The quick brown fox jumps over the lazy dog.\n0O1lI {}[]() => !== +- @#$%"; + +export function FontPreview({ + fontFamily, + fontSize, + variant, +}: { + fontFamily: string; + fontSize: number; + variant: "editor" | "terminal"; +}) { + const isTerminal = variant === "terminal"; + return ( +
+ {FONT_PREVIEW_TEXT} +
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/FontPreview/index.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/FontPreview/index.ts new file mode 100644 index 00000000000..79150bcdefc --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/FontPreview/index.ts @@ -0,0 +1 @@ +export { FontPreview } from "./FontPreview"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/FontSettingSection/FontSettingSection.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/FontSettingSection/FontSettingSection.tsx new file mode 100644 index 00000000000..faefac9f02e --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/FontSettingSection/FontSettingSection.tsx @@ -0,0 +1,178 @@ +import { Button } from "@superset/ui/button"; +import { Input } from "@superset/ui/input"; +import { useCallback, useEffect, useState } from "react"; +import { electronTrpc } from "renderer/lib/electron-trpc"; +import { MONACO_EDITOR_OPTIONS } from "renderer/providers/MonacoProvider"; +import { + DEFAULT_TERMINAL_FONT_FAMILY, + DEFAULT_TERMINAL_FONT_SIZE, +} from "renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/config"; +import { FontPreview } from "../FontPreview"; + +const DEFAULT_EDITOR_FONT_FAMILY = MONACO_EDITOR_OPTIONS.fontFamily; +const DEFAULT_EDITOR_FONT_SIZE = MONACO_EDITOR_OPTIONS.fontSize; + +const VARIANT_CONFIG = { + editor: { + title: "Editor Font", + description: "Font used in diff views and file editors", + defaultFamily: DEFAULT_EDITOR_FONT_FAMILY, + defaultSize: DEFAULT_EDITOR_FONT_SIZE, + familyKey: "editorFontFamily", + sizeKey: "editorFontSize", + }, + terminal: { + title: "Terminal Font", + description: "Font used in terminal panels.", + defaultFamily: DEFAULT_TERMINAL_FONT_FAMILY, + defaultSize: DEFAULT_TERMINAL_FONT_SIZE, + familyKey: "terminalFontFamily", + sizeKey: "terminalFontSize", + }, +} as const; + +interface FontSettingSectionProps { + variant: "editor" | "terminal"; +} + +export function FontSettingSection({ variant }: FontSettingSectionProps) { + const config = VARIANT_CONFIG[variant]; + + const utils = electronTrpc.useUtils(); + + const { data: fontSettings, isLoading } = + electronTrpc.settings.getFontSettings.useQuery(); + + const setFontSettings = electronTrpc.settings.setFontSettings.useMutation({ + onMutate: async (input) => { + await utils.settings.getFontSettings.cancel(); + const previous = utils.settings.getFontSettings.getData(); + utils.settings.getFontSettings.setData(undefined, (old) => ({ + terminalFontFamily: old?.terminalFontFamily ?? null, + terminalFontSize: old?.terminalFontSize ?? null, + editorFontFamily: old?.editorFontFamily ?? null, + editorFontSize: old?.editorFontSize ?? null, + ...input, + })); + return { previous }; + }, + onError: (_err, _vars, context) => { + if (context?.previous !== undefined) { + utils.settings.getFontSettings.setData(undefined, context.previous); + } + }, + onSettled: () => { + utils.settings.getFontSettings.invalidate(); + }, + }); + + const [fontDraft, setFontDraft] = useState(null); + const [fontSizeDraft, setFontSizeDraft] = useState(null); + + // biome-ignore lint/correctness/useExhaustiveDependencies: sync draft state when fontSettings changes + useEffect(() => { + setFontSizeDraft(null); + }, [fontSettings]); + + const currentFamily = fontSettings?.[config.familyKey] ?? null; + const currentSize = fontSettings?.[config.sizeKey] ?? null; + + const handleFontFamilyBlur = useCallback( + (e: React.FocusEvent) => { + const value = e.target.value.trim(); + setFontSettings.mutate({ + [config.familyKey]: value || null, + }); + }, + [setFontSettings, config.familyKey], + ); + + const handleFontSizeBlur = useCallback( + (e: React.FocusEvent) => { + const value = Number.parseInt(e.target.value, 10); + if (!Number.isNaN(value) && value >= 10 && value <= 24) { + setFontSettings.mutate({ [config.sizeKey]: value }); + } + }, + [setFontSettings, config.sizeKey], + ); + + const previewFamily = fontDraft ?? currentFamily ?? config.defaultFamily; + const previewSize = + (fontSizeDraft != null ? Number.parseInt(fontSizeDraft, 10) : undefined) || + currentSize || + config.defaultSize; + + return ( +
+

{config.title}

+

+ {config.description} + {variant === "terminal" && ( + <> + {" "} + + Nerd Fonts + {" "} + recommended for shell theme icons. + + )} +

+
+ setFontDraft(e.target.value)} + onBlur={(e) => { + handleFontFamilyBlur(e); + setFontDraft(null); + }} + disabled={isLoading} + className="flex-1" + /> + setFontSizeDraft(e.target.value)} + onBlur={(e) => { + handleFontSizeBlur(e); + setFontSizeDraft(null); + }} + disabled={isLoading} + className="w-20" + /> + {(currentFamily || currentSize) && ( + + )} +
+
+ +
+
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/FontSettingSection/index.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/FontSettingSection/index.ts new file mode 100644 index 00000000000..7aaa70e2699 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/FontSettingSection/index.ts @@ -0,0 +1 @@ +export { FontSettingSection } from "./FontSettingSection"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/MarkdownStyleSection/MarkdownStyleSection.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/MarkdownStyleSection/MarkdownStyleSection.tsx new file mode 100644 index 00000000000..7879be7b74d --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/MarkdownStyleSection/MarkdownStyleSection.tsx @@ -0,0 +1,42 @@ +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@superset/ui/select"; +import { + type MarkdownStyle, + useMarkdownStyle, + useSetMarkdownStyle, +} from "renderer/stores"; + +export function MarkdownStyleSection() { + const markdownStyle = useMarkdownStyle(); + const setMarkdownStyle = useSetMarkdownStyle(); + + return ( +
+

Markdown Style

+

+ Rendering style for markdown files when viewing rendered content +

+ +

+ Tufte style uses elegant serif typography inspired by Edward Tufte's + books +

+
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/MarkdownStyleSection/index.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/MarkdownStyleSection/index.ts new file mode 100644 index 00000000000..4aa9e23624f --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/MarkdownStyleSection/index.ts @@ -0,0 +1 @@ +export { MarkdownStyleSection } from "./MarkdownStyleSection"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/ThemeSection/ThemeSection.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/ThemeSection/ThemeSection.tsx new file mode 100644 index 00000000000..16aa2c2148d --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/ThemeSection/ThemeSection.tsx @@ -0,0 +1,37 @@ +import { + SYSTEM_THEME_ID, + useSetTheme, + useThemeId, + useThemeStore, +} from "renderer/stores"; +import { builtInThemes } from "shared/themes"; +import { SystemThemeCard } from "../SystemThemeCard"; +import { ThemeCard } from "../ThemeCard"; + +export function ThemeSection() { + const activeThemeId = useThemeId(); + const setTheme = useSetTheme(); + const customThemes = useThemeStore((state) => state.customThemes); + + const allThemes = [...builtInThemes, ...customThemes]; + + return ( +
+

Theme

+
+ setTheme(SYSTEM_THEME_ID)} + /> + {allThemes.map((theme) => ( + setTheme(theme.id)} + /> + ))} +
+
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/ThemeSection/index.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/ThemeSection/index.ts new file mode 100644 index 00000000000..c8536613cd6 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/ThemeSection/index.ts @@ -0,0 +1 @@ +export { ThemeSection } from "./ThemeSection"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.test.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.test.ts new file mode 100644 index 00000000000..5b860fb8946 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from "bun:test"; +import { + SETTING_ITEM_ID, + type SettingsItem, + searchSettings, +} from "./settings-search"; + +function getIds(items: SettingsItem[]): string[] { + return items.map((item) => item.id); +} + +describe("settings search - font settings", () => { + it('searching "font" returns both APPEARANCE_EDITOR_FONT and APPEARANCE_TERMINAL_FONT', () => { + const results = searchSettings("font"); + const ids = getIds(results); + expect(ids).toContain(SETTING_ITEM_ID.APPEARANCE_EDITOR_FONT); + expect(ids).toContain(SETTING_ITEM_ID.APPEARANCE_TERMINAL_FONT); + }); + + it('searching "terminal font" returns APPEARANCE_TERMINAL_FONT', () => { + const results = searchSettings("terminal font"); + const ids = getIds(results); + expect(ids).toContain(SETTING_ITEM_ID.APPEARANCE_TERMINAL_FONT); + }); + + it('searching "editor" returns APPEARANCE_EDITOR_FONT', () => { + const results = searchSettings("editor"); + const ids = getIds(results); + expect(ids).toContain(SETTING_ITEM_ID.APPEARANCE_EDITOR_FONT); + }); + + it('searching "monospace" returns both font items', () => { + const results = searchSettings("monospace"); + const ids = getIds(results); + expect(ids).toContain(SETTING_ITEM_ID.APPEARANCE_EDITOR_FONT); + expect(ids).toContain(SETTING_ITEM_ID.APPEARANCE_TERMINAL_FONT); + }); + + it('searching "Editor Font" is case-insensitive', () => { + const results = searchSettings("Editor Font"); + const ids = getIds(results); + expect(ids).toContain(SETTING_ITEM_ID.APPEARANCE_EDITOR_FONT); + }); + + it("empty search returns all settings items", () => { + const results = searchSettings(""); + expect(results.length).toBeGreaterThan(0); + const ids = getIds(results); + expect(ids).toContain(SETTING_ITEM_ID.APPEARANCE_EDITOR_FONT); + expect(ids).toContain(SETTING_ITEM_ID.APPEARANCE_TERMINAL_FONT); + }); + + it("font items have correct section", () => { + const results = searchSettings("font"); + const editorFont = results.find( + (r) => r.id === SETTING_ITEM_ID.APPEARANCE_EDITOR_FONT, + ); + const terminalFont = results.find( + (r) => r.id === SETTING_ITEM_ID.APPEARANCE_TERMINAL_FONT, + ); + + expect(editorFont?.section).toBe("appearance"); + expect(terminalFont?.section).toBe("appearance"); + }); +}); diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts index b69f8c9093e..89ef2fbbb29 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts @@ -15,6 +15,8 @@ export const SETTING_ITEM_ID = { APPEARANCE_THEME: "appearance-theme", APPEARANCE_MARKDOWN: "appearance-markdown", APPEARANCE_CUSTOM_THEMES: "appearance-custom-themes", + APPEARANCE_EDITOR_FONT: "appearance-editor-font", + APPEARANCE_TERMINAL_FONT: "appearance-terminal-font", RINGTONES_NOTIFICATION: "ringtones-notification", @@ -251,6 +253,42 @@ export const SETTINGS_ITEMS: SettingsItem[] = [ "customize", ], }, + { + id: SETTING_ITEM_ID.APPEARANCE_EDITOR_FONT, + section: "appearance", + title: "Editor Font", + description: "Font used in diff views and file editors", + keywords: [ + "appearance", + "font", + "family", + "size", + "editor", + "diff", + "mono", + "monospace", + "typography", + "custom", + ], + }, + { + id: SETTING_ITEM_ID.APPEARANCE_TERMINAL_FONT, + section: "appearance", + title: "Terminal Font", + description: "Font used in terminal panels", + keywords: [ + "appearance", + "font", + "family", + "size", + "terminal", + "mono", + "monospace", + "typography", + "custom", + "nerd", + ], + }, { id: SETTING_ITEM_ID.RINGTONES_NOTIFICATION, section: "ringtones", diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/components/DiffViewer/DiffViewer.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/components/DiffViewer/DiffViewer.tsx index 483d9489caa..09342e7bc36 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/components/DiffViewer/DiffViewer.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/components/DiffViewer/DiffViewer.tsx @@ -3,9 +3,9 @@ import type * as Monaco from "monaco-editor"; import { useCallback, useEffect, useRef, useState } from "react"; import { LuLoader } from "react-icons/lu"; import { - MONACO_EDITOR_OPTIONS, registerSaveAction, SUPERSET_THEME, + useMonacoEditorOptions, useMonacoReady, } from "renderer/providers/MonacoProvider"; import type { Tab } from "renderer/stores/tabs/types"; @@ -72,6 +72,7 @@ export function DiffViewer({ fitContent = false, }: DiffViewerProps) { const isMonacoReady = useMonacoReady(); + const monacoEditorOptions = useMonacoEditorOptions(); const diffEditorRef = useRef( null, ); @@ -272,7 +273,7 @@ export function DiffViewer({ } options={{ - ...MONACO_EDITOR_OPTIONS, + ...monacoEditorOptions, lineNumbersMinChars: getLineNumbersMinChars( contents.original, contents.modified, diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/FileViewerContent.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/FileViewerContent.tsx index e6d852f2dd2..7946b1ccc68 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/FileViewerContent.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/FileViewerContent.tsx @@ -4,9 +4,9 @@ import { type MutableRefObject, useCallback, useEffect, useRef } from "react"; import { LuLoader } from "react-icons/lu"; import { MarkdownRenderer } from "renderer/components/MarkdownRenderer"; import { - MONACO_EDITOR_OPTIONS, registerSaveAction, SUPERSET_THEME, + useMonacoEditorOptions, useMonacoReady, } from "renderer/providers/MonacoProvider"; import type { Tab } from "renderer/stores/tabs/types"; @@ -124,6 +124,7 @@ export function FileViewerContent({ }: FileViewerContentProps) { const isImage = isImageFile(filePath); const isMonacoReady = useMonacoReady(); + const monacoEditorOptions = useMonacoEditorOptions(); const hasAppliedInitialLocationRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: Reset on file change only @@ -346,7 +347,7 @@ export function FileViewerContent({ } options={{ - ...MONACO_EDITOR_OPTIONS, + ...monacoEditorOptions, contextmenu: false, // Disable Monaco's native context menu to use our custom one }} /> diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx index c56d766da75..5961cf81146 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx @@ -7,6 +7,10 @@ import { electronTrpc } from "renderer/lib/electron-trpc"; import { useTabsStore } from "renderer/stores/tabs/store"; import { useTerminalTheme } from "renderer/stores/theme"; import { ConnectionErrorOverlay, SessionKilledOverlay } from "./components"; +import { + DEFAULT_TERMINAL_FONT_FAMILY, + DEFAULT_TERMINAL_FONT_SIZE, +} from "./config"; import { getDefaultTerminalBg, type TerminalRendererRef } from "./helpers"; import { useFileLinkClick, @@ -303,6 +307,24 @@ export const Terminal = ({ paneId, tabId, workspaceId }: TerminalProps) => { xterm.options.theme = terminalTheme; }, [terminalTheme]); + const { data: fontSettings } = electronTrpc.settings.getFontSettings.useQuery( + undefined, + { + staleTime: 30_000, + }, + ); + + useEffect(() => { + const xterm = xtermRef.current; + if (!xterm || !fontSettings) return; + const family = + fontSettings.terminalFontFamily || DEFAULT_TERMINAL_FONT_FAMILY; + const size = fontSettings.terminalFontSize ?? DEFAULT_TERMINAL_FONT_SIZE; + xterm.options.fontFamily = family; + xterm.options.fontSize = size; + fitAddonRef.current?.fit(); + }, [fontSettings]); + const terminalBg = terminalTheme?.background ?? getDefaultTerminalBg(); const handleDragOver = (event: React.DragEvent) => { diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/config.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/config.ts index 33bb8cdc195..a9fed0033fe 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/config.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/config.ts @@ -13,7 +13,7 @@ export const DEBUG_TERMINAL = localStorage.getItem("SUPERSET_TERMINAL_DEBUG") === "1"; // Nerd Fonts first for shell theme compatibility (Oh My Posh, Powerlevel10k, etc.) -const TERMINAL_FONT_FAMILY = [ +export const DEFAULT_TERMINAL_FONT_FAMILY = [ "MesloLGM Nerd Font", "MesloLGM NF", "MesloLGS NF", @@ -31,10 +31,12 @@ const TERMINAL_FONT_FAMILY = [ "monospace", ].join(", "); +export const DEFAULT_TERMINAL_FONT_SIZE = 14; + export const TERMINAL_OPTIONS: ITerminalOptions = { cursorBlink: true, - fontSize: 14, - fontFamily: TERMINAL_FONT_FAMILY, + fontSize: DEFAULT_TERMINAL_FONT_SIZE, + fontFamily: DEFAULT_TERMINAL_FONT_FAMILY, theme: TERMINAL_THEME, allowProposedApi: true, scrollback: 10000, diff --git a/packages/local-db/src/schema/schema.ts b/packages/local-db/src/schema/schema.ts index 2360f6d82da..0458df90992 100644 --- a/packages/local-db/src/schema/schema.ts +++ b/packages/local-db/src/schema/schema.ts @@ -164,6 +164,10 @@ export const settings = sqliteTable("settings", { deleteLocalBranch: integer("delete_local_branch", { mode: "boolean" }), fileOpenMode: text("file_open_mode").$type(), showPresetsBar: integer("show_presets_bar", { mode: "boolean" }), + terminalFontFamily: text("terminal_font_family"), + terminalFontSize: integer("terminal_font_size"), + editorFontFamily: text("editor_font_family"), + editorFontSize: integer("editor_font_size"), }); export type InsertSettings = typeof settings.$inferInsert;