diff --git a/Cargo.lock b/Cargo.lock index 49a2f3f05f..8de934b0ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1334,6 +1334,19 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types 0.5.0", + "libc", +] + [[package]] name = "core-graphics" version = "0.25.0" @@ -1342,11 +1355,22 @@ checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ "bitflags 2.11.1", "core-foundation 0.10.1", - "core-graphics-types", + "core-graphics-types 0.2.0", "foreign-types 0.5.0", "libc", ] +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + [[package]] name = "core-graphics-types" version = "0.2.0" @@ -1358,6 +1382,18 @@ dependencies = [ "libc", ] +[[package]] +name = "core-text" +version = "20.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5" +dependencies = [ + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types 0.5.0", + "libc", +] + [[package]] name = "cpubits" version = "0.1.1" @@ -1698,6 +1734,7 @@ dependencies = [ "csv", "dbx-core", "duckdb", + "font-kit", "futures", "log", "mongodb", @@ -1963,6 +2000,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + [[package]] name = "dlopen2" version = "0.8.2" @@ -2079,6 +2125,18 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dwrote" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b35532432acc8b19ceed096e35dfa088d3ea037fe4f3c085f1f97f33b4d02" +dependencies = [ + "lazy_static", + "libc", + "winapi", + "wio", +] + [[package]] name = "dyn-clone" version = "1.0.20" @@ -2393,6 +2451,12 @@ dependencies = [ "zlib-rs", ] +[[package]] +name = "float-ord" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" + [[package]] name = "flume" version = "0.11.1" @@ -2422,6 +2486,31 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "font-kit" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c7e611d49285d4c4b2e1727b72cf05353558885cc5252f93707b845dfcaf3d3" +dependencies = [ + "bitflags 2.11.1", + "byteorder", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "core-text", + "dirs", + "dwrote", + "float-ord", + "freetype-sys", + "lazy_static", + "libc", + "log", + "pathfinder_geometry", + "pathfinder_simd", + "walkdir", + "winapi", + "yeslogic-fontconfig-sys", +] + [[package]] name = "foreign-types" version = "0.3.2" @@ -2473,6 +2562,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "freetype-sys" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "fs_extra" version = "1.3.0" @@ -4903,6 +5003,25 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pathfinder_geometry" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3" +dependencies = [ + "log", + "pathfinder_simd", +] + +[[package]] +name = "pathfinder_simd" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4500030c302e4af1d423f36f3b958d1aecb6c04184356ed5a833bf6b60435777" +dependencies = [ + "rustc_version", +] + [[package]] name = "pbkdf2" version = "0.12.2" @@ -7262,7 +7381,7 @@ dependencies = [ "bitflags 2.11.1", "block2", "core-foundation 0.10.1", - "core-graphics", + "core-graphics 0.25.0", "crossbeam-channel", "dbus", "dispatch2", @@ -9452,6 +9571,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "wio" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" +dependencies = [ + "winapi", +] + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -9636,6 +9764,17 @@ dependencies = [ "rustix 1.1.4", ] +[[package]] +name = "yeslogic-fontconfig-sys" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8b8abf912b9a29ff112e1671c97c33636903d13a69712037190e6805af4f76" +dependencies = [ + "dlib", + "once_cell", + "pkg-config", +] + [[package]] name = "yoke" version = "0.8.2" diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index d7a5f6837e..b80edac80b 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -23,7 +23,7 @@ import { } from "@/stores/settingsStore"; import { loadEditorTheme, editorFontTheme } from "@/lib/editorThemes"; import { isTauriRuntime } from "@/lib/tauriRuntime"; -import { aiListModels, aiTestConnection, type AiModelInfo } from "@/lib/api"; +import { aiListModels, aiTestConnection, listSystemFonts, type AiModelInfo } from "@/lib/api"; import { eventToShortcut } from "@/lib/keyboardShortcuts"; import { SHORTCUT_DEFINITIONS, @@ -58,6 +58,51 @@ const editRedisScanPageSize = ref(settingsStore.editorSettings.redisScanPageSize const editShortcuts = ref(normalizeShortcutSettings(settingsStore.editorSettings.shortcuts)); const editSidebarActivation = ref(settingsStore.editorSettings.sidebarActivation); const redisScanPageSizeOptions = [200, 1000, 5000, 10000]; +const systemFonts = ref([]); +const systemFontsLoading = ref(false); +const systemFontsLoaded = ref(false); + +const presetFontLabels = new Map(FONT_FAMILIES.map((font) => [font.value, font.label])); + +function cssFontFamilyForName(name: string): string { + return `'${name.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}', monospace`; +} + +function readableFontFamily(value: string): string { + const first = value.split(",")[0]?.trim() ?? value; + return first.replace(/^['"]|['"]$/g, "").replace(/\\'/g, "'"); +} + +function normalizeCustomFontFamilyInput(value: string): string { + const trimmed = value.trim(); + if (!trimmed) return ""; + if (trimmed.includes(",") || trimmed.includes("'") || trimmed.includes('"')) return trimmed; + return cssFontFamilyForName(trimmed); +} + +const systemFontOptions = computed(() => { + const options = new Set(FONT_FAMILIES.map((font) => font.value)); + for (const font of systemFonts.value) options.add(cssFontFamilyForName(font)); + if (editFontFamily.value) options.add(editFontFamily.value); + return [...options]; +}); + +function displayFontFamily(value: string): string { + return presetFontLabels.get(value) ?? readableFontFamily(value); +} + +async function loadSystemFontOptions() { + if (systemFontsLoaded.value || systemFontsLoading.value) return; + systemFontsLoading.value = true; + try { + systemFonts.value = await listSystemFonts(); + systemFontsLoaded.value = true; + } catch { + systemFonts.value = []; + } finally { + systemFontsLoading.value = false; + } +} // Sync from store when dialog opens watch( @@ -73,6 +118,7 @@ watch( editRedisScanPageSize.value = settingsStore.editorSettings.redisScanPageSize; editShortcuts.value = normalizeShortcutSettings(settingsStore.editorSettings.shortcuts); editSidebarActivation.value = settingsStore.editorSettings.sidebarActivation; + void loadSystemFontOptions(); } }, ); @@ -187,6 +233,17 @@ const settingsCategoryNav = computed<{ value: SettingsCategory; label: string }[ ...(isWeb ? [{ value: "security" as const, label: t("settings.securityTab") }] : []), { value: "about", label: t("settings.aboutTab") }, ]); +const settingsTabsWithApplyFooter = new Set([ + "editor", + "appearance", + "navigation", + "redis", + "shortcuts", +]); + +function hasSettingsApplyFooter(value: SettingsCategory): boolean { + return settingsTabsWithApplyFooter.has(value); +} function settingsCategoryButton(value: SettingsCategory): string { return [ @@ -582,654 +639,634 @@ watch( -
-
-
- -
- - -
- - -
- - -
-
- - -
-
- - {{ editFontSize }}px -
- -
- 10px - - 24px -
-
- - - -
-
- - -
-
- -
-
-
- -

{{ t("settings.wordWrapDescription") }}

-
- -
-
- - - - -
- -
-
-
-
- - - -
- - - -
- -
-
- -
- - -
-
- - - -
- - - -
- -
-
- -
- - -
-
- - - -
- - - -
- -
-
- - -

{{ t("settings.redisScanPageSizeDescription") }}

-
- - - -
- - - -
- -
-
-
-
- -

- {{ t(`settings.shortcutScope${definition.scope[0].toUpperCase()}${definition.scope.slice(1)}`) }} -

-
-
-
- - -
-

- {{ t("settings.shortcutConflict") }} -

-
-
-
- - - -
- - - -
- -
-

{{ t("ai.settingsHint") }}

- -
-
- - -
- -
- - -
- -
- - -
- -
- -
-
- - - - - - -
-

{{ aiModelError }}

-

- {{ aiModelListSupported ? t("ai.modelListHint") : t("ai.modelListUnsupported") }} -

-
-
- -
- -
- - +
+
+
+ +
+ + + + + + +
+ + +
+ +
-
- -
- - - - - - - {{ t("ai.enableThinkingHint") }} - - + +
+
+ + {{ editFontSize }}px
-
- -
- - -
- -
- - -
-
- - -
- - {{ - t("connection.testSuccess") - }} - {{ aiTestError }} -
- - -
-
- -
-
- -

{{ t("auth.changePasswordDescription") }}

- - - -

- {{ passwordMessage }} -

-
- - - - -
- -
-
-
-
-
DBX
-

{{ t("settings.aboutDescription") }}

+
+ 10px + + 24px
+
+ + + +
+
+ + +
+ +
+
+ +

{{ t("settings.wordWrapDescription") }}

+
+ +
+
+ + + + +
+
- {{ displayedAppVersion }} +
-
+
-
- +
-
- QQ - {{ t("settings.qqGroup") }} - -
-
1087880322
- - - - -
+
+
- - + +
+ + + +
+
+ + +

{{ t("settings.redisScanPageSizeDescription") }}

+
+
+ +
+
+
+
+ +

+ {{ t(`settings.shortcutScope${definition.scope[0].toUpperCase()}${definition.scope.slice(1)}`) }} +

+
+
+
+ + +
+

+ {{ t("settings.shortcutConflict") }} +

+
+
+
+
+ +
+

{{ t("ai.settingsHint") }}

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ + + + + + +
+

{{ aiModelError }}

+

+ {{ aiModelListSupported ? t("ai.modelListHint") : t("ai.modelListUnsupported") }} +

+
+
+ +
+ +
+ + +
+
+ +
+ +
+ + + + + + + {{ t("ai.enableThinkingHint") }} + + +
+
+ +
+ + +
+ +
+ + +
+
+
+ +
+
+ +

{{ t("auth.changePasswordDescription") }}

+ + + +

+ {{ passwordMessage }} +

+
+
+ +
+
+
+
+
DBX
+

{{ t("settings.aboutDescription") }}

+
+
+ {{ displayedAppVersion }} +
+
+
+ +
+ + + + +
+
+ + + + +
+ + + + + +
+ - - + + {{ t("connection.testSuccess") }} + + + {{ aiTestError }} + +
+ + +
+ + + + + + + + +
diff --git a/apps/desktop/src/components/ui/searchable-select/SearchableSelect.vue b/apps/desktop/src/components/ui/searchable-select/SearchableSelect.vue index 54c6c53fd0..fb760e444b 100644 --- a/apps/desktop/src/components/ui/searchable-select/SearchableSelect.vue +++ b/apps/desktop/src/components/ui/searchable-select/SearchableSelect.vue @@ -17,13 +17,17 @@ const props = withDefaults( emptyText: string; loadingText: string; loading?: boolean; + allowCustom?: boolean; triggerClass?: HTMLAttributes["class"]; contentClass?: HTMLAttributes["class"]; displayName?: (option: string) => string; + normalizeCustom?: (value: string) => string; }>(), { loading: false, + allowCustom: false, displayName: (option: string) => option, + normalizeCustom: (value: string) => value, }, ); @@ -42,6 +46,10 @@ const selectedLabel = computed(() => { }); const filteredOptions = computed(() => filterDatabaseOptions(props.options, searchText.value, props.displayName)); +const customOptionValue = computed(() => props.normalizeCustom(searchText.value.trim())); +const canSelectCustom = computed( + () => props.allowCustom && !!customOptionValue.value && !props.options.includes(customOptionValue.value), +); watch(open, async (value) => { emit("update:open", value); @@ -58,6 +66,11 @@ function selectOption(option: string) { emit("update:modelValue", option); open.value = false; } + +function selectCustomOption() { + if (!canSelectCustom.value) return; + selectOption(customOptionValue.value); +} +
{{ emptyText }}
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 30ee23c9b5..c3f656875b 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1172,6 +1172,10 @@ export default { aboutTab: "About", fontFamily: "Font Family", selectFont: "Select font...", + searchFont: "Search fonts...", + loadingFonts: "Loading fonts...", + noFontsFound: "No fonts found", + useCustomFont: "Use “{font}”", fontSize: "Font Size", theme: "Theme", selectTheme: "Select theme...", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index c801bba8ad..ef18f2d5c4 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -1071,6 +1071,10 @@ export default { aboutTab: "Acerca de", fontFamily: "Familia tipográfica", selectFont: "Seleccionar fuente...", + searchFont: "Buscar fuentes...", + loadingFonts: "Cargando fuentes...", + noFontsFound: "No se encontraron fuentes", + useCustomFont: "Usar “{font}”", fontSize: "Tamaño de fuente", theme: "Tema", selectTheme: "Seleccionar tema...", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 791f1612d4..fd1aa3b00b 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1150,6 +1150,10 @@ export default { aboutTab: "关于我们", fontFamily: "字体", selectFont: "选择字体...", + searchFont: "搜索字体...", + loadingFonts: "正在加载字体...", + noFontsFound: "未找到字体", + useCustomFont: "使用“{font}”", fontSize: "字号", theme: "主题", selectTheme: "选择主题...", diff --git a/apps/desktop/src/lib/api.ts b/apps/desktop/src/lib/api.ts index 52d5c29525..fac2242ab4 100644 --- a/apps/desktop/src/lib/api.ts +++ b/apps/desktop/src/lib/api.ts @@ -99,6 +99,9 @@ export const saveAiConversation = forward("saveAiConversation"); export const loadAiConversations = forward("loadAiConversations"); export const deleteAiConversation = forward("deleteAiConversation"); +// System +export const listSystemFonts = forward("listSystemFonts"); + // SQL File Execution export const previewSqlFile = forward("previewSqlFile"); export const executeSqlFile = forward("executeSqlFile"); diff --git a/apps/desktop/src/lib/http.ts b/apps/desktop/src/lib/http.ts index c52303107c..6068c394cc 100644 --- a/apps/desktop/src/lib/http.ts +++ b/apps/desktop/src/lib/http.ts @@ -105,6 +105,10 @@ export async function loadConnections(): Promise { return get("/api/connection/list"); } +export async function listSystemFonts(): Promise { + return []; +} + export async function listPlugins(): Promise { return get("/api/plugins"); } diff --git a/apps/desktop/src/lib/tauri.ts b/apps/desktop/src/lib/tauri.ts index 64215a8069..8022f4fb3d 100644 --- a/apps/desktop/src/lib/tauri.ts +++ b/apps/desktop/src/lib/tauri.ts @@ -117,6 +117,10 @@ export async function loadAiConfig(): Promise { return invoke("load_ai_config"); } +export async function listSystemFonts(): Promise { + return invoke("list_system_fonts"); +} + export async function pendingOpenSqlFiles(): Promise { return invoke("pending_open_sql_files"); } diff --git a/packages/app-tests/editorSystemFonts.test.ts b/packages/app-tests/editorSystemFonts.test.ts new file mode 100644 index 0000000000..e3930bd32a --- /dev/null +++ b/packages/app-tests/editorSystemFonts.test.ts @@ -0,0 +1,32 @@ +import { readFileSync } from "node:fs"; +import { strict as assert } from "node:assert"; +import test from "node:test"; + +test("frontend API exposes system font loading", () => { + const api = readFileSync("apps/desktop/src/lib/api.ts", "utf8"); + const tauri = readFileSync("apps/desktop/src/lib/tauri.ts", "utf8"); + const http = readFileSync("apps/desktop/src/lib/http.ts", "utf8"); + + assert.match(api, /export const listSystemFonts = forward\("listSystemFonts"\)/); + assert.match(tauri, /invoke\("list_system_fonts"\)/); + assert.match(http, /export async function listSystemFonts\(\): Promise/); +}); + +test("settings editor font picker loads system fonts and accepts custom names", () => { + const source = readFileSync("apps/desktop/src/components/editor/EditorSettingsDialog.vue", "utf8"); + + assert.match(source, /listSystemFonts/); + assert.match(source, /systemFontOptions/); + assert.match(source, /allow-custom/); + assert.match(source, /normalizeCustomFontFamilyInput/); + assert.match(source, /settings\.useCustomFont/); + assert.doesNotMatch(source, /settings\.customFontFamily/); +}); + +test("Tauri registers the system font command", () => { + const commands = readFileSync("src-tauri/src/commands/mod.rs", "utf8"); + const lib = readFileSync("src-tauri/src/lib.rs", "utf8"); + + assert.match(commands, /pub mod system_fonts/); + assert.match(lib, /commands::system_fonts::list_system_fonts/); +}); diff --git a/packages/app-tests/settingsDialogLayout.test.ts b/packages/app-tests/settingsDialogLayout.test.ts index 5d3d97da1d..f0546a7c9f 100644 --- a/packages/app-tests/settingsDialogLayout.test.ts +++ b/packages/app-tests/settingsDialogLayout.test.ts @@ -22,8 +22,12 @@ test("Redis scan size lives in its own settings category", () => { }); test("settings action footer stays at the bottom of the content pane", () => { - assert.match(source, /class="[^"]*min-h-full[^"]*flex-col[^"]*"/); - assert.match(source, / { diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7ddda85c0c..9d7edfe867 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -50,3 +50,4 @@ csv = "1.4.0" calamine = "0.30.1" zip = "4.6.1" dbx-core = { path = "../crates/dbx-core" } +font-kit = "0.14.3" diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 75ab12f1fc..7a5dc86b56 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -17,6 +17,7 @@ pub mod saved_sql; pub mod schema; pub mod schema_cache; pub mod sql_file; +pub mod system_fonts; pub mod table_import; pub mod transfer; pub mod update; diff --git a/src-tauri/src/commands/system_fonts.rs b/src-tauri/src/commands/system_fonts.rs new file mode 100644 index 0000000000..ae4420be12 --- /dev/null +++ b/src-tauri/src/commands/system_fonts.rs @@ -0,0 +1,36 @@ +use std::collections::BTreeSet; + +use font_kit::source::SystemSource; + +pub fn normalize_font_families(families: Vec) -> Vec { + families + .into_iter() + .map(|family| family.trim().to_string()) + .filter(|family| !family.is_empty()) + .collect::>() + .into_iter() + .collect() +} + +#[tauri::command] +pub fn list_system_fonts() -> Result, String> { + SystemSource::new().all_families().map(normalize_font_families).map_err(|err| err.to_string()) +} + +#[cfg(test)] +mod tests { + use super::normalize_font_families; + + #[test] + fn normalizes_and_sorts_font_family_names() { + assert_eq!( + normalize_font_families(vec![ + "Maple Mono NF CN".to_string(), + " ".to_string(), + "Arial".to_string(), + "Maple Mono NF CN".to_string(), + ]), + vec!["Arial".to_string(), "Maple Mono NF CN".to_string()], + ); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b63222b27b..6c0468a1ba 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -278,6 +278,7 @@ pub fn run() { commands::agents::invalidate_agent_registry_cache, commands::agents::import_agents_from_zip, commands::agents::import_agent_jar_cmd, + commands::system_fonts::list_system_fonts, ]) .build(tauri::generate_context!()) .expect("error while building tauri application")