add support for changing paste method (#187)
* add support for changing paste method * rename/refactor of methods
This commit is contained in:
parent
03289f84d7
commit
1432bac30f
8 changed files with 167 additions and 23 deletions
|
|
@ -1,3 +1,4 @@
|
|||
use crate::settings::{get_settings, PasteMethod};
|
||||
use enigo::Enigo;
|
||||
use enigo::Key;
|
||||
use enigo::Keyboard;
|
||||
|
|
@ -38,14 +39,29 @@ fn send_paste() -> Result<(), String> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn paste(text: String, app_handle: AppHandle) -> Result<(), String> {
|
||||
/// Pastes text directly using the enigo text method.
|
||||
/// This tries to use system input methods if possible, otherwise simulates keystrokes one by one.
|
||||
fn paste_via_direct_input(text: &str) -> Result<(), String> {
|
||||
let mut enigo = Enigo::new(&Settings::default())
|
||||
.map_err(|e| format!("Failed to initialize Enigo: {}", e))?;
|
||||
|
||||
enigo
|
||||
.text(text)
|
||||
.map_err(|e| format!("Failed to send text directly: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Pastes text using the clipboard method (Ctrl+V/Cmd+V).
|
||||
/// Saves the current clipboard, writes the text, sends paste command, then restores the clipboard.
|
||||
fn paste_via_clipboard(text: &str, app_handle: &AppHandle) -> Result<(), String> {
|
||||
let clipboard = app_handle.clipboard();
|
||||
|
||||
// get the current clipboard content
|
||||
let clipboard_content = clipboard.read_text().unwrap_or_default();
|
||||
|
||||
clipboard
|
||||
.write_text(&text)
|
||||
.write_text(text)
|
||||
.map_err(|e| format!("Failed to write to clipboard: {}", e))?;
|
||||
|
||||
// small delay to ensure the clipboard content has been written to
|
||||
|
|
@ -62,3 +78,15 @@ pub fn paste(text: String, app_handle: AppHandle) -> Result<(), String> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn paste(text: String, app_handle: AppHandle) -> Result<(), String> {
|
||||
let settings = get_settings(&app_handle);
|
||||
let paste_method = settings.paste_method;
|
||||
|
||||
println!("Using paste method: {:?}", paste_method);
|
||||
|
||||
match paste_method {
|
||||
PasteMethod::CtrlV => paste_via_clipboard(&text, &app_handle),
|
||||
PasteMethod::Direct => paste_via_direct_input(&text),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -210,6 +210,7 @@ pub fn run() {
|
|||
shortcut::change_overlay_position_setting,
|
||||
shortcut::change_debug_mode_setting,
|
||||
shortcut::change_word_correction_threshold_setting,
|
||||
shortcut::change_paste_method_setting,
|
||||
shortcut::update_custom_words,
|
||||
shortcut::suspend_binding,
|
||||
shortcut::resume_binding,
|
||||
|
|
|
|||
|
|
@ -33,12 +33,29 @@ pub enum ModelUnloadTimeout {
|
|||
Sec5, // Debug mode only
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PasteMethod {
|
||||
CtrlV,
|
||||
Direct,
|
||||
}
|
||||
|
||||
impl Default for ModelUnloadTimeout {
|
||||
fn default() -> Self {
|
||||
ModelUnloadTimeout::Never
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PasteMethod {
|
||||
fn default() -> Self {
|
||||
// Default to CtrlV for macOS and Windows, Direct for Linux
|
||||
#[cfg(target_os = "linux")]
|
||||
return PasteMethod::Direct;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
return PasteMethod::CtrlV;
|
||||
}
|
||||
}
|
||||
|
||||
impl ModelUnloadTimeout {
|
||||
pub fn to_minutes(self) -> Option<u64> {
|
||||
match self {
|
||||
|
|
@ -93,6 +110,8 @@ pub struct AppSettings {
|
|||
pub model_unload_timeout: ModelUnloadTimeout,
|
||||
#[serde(default = "default_word_correction_threshold")]
|
||||
pub word_correction_threshold: f64,
|
||||
#[serde(default)]
|
||||
pub paste_method: PasteMethod,
|
||||
}
|
||||
|
||||
fn default_model() -> String {
|
||||
|
|
@ -167,6 +186,7 @@ pub fn get_default_settings() -> AppSettings {
|
|||
custom_words: Vec::new(),
|
||||
model_unload_timeout: ModelUnloadTimeout::Never,
|
||||
word_correction_threshold: default_word_correction_threshold(),
|
||||
paste_method: PasteMethod::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use tauri_plugin_global_shortcut::{Shortcut, ShortcutState};
|
|||
|
||||
use crate::actions::ACTION_MAP;
|
||||
use crate::settings::ShortcutBinding;
|
||||
use crate::settings::{self, get_settings, OverlayPosition};
|
||||
use crate::settings::{self, get_settings, OverlayPosition, PasteMethod};
|
||||
use crate::ManagedToggleState;
|
||||
|
||||
pub fn init_shortcuts(app: &App) {
|
||||
|
|
@ -210,6 +210,22 @@ pub fn change_word_correction_threshold_setting(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn change_paste_method_setting(app: AppHandle, method: String) -> Result<(), String> {
|
||||
let mut settings = settings::get_settings(&app);
|
||||
let parsed = match method.as_str() {
|
||||
"ctrl_v" => PasteMethod::CtrlV,
|
||||
"direct" => PasteMethod::Direct,
|
||||
other => {
|
||||
eprintln!("Invalid paste method '{}', defaulting to ctrl_v", other);
|
||||
PasteMethod::CtrlV
|
||||
}
|
||||
};
|
||||
settings.paste_method = 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").
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { TranslateToEnglish } from "./TranslateToEnglish";
|
|||
import { ModelUnloadTimeoutSetting } from "./ModelUnloadTimeout";
|
||||
import { CustomWords } from "./CustomWords";
|
||||
import { AlwaysOnMicrophone } from "./AlwaysOnMicrophone";
|
||||
import { PasteMethodSetting } from "./PasteMethod";
|
||||
import { SettingsGroup } from "../ui/SettingsGroup";
|
||||
import { StartHidden } from "./StartHidden";
|
||||
|
||||
|
|
@ -13,6 +14,7 @@ export const AdvancedSettings: React.FC = () => {
|
|||
<SettingsGroup title="Advanced">
|
||||
<StartHidden descriptionMode="tooltip" grouped={true} />
|
||||
<ShowOverlay descriptionMode="tooltip" grouped={true} />
|
||||
<PasteMethodSetting descriptionMode="tooltip" grouped={true} />
|
||||
<TranslateToEnglish descriptionMode="tooltip" grouped={true} />
|
||||
<ModelUnloadTimeoutSetting descriptionMode="tooltip" grouped={true} />
|
||||
<CustomWords descriptionMode="tooltip" grouped />
|
||||
|
|
|
|||
43
src/components/settings/PasteMethod.tsx
Normal file
43
src/components/settings/PasteMethod.tsx
Normal 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 { PasteMethod } from "../../lib/types";
|
||||
|
||||
interface PasteMethodProps {
|
||||
descriptionMode?: "inline" | "tooltip";
|
||||
grouped?: boolean;
|
||||
}
|
||||
|
||||
const pasteMethodOptions = [
|
||||
{ value: "ctrl_v", label: "Clipboard (Ctrl+V)" },
|
||||
{ value: "direct", label: "Direct" },
|
||||
];
|
||||
|
||||
export const PasteMethodSetting: React.FC<PasteMethodProps> = React.memo(({
|
||||
descriptionMode = "tooltip",
|
||||
grouped = false,
|
||||
}) => {
|
||||
const { getSetting, updateSetting, isUpdating } = useSettings();
|
||||
|
||||
const selectedMethod = (getSetting("paste_method") ||
|
||||
"ctrl_v") as PasteMethod;
|
||||
|
||||
return (
|
||||
<SettingContainer
|
||||
title="Paste Method"
|
||||
description="Clipboard (Ctrl+V) simulates Ctrl/Cmd+V keystrokes to paste from your clipboard. Direct tries to use system input methods if possible, otherwise inputs keystrokes one by one into the text field."
|
||||
descriptionMode={descriptionMode}
|
||||
grouped={grouped}
|
||||
>
|
||||
<Dropdown
|
||||
options={pasteMethodOptions}
|
||||
selectedValue={selectedMethod}
|
||||
onSelect={(value) =>
|
||||
updateSetting("paste_method", value as PasteMethod)
|
||||
}
|
||||
disabled={isUpdating("paste_method")}
|
||||
/>
|
||||
</SettingContainer>
|
||||
);
|
||||
});
|
||||
|
|
@ -34,6 +34,9 @@ export const ModelUnloadTimeoutSchema = z.enum([
|
|||
]);
|
||||
export type ModelUnloadTimeout = z.infer<typeof ModelUnloadTimeoutSchema>;
|
||||
|
||||
export const PasteMethodSchema = z.enum(["ctrl_v", "direct"]);
|
||||
export type PasteMethod = z.infer<typeof PasteMethodSchema>;
|
||||
|
||||
export const SettingsSchema = z.object({
|
||||
bindings: ShortcutBindingsMapSchema,
|
||||
push_to_talk: z.boolean(),
|
||||
|
|
@ -50,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),
|
||||
paste_method: PasteMethodSchema.optional().default("ctrl_v"),
|
||||
});
|
||||
|
||||
export const BindingResponseSchema = z.object({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { create } from 'zustand';
|
||||
import { subscribeWithSelector } from 'zustand/middleware';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { Settings, AudioDevice } from '../lib/types';
|
||||
import { create } from "zustand";
|
||||
import { subscribeWithSelector } from "zustand/middleware";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { Settings, AudioDevice } from "../lib/types";
|
||||
|
||||
interface SettingsStore {
|
||||
settings: Settings | null;
|
||||
|
|
@ -12,7 +12,10 @@ interface SettingsStore {
|
|||
|
||||
// Actions
|
||||
initialize: () => Promise<void>;
|
||||
updateSetting: <K extends keyof Settings>(key: K, value: Settings[K]) => Promise<void>;
|
||||
updateSetting: <K extends keyof Settings>(
|
||||
key: K,
|
||||
value: Settings[K],
|
||||
) => Promise<void>;
|
||||
resetSetting: (key: keyof Settings) => Promise<void>;
|
||||
refreshSettings: () => Promise<void>;
|
||||
refreshAudioDevices: () => Promise<void>;
|
||||
|
|
@ -47,7 +50,7 @@ const DEFAULT_SETTINGS: Partial<Settings> = {
|
|||
const DEFAULT_AUDIO_DEVICE: AudioDevice = {
|
||||
index: "default",
|
||||
name: "Default",
|
||||
is_default: true
|
||||
is_default: true,
|
||||
};
|
||||
|
||||
export const useSettingsStore = create<SettingsStore>()(
|
||||
|
|
@ -63,7 +66,7 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||
setLoading: (isLoading) => set({ isLoading }),
|
||||
setUpdating: (key, updating) =>
|
||||
set((state) => ({
|
||||
isUpdating: { ...state.isUpdating, [key]: updating }
|
||||
isUpdating: { ...state.isUpdating, [key]: updating },
|
||||
})),
|
||||
setAudioDevices: (audioDevices) => set({ audioDevices }),
|
||||
setOutputDevices: (outputDevices) => set({ outputDevices }),
|
||||
|
|
@ -114,10 +117,14 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||
// Load audio devices
|
||||
refreshAudioDevices: async () => {
|
||||
try {
|
||||
const devices: AudioDevice[] = await invoke("get_available_microphones");
|
||||
const devices: AudioDevice[] = await invoke(
|
||||
"get_available_microphones",
|
||||
);
|
||||
const devicesWithDefault = [
|
||||
DEFAULT_AUDIO_DEVICE,
|
||||
...devices.filter((d) => d.name !== "Default" && d.name !== "default"),
|
||||
...devices.filter(
|
||||
(d) => d.name !== "Default" && d.name !== "default",
|
||||
),
|
||||
];
|
||||
set({ audioDevices: devicesWithDefault });
|
||||
} catch (error) {
|
||||
|
|
@ -129,10 +136,14 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||
// Load output devices
|
||||
refreshOutputDevices: async () => {
|
||||
try {
|
||||
const devices: AudioDevice[] = await invoke("get_available_output_devices");
|
||||
const devices: AudioDevice[] = await invoke(
|
||||
"get_available_output_devices",
|
||||
);
|
||||
const devicesWithDefault = [
|
||||
DEFAULT_AUDIO_DEVICE,
|
||||
...devices.filter((d) => d.name !== "Default" && d.name !== "default"),
|
||||
...devices.filter(
|
||||
(d) => d.name !== "Default" && d.name !== "default",
|
||||
),
|
||||
];
|
||||
set({ outputDevices: devicesWithDefault });
|
||||
} catch (error) {
|
||||
|
|
@ -142,7 +153,10 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||
},
|
||||
|
||||
// Update a specific setting
|
||||
updateSetting: async <K extends keyof Settings>(key: K, value: Settings[K]) => {
|
||||
updateSetting: async <K extends keyof Settings>(
|
||||
key: K,
|
||||
value: Settings[K],
|
||||
) => {
|
||||
const { settings, setUpdating, refreshSettings } = get();
|
||||
const updateKey = String(key);
|
||||
const originalValue = settings?.[key];
|
||||
|
|
@ -171,20 +185,30 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||
break;
|
||||
case "selected_microphone":
|
||||
const micDeviceName = value === "Default" ? "default" : value;
|
||||
await invoke("set_selected_microphone", { deviceName: micDeviceName });
|
||||
await invoke("set_selected_microphone", {
|
||||
deviceName: micDeviceName,
|
||||
});
|
||||
break;
|
||||
case "selected_output_device":
|
||||
const outputDeviceName = value === "Default" ? "default" : value;
|
||||
await invoke("set_selected_output_device", { deviceName: outputDeviceName });
|
||||
await invoke("set_selected_output_device", {
|
||||
deviceName: outputDeviceName,
|
||||
});
|
||||
break;
|
||||
case "translate_to_english":
|
||||
await invoke("change_translate_to_english_setting", { enabled: value });
|
||||
await invoke("change_translate_to_english_setting", {
|
||||
enabled: value,
|
||||
});
|
||||
break;
|
||||
case "selected_language":
|
||||
await invoke("change_selected_language_setting", { language: value });
|
||||
await invoke("change_selected_language_setting", {
|
||||
language: value,
|
||||
});
|
||||
break;
|
||||
case "overlay_position":
|
||||
await invoke("change_overlay_position_setting", { position: value });
|
||||
await invoke("change_overlay_position_setting", {
|
||||
position: value,
|
||||
});
|
||||
break;
|
||||
case "debug_mode":
|
||||
await invoke("change_debug_mode_setting", { enabled: value });
|
||||
|
|
@ -193,7 +217,12 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||
await invoke("update_custom_words", { words: value });
|
||||
break;
|
||||
case "word_correction_threshold":
|
||||
await invoke("change_word_correction_threshold_setting", { threshold: value });
|
||||
await invoke("change_word_correction_threshold_setting", {
|
||||
threshold: value,
|
||||
});
|
||||
break;
|
||||
case "paste_method":
|
||||
await invoke("change_paste_method_setting", { method: value });
|
||||
break;
|
||||
case "bindings":
|
||||
case "selected_model":
|
||||
|
|
@ -293,12 +322,13 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||
|
||||
// Initialize everything
|
||||
initialize: async () => {
|
||||
const { refreshSettings, refreshAudioDevices, refreshOutputDevices } = get();
|
||||
const { refreshSettings, refreshAudioDevices, refreshOutputDevices } =
|
||||
get();
|
||||
await Promise.all([
|
||||
refreshSettings(),
|
||||
refreshAudioDevices(),
|
||||
refreshOutputDevices(),
|
||||
]);
|
||||
},
|
||||
}))
|
||||
})),
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue