generated from nhcarrigan/template
feat: multiple UI improvements, font settings, and memory file display names (#175)
## Summary - **fix**: `show_thinking_blocks` setting now persists across sessions — it was defined on the TypeScript side but missing from the Rust `HikariConfig` struct, so serde silently dropped it on every save/load - **feat**: Tool calls are now rendered as collapsible blocks matching the Extended Thinking block aesthetic, replacing the old inline dropdown approach - **feat**: Add configurable max output tokens setting - **feat**: Use random creative names for conversation tabs - **test**: Significantly expanded frontend unit test coverage - **docs**: Require tests for all changes in CLAUDE.md - **feat**: Allow users to specify a custom terminal font (Closes #176) - **feat**: Display friendly names for memory files derived from the first heading (Closes #177) - **feat**: Add custom UI font support for the app chrome (buttons, labels, tabs) - **fix**: Apply custom UI font to the full app interface — `.app-container` was hardcoded, blocking inheritance from `body`; also renamed "Custom Font" to "Custom Terminal Font" for clarity ✨ This PR was created with help from Hikari~ 🌸 Reviewed-on: #175 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #175.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { writable, derived } from "svelte/store";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { readFile } from "@tauri-apps/plugin-fs";
|
||||
|
||||
export type Theme = "dark" | "light" | "high-contrast" | "custom";
|
||||
export type BudgetAction = "warn" | "block";
|
||||
@@ -51,11 +52,19 @@ export interface HikariConfig {
|
||||
use_worktree: boolean;
|
||||
// Disable 1M context window
|
||||
disable_1m_context: boolean;
|
||||
// Max output tokens for Claude Code responses
|
||||
max_output_tokens: number | null;
|
||||
// Workspaces the user has explicitly trusted
|
||||
trusted_workspaces: string[];
|
||||
// Background image settings
|
||||
background_image_path: string | null;
|
||||
background_image_opacity: number;
|
||||
// Custom terminal font settings
|
||||
custom_font_path: string | null;
|
||||
custom_font_family: string | null;
|
||||
// Custom UI font settings
|
||||
custom_ui_font_path: string | null;
|
||||
custom_ui_font_family: string | null;
|
||||
}
|
||||
|
||||
const defaultConfig: HikariConfig = {
|
||||
@@ -98,9 +107,14 @@ const defaultConfig: HikariConfig = {
|
||||
show_thinking_blocks: true,
|
||||
use_worktree: false,
|
||||
disable_1m_context: false,
|
||||
max_output_tokens: null,
|
||||
trusted_workspaces: [],
|
||||
background_image_path: null,
|
||||
background_image_opacity: 0.3,
|
||||
custom_font_path: null,
|
||||
custom_font_family: null,
|
||||
custom_ui_font_path: null,
|
||||
custom_ui_font_family: null,
|
||||
};
|
||||
|
||||
function createConfigStore() {
|
||||
@@ -237,6 +251,16 @@ function createConfigStore() {
|
||||
setCompactMode: async (enabled: boolean) => {
|
||||
await updateConfig({ compact_mode: enabled });
|
||||
},
|
||||
|
||||
setCustomFont: async (path: string | null, family: string | null) => {
|
||||
await updateConfig({ custom_font_path: path, custom_font_family: family });
|
||||
await applyCustomFont(path, family);
|
||||
},
|
||||
|
||||
setCustomUiFont: async (path: string | null, family: string | null) => {
|
||||
await updateConfig({ custom_ui_font_path: path, custom_ui_font_family: family });
|
||||
await applyCustomUiFont(path, family);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -302,6 +326,99 @@ export function clampFontSize(size: number): number {
|
||||
return Math.max(MIN_FONT_SIZE, Math.min(MAX_FONT_SIZE, size));
|
||||
}
|
||||
|
||||
const DIRECT_FONT_EXTENSIONS = new Set(["woff", "woff2", "ttf", "otf", "eot"]);
|
||||
|
||||
const FONT_MIME_MAP: Record<string, string> = {
|
||||
woff: "font/woff",
|
||||
woff2: "font/woff2",
|
||||
ttf: "font/ttf",
|
||||
otf: "font/otf",
|
||||
eot: "application/vnd.ms-fontobject",
|
||||
};
|
||||
|
||||
async function applyFontFromSource(path: string, family: string, styleId: string): Promise<void> {
|
||||
const style = document.createElement("style");
|
||||
style.id = styleId;
|
||||
|
||||
if (path.startsWith("http://") || path.startsWith("https://")) {
|
||||
const ext = path.split(".").pop()?.toLowerCase() ?? "";
|
||||
|
||||
if (DIRECT_FONT_EXTENSIONS.has(ext)) {
|
||||
style.textContent = `@font-face { font-family: '${family}'; src: url('${path}'); }`;
|
||||
} else {
|
||||
style.textContent = `@import url('${path}');`;
|
||||
}
|
||||
} else {
|
||||
const data = await readFile(path);
|
||||
const chunks: string[] = [];
|
||||
const chunkSize = 8192;
|
||||
for (let i = 0; i < data.length; i += chunkSize) {
|
||||
chunks.push(String.fromCharCode(...data.slice(i, i + chunkSize)));
|
||||
}
|
||||
const ext = path.split(".").pop()?.toLowerCase() ?? "ttf";
|
||||
const mime = FONT_MIME_MAP[ext] ?? "font/ttf";
|
||||
const dataUrl = `data:${mime};base64,${btoa(chunks.join(""))}`;
|
||||
style.textContent = `@font-face { font-family: '${family}'; src: url('${dataUrl}'); }`;
|
||||
}
|
||||
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
export async function applyCustomFont(path: string | null, family: string | null): Promise<void> {
|
||||
if (typeof document === "undefined") return;
|
||||
|
||||
const styleId = "hikari-custom-font";
|
||||
const cssVar = "--terminal-font-family";
|
||||
const fallbackFamily = "HikariCustomFont";
|
||||
|
||||
document.getElementById(styleId)?.remove();
|
||||
|
||||
const trimmedPath = path?.trim() ?? "";
|
||||
const trimmedFamily = family?.trim() ?? "";
|
||||
|
||||
if (!trimmedPath && !trimmedFamily) {
|
||||
document.documentElement.style.removeProperty(cssVar);
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmedPath) {
|
||||
await applyFontFromSource(trimmedPath, trimmedFamily || fallbackFamily, styleId);
|
||||
}
|
||||
|
||||
if (trimmedFamily) {
|
||||
document.documentElement.style.setProperty(cssVar, `'${trimmedFamily}', monospace`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function applyCustomUiFont(path: string | null, family: string | null): Promise<void> {
|
||||
if (typeof document === "undefined") return;
|
||||
|
||||
const styleId = "hikari-custom-ui-font";
|
||||
const cssVar = "--ui-font-family";
|
||||
const fallbackFamily = "HikariCustomUiFont";
|
||||
|
||||
document.getElementById(styleId)?.remove();
|
||||
|
||||
const trimmedPath = path?.trim() ?? "";
|
||||
const trimmedFamily = family?.trim() ?? "";
|
||||
|
||||
if (!trimmedPath && !trimmedFamily) {
|
||||
document.documentElement.style.removeProperty(cssVar);
|
||||
document.body?.style.removeProperty("font-family");
|
||||
return;
|
||||
}
|
||||
|
||||
const effectiveFamily = trimmedFamily || fallbackFamily;
|
||||
|
||||
if (trimmedPath) {
|
||||
await applyFontFromSource(trimmedPath, effectiveFamily, styleId);
|
||||
}
|
||||
|
||||
const fontValue = `'${effectiveFamily}', sans-serif`;
|
||||
document.documentElement.style.setProperty(cssVar, fontValue);
|
||||
document.body?.style.setProperty("font-family", fontValue);
|
||||
}
|
||||
|
||||
export { MIN_FONT_SIZE, MAX_FONT_SIZE, DEFAULT_FONT_SIZE };
|
||||
|
||||
export const configStore = createConfigStore();
|
||||
|
||||
Reference in New Issue
Block a user