feat: add font size and zoom settings

- Add font_size config field (10-24px, default 14px)
- Add keyboard shortcuts: Ctrl++/- to adjust, Ctrl+0 to reset
- Add font size slider in Settings > Appearance
- Apply font size to Terminal and InputBar via CSS variable
- Persist font size preference between sessions

Closes #19
This commit is contained in:
2026-01-23 18:16:45 -08:00
committed by Naomi Carrigan
parent 13c96a973a
commit 2db858080d
17 changed files with 596 additions and 255 deletions
+31 -27
View File
@@ -1,11 +1,11 @@
use tauri::{AppHandle, State};
use tauri_plugin_store::StoreExt;
use tauri_plugin_http::reqwest;
use tauri_plugin_store::StoreExt;
use crate::achievements::{get_achievement_info, load_achievements, AchievementUnlockedEvent};
use crate::bridge_manager::SharedBridgeManager;
use crate::config::{ClaudeStartOptions, HikariConfig};
use crate::stats::UsageStats;
use crate::bridge_manager::SharedBridgeManager;
use crate::achievements::{load_achievements, get_achievement_info, AchievementUnlockedEvent};
const CONFIG_STORE_KEY: &str = "config";
@@ -72,23 +72,17 @@ pub async fn select_wsl_directory() -> Result<String, String> {
#[tauri::command]
pub async fn get_config(app: AppHandle) -> Result<HikariConfig, String> {
let store = app
.store("hikari-config.json")
.map_err(|e| e.to_string())?;
let store = app.store("hikari-config.json").map_err(|e| e.to_string())?;
match store.get(CONFIG_STORE_KEY) {
Some(value) => {
serde_json::from_value(value.clone()).map_err(|e| e.to_string())
}
Some(value) => serde_json::from_value(value.clone()).map_err(|e| e.to_string()),
None => Ok(HikariConfig::default()),
}
}
#[tauri::command]
pub async fn save_config(app: AppHandle, config: HikariConfig) -> Result<(), String> {
let store = app
.store("hikari-config.json")
.map_err(|e| e.to_string())?;
let store = app.store("hikari-config.json").map_err(|e| e.to_string())?;
let value = serde_json::to_value(&config).map_err(|e| e.to_string())?;
store.set(CONFIG_STORE_KEY, value);
@@ -107,7 +101,10 @@ pub async fn get_usage_stats(
}
#[tauri::command]
pub async fn validate_directory(path: String, current_dir: Option<String>) -> Result<String, String> {
pub async fn validate_directory(
path: String,
current_dir: Option<String>,
) -> Result<String, String> {
use std::path::Path;
let path = Path::new(&path);
@@ -137,11 +134,17 @@ pub async fn validate_directory(path: String, current_dir: Option<String>) -> Re
// Check if the path exists and is a directory
if !expanded_path.exists() {
return Err(format!("Directory does not exist: {}", expanded_path.display()));
return Err(format!(
"Directory does not exist: {}",
expanded_path.display()
));
}
if !expanded_path.is_dir() {
return Err(format!("Path is not a directory: {}", expanded_path.display()));
return Err(format!(
"Path is not a directory: {}",
expanded_path.display()
));
}
// Return the canonicalized (absolute) path
@@ -152,7 +155,9 @@ pub async fn validate_directory(path: String, current_dir: Option<String>) -> Re
}
#[tauri::command]
pub async fn load_saved_achievements(app: AppHandle) -> Result<Vec<AchievementUnlockedEvent>, String> {
pub async fn load_saved_achievements(
app: AppHandle,
) -> Result<Vec<AchievementUnlockedEvent>, String> {
use chrono::Utc;
// Load achievements from persistent store
@@ -163,9 +168,7 @@ pub async fn load_saved_achievements(app: AppHandle) -> Result<Vec<AchievementUn
for achievement_id in &progress.unlocked {
let mut info = get_achievement_info(achievement_id);
info.unlocked_at = Some(Utc::now()); // We don't store timestamps, so just use now
events.push(AchievementUnlockedEvent {
achievement: info,
});
events.push(AchievementUnlockedEvent { achievement: info });
}
Ok(events)
@@ -184,12 +187,12 @@ pub async fn answer_question(
#[tauri::command]
pub async fn list_skills() -> Result<Vec<String>, String> {
use std::path::Path;
use std::fs;
use std::path::Path;
// Get the home directory
let home = std::env::var_os("HOME")
.ok_or_else(|| "Could not determine home directory".to_string())?;
let home =
std::env::var_os("HOME").ok_or_else(|| "Could not determine home directory".to_string())?;
let skills_dir = Path::new(&home).join(".claude").join("skills");
@@ -200,8 +203,8 @@ pub async fn list_skills() -> Result<Vec<String>, String> {
// Read the directory and collect skill names
let mut skills = Vec::new();
let entries = fs::read_dir(&skills_dir)
.map_err(|e| format!("Failed to read skills directory: {}", e))?;
let entries =
fs::read_dir(&skills_dir).map_err(|e| format!("Failed to read skills directory: {}", e))?;
for entry in entries {
let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
@@ -244,7 +247,8 @@ struct GiteaRelease {
#[tauri::command]
pub async fn check_for_updates() -> Result<UpdateInfo, String> {
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
const RELEASES_API: &str = "https://git.nhcarrigan.com/api/v1/repos/nhcarrigan/hikari-desktop/releases";
const RELEASES_API: &str =
"https://git.nhcarrigan.com/api/v1/repos/nhcarrigan/hikari-desktop/releases";
// Fetch releases from Gitea API
let client = reqwest::Client::new();
@@ -264,8 +268,8 @@ pub async fn check_for_updates() -> Result<UpdateInfo, String> {
.await
.map_err(|e| format!("Failed to read response: {}", e))?;
let releases: Vec<GiteaRelease> = serde_json::from_str(&text)
.map_err(|e| format!("Failed to parse releases: {}", e))?;
let releases: Vec<GiteaRelease> =
serde_json::from_str(&text).map_err(|e| format!("Failed to parse releases: {}", e))?;
// Find the latest non-prerelease, or fall back to latest prerelease
let latest = releases