Add Setting to Copy to Clipboard (#220)

* update transcribe-rs

* add copy to clipboard feat
This commit is contained in:
CJ Pais 2025-10-17 16:25:35 -07:00 committed by GitHub
parent 93215dc097
commit db00ff240f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 102 additions and 7 deletions

4
src-tauri/Cargo.lock generated
View file

@ -6924,9 +6924,9 @@ dependencies = [
[[package]]
name = "transcribe-rs"
version = "0.1.3"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77dbb8e1048585b7f0bc216d87025953e2f7b04a3e4f4489948f18ab847486a9"
checksum = "856646076d3d9739998ba693ebfff3afe95c0cdb71d4fead32e761b11496094f"
dependencies = [
"async-openai",
"async-trait",

View file

@ -64,7 +64,7 @@ rusqlite = { version = "0.32", features = ["bundled"] }
tauri-plugin-sql = { version = "2", features = ["sqlite"] }
tar = "0.4.44"
flate2 = "1.0"
transcribe-rs = "0.1.3"
transcribe-rs = "0.1.4"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = "2"

View file

@ -1,4 +1,4 @@
use crate::settings::{get_settings, PasteMethod};
use crate::settings::{get_settings, ClipboardHandling, PasteMethod};
use enigo::Enigo;
use enigo::Key;
use enigo::Keyboard;
@ -83,8 +83,19 @@ pub fn paste(text: String, app_handle: AppHandle) -> Result<(), String> {
println!("Using paste method: {:?}", paste_method);
// Perform the paste operation
match paste_method {
PasteMethod::CtrlV => paste_via_clipboard(&text, &app_handle),
PasteMethod::Direct => paste_via_direct_input(&text),
PasteMethod::CtrlV => paste_via_clipboard(&text, &app_handle)?,
PasteMethod::Direct => paste_via_direct_input(&text)?,
}
// After pasting, optionally copy to clipboard based on settings
if settings.clipboard_handling == ClipboardHandling::CopyToClipboard {
let clipboard = app_handle.clipboard();
clipboard
.write_text(&text)
.map_err(|e| format!("Failed to copy to clipboard: {}", e))?;
}
Ok(())
}

View file

@ -233,6 +233,7 @@ pub fn run() {
shortcut::change_debug_mode_setting,
shortcut::change_word_correction_threshold_setting,
shortcut::change_paste_method_setting,
shortcut::change_clipboard_handling_setting,
shortcut::update_custom_words,
shortcut::suspend_binding,
shortcut::resume_binding,

View file

@ -40,6 +40,13 @@ pub enum PasteMethod {
Direct,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ClipboardHandling {
DontModify,
CopyToClipboard,
}
impl Default for ModelUnloadTimeout {
fn default() -> Self {
ModelUnloadTimeout::Never
@ -56,6 +63,12 @@ impl Default for PasteMethod {
}
}
impl Default for ClipboardHandling {
fn default() -> Self {
ClipboardHandling::DontModify
}
}
impl ModelUnloadTimeout {
pub fn to_minutes(self) -> Option<u64> {
match self {
@ -146,6 +159,8 @@ pub struct AppSettings {
pub history_limit: usize,
#[serde(default)]
pub paste_method: PasteMethod,
#[serde(default)]
pub clipboard_handling: ClipboardHandling,
}
fn default_model() -> String {
@ -244,6 +259,7 @@ pub fn get_default_settings() -> AppSettings {
word_correction_threshold: default_word_correction_threshold(),
history_limit: default_history_limit(),
paste_method: PasteMethod::default(),
clipboard_handling: ClipboardHandling::default(),
}
}

View file

@ -5,7 +5,7 @@ use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutState};
use crate::actions::ACTION_MAP;
use crate::settings::ShortcutBinding;
use crate::settings::{self, get_settings, OverlayPosition, PasteMethod, SoundTheme};
use crate::settings::{self, get_settings, ClipboardHandling, OverlayPosition, PasteMethod, SoundTheme};
use crate::ManagedToggleState;
pub fn init_shortcuts(app: &AppHandle) {
@ -276,6 +276,22 @@ pub fn change_paste_method_setting(app: AppHandle, method: String) -> Result<(),
Ok(())
}
#[tauri::command]
pub fn change_clipboard_handling_setting(app: AppHandle, handling: String) -> Result<(), String> {
let mut settings = settings::get_settings(&app);
let parsed = match handling.as_str() {
"dont_modify" => ClipboardHandling::DontModify,
"copy_to_clipboard" => ClipboardHandling::CopyToClipboard,
other => {
eprintln!("Invalid clipboard handling '{}', defaulting to dont_modify", other);
ClipboardHandling::DontModify
}
};
settings.clipboard_handling = parsed;
settings::write_settings(&app, settings);
Ok(())
}
/// Determine whether a shortcut string contains at least one non-modifier key.
/// We allow single non-modifier keys (e.g. "f5" or "space") but disallow
/// modifier-only combos (e.g. "ctrl" or "ctrl+shift").

View file

@ -0,0 +1,43 @@
import React from "react";
import { Dropdown } from "../ui/Dropdown";
import { SettingContainer } from "../ui/SettingContainer";
import { useSettings } from "../../hooks/useSettings";
import type { ClipboardHandling } from "../../lib/types";
interface ClipboardHandlingProps {
descriptionMode?: "inline" | "tooltip";
grouped?: boolean;
}
const clipboardHandlingOptions = [
{ value: "dont_modify", label: "Don't Modify Clipboard" },
{ value: "copy_to_clipboard", label: "Copy to Clipboard" },
];
export const ClipboardHandlingSetting: React.FC<ClipboardHandlingProps> = React.memo(({
descriptionMode = "tooltip",
grouped = false,
}) => {
const { getSetting, updateSetting, isUpdating } = useSettings();
const selectedHandling = (getSetting("clipboard_handling") ||
"dont_modify") as ClipboardHandling;
return (
<SettingContainer
title="Clipboard Handling"
description="Don't Modify Clipboard preserves your current clipboard contents after transcription. Copy to Clipboard leaves the transcription result in your clipboard after pasting."
descriptionMode={descriptionMode}
grouped={grouped}
>
<Dropdown
options={clipboardHandlingOptions}
selectedValue={selectedHandling}
onSelect={(value) =>
updateSetting("clipboard_handling", value as ClipboardHandling)
}
disabled={isUpdating("clipboard_handling")}
/>
</SettingContainer>
);
});

View file

@ -4,6 +4,7 @@ import { AppDataDirectory } from "./AppDataDirectory";
import { SettingsGroup } from "../ui/SettingsGroup";
import { HistoryLimit } from "./HistoryLimit";
import { PasteMethodSetting } from "./PasteMethod";
import { ClipboardHandlingSetting } from "./ClipboardHandling";
import { AlwaysOnMicrophone } from "./AlwaysOnMicrophone";
import { SoundPicker } from "./SoundPicker";
@ -12,6 +13,7 @@ export const DebugSettings: React.FC = () => {
<div className="max-w-3xl w-full mx-auto space-y-6">
<SettingsGroup title="Debug">
<PasteMethodSetting descriptionMode="tooltip" grouped={true} />
<ClipboardHandlingSetting descriptionMode="tooltip" grouped={true} />
<SoundPicker
label="Sound Theme"
description="Choose a sound theme for recording start and stop feedback"

View file

@ -37,6 +37,9 @@ export type ModelUnloadTimeout = z.infer<typeof ModelUnloadTimeoutSchema>;
export const PasteMethodSchema = z.enum(["ctrl_v", "direct"]);
export type PasteMethod = z.infer<typeof PasteMethodSchema>;
export const ClipboardHandlingSchema = z.enum(["dont_modify", "copy_to_clipboard"]);
export type ClipboardHandling = z.infer<typeof ClipboardHandlingSchema>;
export const SettingsSchema = z.object({
bindings: ShortcutBindingsMapSchema,
push_to_talk: z.boolean(),
@ -61,6 +64,7 @@ export const SettingsSchema = z.object({
word_correction_threshold: z.number().optional().default(0.18),
history_limit: z.number().optional().default(5),
paste_method: PasteMethodSchema.optional().default("ctrl_v"),
clipboard_handling: ClipboardHandlingSchema.optional().default("dont_modify"),
});
export const BindingResponseSchema = z.object({

View file

@ -98,6 +98,8 @@ const settingUpdaters: {
invoke("change_word_correction_threshold_setting", { threshold: value }),
paste_method: (value) =>
invoke("change_paste_method_setting", { method: value }),
clipboard_handling: (value) =>
invoke("change_clipboard_handling_setting", { handling: value }),
history_limit: (value) => invoke("update_history_limit", { limit: value }),
};