Add "external script" paste method (#638)
* feat(linux): add "external script" paste method * feat(i18n): external script translation for fr, de, it
This commit is contained in:
parent
f8ee7fce7c
commit
b90077b228
11 changed files with 114 additions and 22 deletions
|
|
@ -4,14 +4,13 @@ use crate::settings::TypingTool;
|
|||
use crate::settings::{get_settings, AutoSubmitKey, ClipboardHandling, PasteMethod};
|
||||
use enigo::{Direction, Enigo, Key, Keyboard};
|
||||
use log::info;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use crate::utils::{is_kde_wayland, is_wayland};
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::process::Command;
|
||||
|
||||
/// Pastes text using the clipboard: saves current content, writes text, sends paste keystroke, restores clipboard.
|
||||
fn paste_via_clipboard(
|
||||
|
|
@ -500,6 +499,31 @@ fn send_key_combo_via_xdotool(paste_method: &PasteMethod) -> Result<(), String>
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Pastes text by invoking an external script.
|
||||
/// The script receives the text to paste as a single argument.
|
||||
fn paste_via_external_script(text: &str, script_path: &str) -> Result<(), String> {
|
||||
info!("Pasting via external script: {}", script_path);
|
||||
|
||||
let output = Command::new(script_path)
|
||||
.arg(text)
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute external script '{}': {}", script_path, e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
return Err(format!(
|
||||
"External script '{}' failed with exit code {:?}. stderr: {}, stdout: {}",
|
||||
script_path,
|
||||
output.status.code(),
|
||||
stderr.trim(),
|
||||
stdout.trim()
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Types text directly by simulating individual key presses.
|
||||
fn paste_direct(
|
||||
enigo: &mut Enigo,
|
||||
|
|
@ -612,6 +636,14 @@ pub fn paste(text: String, app_handle: AppHandle) -> Result<(), String> {
|
|||
paste_delay_ms,
|
||||
)?
|
||||
}
|
||||
PasteMethod::ExternalScript => {
|
||||
let script_path = settings
|
||||
.external_script_path
|
||||
.as_ref()
|
||||
.filter(|p| !p.is_empty())
|
||||
.ok_or("External script path is not configured")?;
|
||||
paste_via_external_script(&text, script_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
if should_send_auto_submit(settings.auto_submit, paste_method) {
|
||||
|
|
|
|||
|
|
@ -272,6 +272,7 @@ pub fn run(cli_args: CliArgs) {
|
|||
shortcut::change_paste_method_setting,
|
||||
shortcut::get_available_typing_tools,
|
||||
shortcut::change_typing_tool_setting,
|
||||
shortcut::change_external_script_path_setting,
|
||||
shortcut::change_clipboard_handling_setting,
|
||||
shortcut::change_auto_submit_setting,
|
||||
shortcut::change_auto_submit_key_setting,
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ pub enum PasteMethod {
|
|||
None,
|
||||
ShiftInsert,
|
||||
CtrlShiftV,
|
||||
ExternalScript,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Type)]
|
||||
|
|
@ -358,6 +359,7 @@ pub struct AppSettings {
|
|||
pub paste_delay_ms: u64,
|
||||
#[serde(default = "default_typing_tool")]
|
||||
pub typing_tool: TypingTool,
|
||||
pub external_script_path: Option<String>,
|
||||
}
|
||||
|
||||
fn default_model() -> String {
|
||||
|
|
@ -721,6 +723,7 @@ pub fn get_default_settings() -> AppSettings {
|
|||
show_tray_icon: default_show_tray_icon(),
|
||||
paste_delay_ms: default_paste_delay_ms(),
|
||||
typing_tool: default_typing_tool(),
|
||||
external_script_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -668,6 +668,7 @@ pub fn change_paste_method_setting(app: AppHandle, method: String) -> Result<(),
|
|||
"none" => PasteMethod::None,
|
||||
"shift_insert" => PasteMethod::ShiftInsert,
|
||||
"ctrl_shift_v" => PasteMethod::CtrlShiftV,
|
||||
"external_script" => PasteMethod::ExternalScript,
|
||||
other => {
|
||||
warn!("Invalid paste method '{}', defaulting to ctrl_v", other);
|
||||
PasteMethod::CtrlV
|
||||
|
|
@ -712,6 +713,18 @@ pub fn change_typing_tool_setting(app: AppHandle, tool: String) -> Result<(), St
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn change_external_script_path_setting(
|
||||
app: AppHandle,
|
||||
path: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let mut settings = settings::get_settings(&app);
|
||||
settings.external_script_path = path;
|
||||
settings::write_settings(&app, settings);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn change_clipboard_handling_setting(app: AppHandle, handling: String) -> Result<(), String> {
|
||||
|
|
|
|||
|
|
@ -128,6 +128,14 @@ async changeTypingToolSetting(tool: string) : Promise<Result<null, string>> {
|
|||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async changeExternalScriptPathSetting(path: string | null) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("change_external_script_path_setting", { path }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async changeClipboardHandlingSetting(handling: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("change_clipboard_handling_setting", { handling }) };
|
||||
|
|
@ -706,7 +714,7 @@ async updateRecordingRetentionPeriod(period: string) : Promise<Result<null, stri
|
|||
},
|
||||
/**
|
||||
* Checks if the Mac is a laptop by detecting battery presence
|
||||
*
|
||||
*
|
||||
* This uses pmset to check for battery information.
|
||||
* Returns true if a battery is detected (laptop), false otherwise (desktop)
|
||||
*/
|
||||
|
|
@ -730,7 +738,7 @@ async isLaptop() : Promise<Result<boolean, string>> {
|
|||
|
||||
/** user-defined types **/
|
||||
|
||||
export type AppSettings = { bindings: Partial<{ [key in string]: ShortcutBinding }>; push_to_talk: boolean; audio_feedback: boolean; audio_feedback_volume?: number; sound_theme?: SoundTheme; start_hidden?: boolean; autostart_enabled?: boolean; update_checks_enabled?: boolean; selected_model?: string; always_on_microphone?: boolean; selected_microphone?: string | null; clamshell_microphone?: string | null; selected_output_device?: string | null; translate_to_english?: boolean; selected_language?: string; overlay_position?: OverlayPosition; debug_mode?: boolean; log_level?: LogLevel; custom_words?: string[]; model_unload_timeout?: ModelUnloadTimeout; word_correction_threshold?: number; history_limit?: number; recording_retention_period?: RecordingRetentionPeriod; paste_method?: PasteMethod; clipboard_handling?: ClipboardHandling; auto_submit?: boolean; auto_submit_key?: AutoSubmitKey; post_process_enabled?: boolean; post_process_provider_id?: string; post_process_providers?: PostProcessProvider[]; post_process_api_keys?: Partial<{ [key in string]: string }>; post_process_models?: Partial<{ [key in string]: string }>; post_process_prompts?: LLMPrompt[]; post_process_selected_prompt_id?: string | null; mute_while_recording?: boolean; append_trailing_space?: boolean; app_language?: string; experimental_enabled?: boolean; keyboard_implementation?: KeyboardImplementation; show_tray_icon?: boolean; paste_delay_ms?: number; typing_tool?: TypingTool }
|
||||
export type AppSettings = { bindings: Partial<{ [key in string]: ShortcutBinding }>; push_to_talk: boolean; audio_feedback: boolean; audio_feedback_volume?: number; sound_theme?: SoundTheme; start_hidden?: boolean; autostart_enabled?: boolean; update_checks_enabled?: boolean; selected_model?: string; always_on_microphone?: boolean; selected_microphone?: string | null; clamshell_microphone?: string | null; selected_output_device?: string | null; translate_to_english?: boolean; selected_language?: string; overlay_position?: OverlayPosition; debug_mode?: boolean; log_level?: LogLevel; custom_words?: string[]; model_unload_timeout?: ModelUnloadTimeout; word_correction_threshold?: number; history_limit?: number; recording_retention_period?: RecordingRetentionPeriod; paste_method?: PasteMethod; clipboard_handling?: ClipboardHandling; auto_submit?: boolean; auto_submit_key?: AutoSubmitKey; post_process_enabled?: boolean; post_process_provider_id?: string; post_process_providers?: PostProcessProvider[]; post_process_api_keys?: Partial<{ [key in string]: string }>; post_process_models?: Partial<{ [key in string]: string }>; post_process_prompts?: LLMPrompt[]; post_process_selected_prompt_id?: string | null; mute_while_recording?: boolean; append_trailing_space?: boolean; app_language?: string; experimental_enabled?: boolean; keyboard_implementation?: KeyboardImplementation; show_tray_icon?: boolean; paste_delay_ms?: number; typing_tool?: TypingTool; external_script_path: string | null }
|
||||
export type AudioDevice = { index: string; name: string; is_default: boolean }
|
||||
export type AutoSubmitKey = "enter" | "ctrl_enter" | "cmd_enter"
|
||||
export type BindingResponse = { success: boolean; binding: ShortcutBinding | null; error: string | null }
|
||||
|
|
@ -741,7 +749,7 @@ export type HistoryEntry = { id: number; file_name: string; timestamp: number; s
|
|||
/**
|
||||
* Result of changing keyboard implementation
|
||||
*/
|
||||
export type ImplementationChangeResult = { success: boolean;
|
||||
export type ImplementationChangeResult = { success: boolean;
|
||||
/**
|
||||
* List of binding IDs that were reset to defaults due to incompatibility
|
||||
*/
|
||||
|
|
@ -753,7 +761,7 @@ export type ModelInfo = { id: string; name: string; description: string; filenam
|
|||
export type ModelLoadStatus = { is_loaded: boolean; current_model: string | null }
|
||||
export type ModelUnloadTimeout = "never" | "immediately" | "min_2" | "min_5" | "min_10" | "min_15" | "hour_1" | "sec_5"
|
||||
export type OverlayPosition = "none" | "top" | "bottom"
|
||||
export type PasteMethod = "ctrl_v" | "direct" | "none" | "shift_insert" | "ctrl_shift_v"
|
||||
export type PasteMethod = "ctrl_v" | "direct" | "none" | "shift_insert" | "ctrl_shift_v" | "external_script"
|
||||
export type PostProcessProvider = { id: string; label: string; base_url: string; allow_base_url_edit?: boolean; models_endpoint?: string | null; supports_structured_output?: boolean }
|
||||
export type RecordingRetentionPeriod = "never" | "preserve_limit" | "days_3" | "weeks_2" | "months_3"
|
||||
export type ShortcutBinding = { id: string; name: string; description: string; default_binding: string; current_binding: string }
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React from "react";
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { Dropdown } from "../ui/Dropdown";
|
||||
import { SettingContainer } from "../ui/SettingContainer";
|
||||
import { Input } from "../ui/Input";
|
||||
import { useSettings } from "../../hooks/useSettings";
|
||||
import { useOsType } from "../../hooks/useOsType";
|
||||
import type { PasteMethod } from "@/bindings";
|
||||
|
|
@ -55,11 +56,20 @@ export const PasteMethodSetting: React.FC<PasteMethodProps> = React.memo(
|
|||
);
|
||||
}
|
||||
|
||||
// External script is only available on Linux
|
||||
if (osType === "linux") {
|
||||
options.push({
|
||||
value: "external_script",
|
||||
label: t("settings.advanced.pasteMethod.options.externalScript"),
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
const selectedMethod = (getSetting("paste_method") ||
|
||||
"ctrl_v") as PasteMethod;
|
||||
const externalScriptPath = getSetting("external_script_path") || "";
|
||||
|
||||
const pasteMethodOptions = getPasteMethodOptions(osType);
|
||||
|
||||
|
|
@ -71,14 +81,29 @@ export const PasteMethodSetting: React.FC<PasteMethodProps> = React.memo(
|
|||
grouped={grouped}
|
||||
tooltipPosition="bottom"
|
||||
>
|
||||
<Dropdown
|
||||
options={pasteMethodOptions}
|
||||
selectedValue={selectedMethod}
|
||||
onSelect={(value) =>
|
||||
updateSetting("paste_method", value as PasteMethod)
|
||||
}
|
||||
disabled={isUpdating("paste_method")}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Dropdown
|
||||
options={pasteMethodOptions}
|
||||
selectedValue={selectedMethod}
|
||||
onSelect={(value) =>
|
||||
updateSetting("paste_method", value as PasteMethod)
|
||||
}
|
||||
disabled={isUpdating("paste_method")}
|
||||
/>
|
||||
{selectedMethod === "external_script" && (
|
||||
<Input
|
||||
type="text"
|
||||
value={externalScriptPath}
|
||||
onChange={(e) =>
|
||||
updateSetting("external_script_path", e.target.value)
|
||||
}
|
||||
placeholder={t(
|
||||
"settings.advanced.pasteMethod.externalScriptPlaceholder",
|
||||
)}
|
||||
disabled={isUpdating("external_script_path")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</SettingContainer>
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -261,8 +261,10 @@
|
|||
"clipboardCtrlShiftV": "Zwischenablage (Strg+Umschalt+V)",
|
||||
"clipboardShiftInsert": "Zwischenablage (Umschalt+Einfg)",
|
||||
"direct": "Direkt",
|
||||
"none": "Keine"
|
||||
}
|
||||
"none": "Keine",
|
||||
"externalScript": "Externes Skript"
|
||||
},
|
||||
"externalScriptPlaceholder": "/pfad/zu/ihrem/skript.sh"
|
||||
},
|
||||
"typingTool": {
|
||||
"title": "Eingabetool",
|
||||
|
|
|
|||
|
|
@ -261,8 +261,10 @@
|
|||
"clipboardCtrlShiftV": "Clipboard (Ctrl+Shift+V)",
|
||||
"clipboardShiftInsert": "Clipboard (Shift+Insert)",
|
||||
"direct": "Direct",
|
||||
"none": "None"
|
||||
}
|
||||
"none": "None",
|
||||
"externalScript": "External Script"
|
||||
},
|
||||
"externalScriptPlaceholder": "/path/to/your/script.sh"
|
||||
},
|
||||
"typingTool": {
|
||||
"title": "Typing Tool",
|
||||
|
|
|
|||
|
|
@ -261,8 +261,10 @@
|
|||
"clipboardCtrlShiftV": "Presse-papiers (Ctrl+Shift+V)",
|
||||
"clipboardShiftInsert": "Presse-papiers (Shift+Insert)",
|
||||
"direct": "Direct",
|
||||
"none": "Aucun"
|
||||
}
|
||||
"none": "Aucun",
|
||||
"externalScript": "Script externe"
|
||||
},
|
||||
"externalScriptPlaceholder": "/chemin/vers/votre/script.sh"
|
||||
},
|
||||
"typingTool": {
|
||||
"title": "Outil de frappe",
|
||||
|
|
|
|||
|
|
@ -261,8 +261,10 @@
|
|||
"clipboardCtrlShiftV": "Appunti (Ctrl+Shift+V)",
|
||||
"clipboardShiftInsert": "Appunti (Shift+Insert)",
|
||||
"direct": "Diretto",
|
||||
"none": "Nessuno"
|
||||
}
|
||||
"none": "Nessuno",
|
||||
"externalScript": "Script esterno"
|
||||
},
|
||||
"externalScriptPlaceholder": "/percorso/del/tuo/script.sh"
|
||||
},
|
||||
"typingTool": {
|
||||
"title": "Strumento di digitazione",
|
||||
|
|
|
|||
|
|
@ -113,6 +113,8 @@ const settingUpdaters: {
|
|||
commands.changeWordCorrectionThresholdSetting(value as number),
|
||||
paste_method: (value) => commands.changePasteMethodSetting(value as string),
|
||||
typing_tool: (value) => commands.changeTypingToolSetting(value as string),
|
||||
external_script_path: (value) =>
|
||||
commands.changeExternalScriptPathSetting(value as string | null),
|
||||
clipboard_handling: (value) =>
|
||||
commands.changeClipboardHandlingSetting(value as string),
|
||||
auto_submit: (value) => commands.changeAutoSubmitSetting(value as boolean),
|
||||
|
|
|
|||
Loading…
Reference in a new issue