From d0e6a17c17a4051f337cb8ccf9ae4819333cecbe Mon Sep 17 00:00:00 2001 From: becker Date: Wed, 8 Oct 2025 13:01:09 -0700 Subject: [PATCH] Add history limit setting (#150) * Add history limit setting Dearest Reviewer, I have added a history limit setting. The limit change is applied instantly. If the limit is 0 then no recordings are saved. Becker * remove extra save save happens on exit * move to debug menu * use the text color --------- Co-authored-by: CJ Pais --- src-tauri/src/commands/history.rs | 18 +++++++- src-tauri/src/lib.rs | 9 ++-- src-tauri/src/managers/history.rs | 54 ++++++++++++++--------- src-tauri/src/settings.rs | 7 +++ src/components/settings/DebugSettings.tsx | 2 + src/components/settings/HistoryLimit.tsx | 48 ++++++++++++++++++++ src/components/settings/index.ts | 1 + src/lib/types.ts | 1 + src/stores/settingsStore.ts | 36 ++++++++------- 9 files changed, 136 insertions(+), 40 deletions(-) create mode 100644 src/components/settings/HistoryLimit.tsx diff --git a/src-tauri/src/commands/history.rs b/src-tauri/src/commands/history.rs index befa3f7..9857960 100644 --- a/src-tauri/src/commands/history.rs +++ b/src-tauri/src/commands/history.rs @@ -44,8 +44,24 @@ pub async fn delete_history_entry( id: i64, ) -> Result<(), String> { history_manager - .as_ref() .delete_entry(id) .await .map_err(|e| e.to_string()) } + +#[tauri::command] +pub async fn update_history_limit( + app: AppHandle, + history_manager: State<'_, Arc>, + limit: usize, +) -> Result<(), String> { + let mut settings = crate::settings::get_settings(&app); + settings.history_limit = limit; + crate::settings::write_settings(&app, settings); + + history_manager + .update_history_limit(limit) + .map_err(|e| e.to_string())?; + + Ok(()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9a26962..7d86123 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -162,8 +162,10 @@ pub fn run() { TranscriptionManager::new(&app, model_manager.clone()) .expect("Failed to initialize transcription manager"), ); - let history_manager = - Arc::new(HistoryManager::new(&app).expect("Failed to initialize history manager")); + let history_manager = Arc::new( + HistoryManager::new(&app, settings.history_limit) + .expect("Failed to initialize history manager"), + ); // Add managers to Tauri's managed state app.manage(recording_manager.clone()); @@ -243,7 +245,8 @@ pub fn run() { commands::history::get_history_entries, commands::history::toggle_history_entry_saved, commands::history::get_audio_file_path, - commands::history::delete_history_entry + commands::history::delete_history_entry, + commands::history::update_history_limit ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/managers/history.rs b/src-tauri/src/managers/history.rs index cd869e8..57a2d4b 100644 --- a/src-tauri/src/managers/history.rs +++ b/src-tauri/src/managers/history.rs @@ -5,13 +5,12 @@ use rusqlite::{params, Connection, OptionalExtension}; use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; +use std::sync::atomic::{AtomicUsize, Ordering}; use tauri::{App, AppHandle, Emitter, Manager}; use tauri_plugin_sql::{Migration, MigrationKind}; use crate::audio_toolkit::save_wav_file; -const HISTORY_LIMIT: usize = 5; - #[derive(Clone, Debug, Serialize, Deserialize)] pub struct HistoryEntry { pub id: i64, @@ -22,15 +21,15 @@ pub struct HistoryEntry { pub transcription_text: String, } -#[derive(Clone)] pub struct HistoryManager { app_handle: AppHandle, recordings_dir: PathBuf, db_path: PathBuf, + history_limit: AtomicUsize, } impl HistoryManager { - pub fn new(app: &App) -> Result { + pub fn new(app: &App, history_limit: usize) -> Result { let app_handle = app.app_handle().clone(); // Create recordings directory in app data dir @@ -48,6 +47,7 @@ impl HistoryManager { app_handle, recordings_dir, db_path, + history_limit: AtomicUsize::new(history_limit), }; // Initialize database @@ -99,6 +99,11 @@ impl HistoryManager { audio_samples: Vec, transcription_text: String, ) -> Result<()> { + // If history limit is 0, do not save at all. + if self.history_limit.load(Ordering::Relaxed) == 0 { + return Ok(()); + } + let timestamp = Utc::now().timestamp(); let file_name = format!("handy-{}.wav", timestamp); let title = self.format_timestamp_title(timestamp); @@ -155,8 +160,9 @@ impl HistoryManager { entries.push(row?); } - if entries.len() > HISTORY_LIMIT { - let entries_to_delete = &entries[HISTORY_LIMIT..]; + let limit = self.history_limit.load(Ordering::Relaxed); + if entries.len() > limit { + let entries_to_delete = &entries[limit..]; for (id, file_name) in entries_to_delete { // Delete database entry @@ -242,26 +248,28 @@ impl HistoryManager { let conn = self.get_connection()?; let mut stmt = conn.prepare( "SELECT id, file_name, timestamp, saved, title, transcription_text - FROM transcription_history WHERE id = ?1" + FROM transcription_history WHERE id = ?1", )?; - - let entry = stmt.query_row([id], |row| { - Ok(HistoryEntry { - id: row.get("id")?, - file_name: row.get("file_name")?, - timestamp: row.get("timestamp")?, - saved: row.get("saved")?, - title: row.get("title")?, - transcription_text: row.get("transcription_text")?, + + let entry = stmt + .query_row([id], |row| { + Ok(HistoryEntry { + id: row.get("id")?, + file_name: row.get("file_name")?, + timestamp: row.get("timestamp")?, + saved: row.get("saved")?, + title: row.get("title")?, + transcription_text: row.get("transcription_text")?, + }) }) - }).optional()?; - + .optional()?; + Ok(entry) } pub async fn delete_entry(&self, id: i64) -> Result<()> { let conn = self.get_connection()?; - + // Get the entry to find the file name if let Some(entry) = self.get_entry_by_id(id).await? { // Delete the audio file first @@ -273,7 +281,7 @@ impl HistoryManager { } } } - + // Delete from database conn.execute( "DELETE FROM transcription_history WHERE id = ?1", @@ -299,4 +307,10 @@ impl HistoryManager { format!("Recording {}", timestamp) } } + + pub fn update_history_limit(&self, new_limit: usize) -> Result<()> { + self.history_limit.swap(new_limit, Ordering::Relaxed); + self.cleanup_old_entries()?; + Ok(()) + } } diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 1427c1f..693f4ff 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -110,6 +110,8 @@ pub struct AppSettings { pub model_unload_timeout: ModelUnloadTimeout, #[serde(default = "default_word_correction_threshold")] pub word_correction_threshold: f64, + #[serde(default = "default_history_limit")] + pub history_limit: usize, #[serde(default)] pub paste_method: PasteMethod, } @@ -146,6 +148,10 @@ fn default_word_correction_threshold() -> f64 { 0.18 } +fn default_history_limit() -> usize { + 5 +} + pub const SETTINGS_STORE_PATH: &str = "settings_store.json"; pub fn get_default_settings() -> AppSettings { @@ -186,6 +192,7 @@ pub fn get_default_settings() -> AppSettings { custom_words: Vec::new(), model_unload_timeout: ModelUnloadTimeout::Never, word_correction_threshold: default_word_correction_threshold(), + history_limit: default_history_limit(), paste_method: PasteMethod::default(), } } diff --git a/src/components/settings/DebugSettings.tsx b/src/components/settings/DebugSettings.tsx index 55ae57d..f65ce97 100644 --- a/src/components/settings/DebugSettings.tsx +++ b/src/components/settings/DebugSettings.tsx @@ -2,6 +2,7 @@ import React from "react"; import { WordCorrectionThreshold } from "./debug/WordCorrectionThreshold"; import { AppDataDirectory } from "./AppDataDirectory"; import { SettingsGroup } from "../ui/SettingsGroup"; +import { HistoryLimit } from "./HistoryLimit"; export const DebugSettings: React.FC = () => { return ( @@ -9,6 +10,7 @@ export const DebugSettings: React.FC = () => { + ); diff --git a/src/components/settings/HistoryLimit.tsx b/src/components/settings/HistoryLimit.tsx new file mode 100644 index 0000000..d685cae --- /dev/null +++ b/src/components/settings/HistoryLimit.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import { useSettings } from "../../hooks/useSettings"; +import { Input } from "../ui/Input"; +import { SettingContainer } from "../ui/SettingContainer"; + +interface HistoryLimitProps { + descriptionMode?: "tooltip" | "inline"; + grouped?: boolean; +} + +export const HistoryLimit: React.FC = ({ + descriptionMode = "inline", + grouped = false, +}) => { + const { getSetting, updateSetting, isUpdating } = useSettings(); + + const historyLimit = getSetting("history_limit") ?? 5; + + const handleChange = async (event: React.ChangeEvent) => { + const value = parseInt(event.target.value, 10); + if (!isNaN(value) && value >= 0) { + updateSetting("history_limit", value); + } + }; + + return ( + +
+ + entries +
+
+ ); +}; diff --git a/src/components/settings/index.ts b/src/components/settings/index.ts index 81803fe..3c8af85 100644 --- a/src/components/settings/index.ts +++ b/src/components/settings/index.ts @@ -18,3 +18,4 @@ export { CustomWords } from "./CustomWords"; export { AppDataDirectory } from "./AppDataDirectory"; export { ModelUnloadTimeoutSetting } from "./ModelUnloadTimeout"; export { StartHidden } from "./StartHidden"; +export { HistoryLimit } from "./HistoryLimit"; diff --git a/src/lib/types.ts b/src/lib/types.ts index 28bcac7..2a95f06 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -53,6 +53,7 @@ export const SettingsSchema = z.object({ custom_words: z.array(z.string()).optional().default([]), model_unload_timeout: ModelUnloadTimeoutSchema.optional().default("never"), word_correction_threshold: z.number().optional().default(0.18), + history_limit: z.number().optional().default(5), paste_method: PasteMethodSchema.optional().default("ctrl_v"), }); diff --git a/src/stores/settingsStore.ts b/src/stores/settingsStore.ts index 31b4ffe..01905e2 100644 --- a/src/stores/settingsStore.ts +++ b/src/stores/settingsStore.ts @@ -45,6 +45,7 @@ const DEFAULT_SETTINGS: Partial = { overlay_position: "bottom", debug_mode: false, custom_words: [], + history_limit: 5, }; const DEFAULT_AUDIO_DEVICE: AudioDevice = { @@ -224,6 +225,9 @@ export const useSettingsStore = create()( case "paste_method": await invoke("change_paste_method_setting", { method: value }); break; + case "history_limit": + await invoke("update_history_limit", { limit: value }); + break; case "bindings": case "selected_model": break; @@ -265,15 +269,15 @@ export const useSettingsStore = create()( set((state) => ({ settings: state.settings ? { - ...state.settings, - bindings: { - ...state.settings.bindings, - [id]: { - ...state.settings.bindings[id], - current_binding: binding, - }, + ...state.settings, + bindings: { + ...state.settings.bindings, + [id]: { + ...state.settings.bindings[id], + current_binding: binding, }, - } + }, + } : null, })); @@ -286,15 +290,15 @@ export const useSettingsStore = create()( set((state) => ({ settings: state.settings ? { - ...state.settings, - bindings: { - ...state.settings.bindings, - [id]: { - ...state.settings.bindings[id], - current_binding: originalBinding, - }, + ...state.settings, + bindings: { + ...state.settings.bindings, + [id]: { + ...state.settings.bindings[id], + current_binding: originalBinding, }, - } + }, + } : null, })); }