diff --git a/src/apps/desktop/Cargo.toml b/src/apps/desktop/Cargo.toml index 682b90d1b4..8193285b94 100644 --- a/src/apps/desktop/Cargo.toml +++ b/src/apps/desktop/Cargo.toml @@ -69,6 +69,7 @@ async-trait = { workspace = true } sha1 = { workspace = true } image = { version = "0.24", default-features = false, features = ["png", "jpeg"] } resvg = { workspace = true } +arboard = "3" [target.'cfg(target_os = "macos")'.dependencies] bitflags = { workspace = true } diff --git a/src/apps/desktop/src/api/system_api.rs b/src/apps/desktop/src/api/system_api.rs index 43e15fcb86..f613536fe9 100644 --- a/src/apps/desktop/src/api/system_api.rs +++ b/src/apps/desktop/src/api/system_api.rs @@ -399,6 +399,34 @@ pub async fn set_macos_edit_menu_mode( Ok(()) } +#[tauri::command] +pub async fn get_clipboard() -> Result { + use arboard::Clipboard; + match Clipboard::new() { + Ok(mut clipboard) => { + match clipboard.get_text() { + Ok(text) => Ok(text), + Err(e) => Err(format!("Failed to get clipboard text: {}", e)) + } + }, + Err(e) => Err(format!("Failed to create clipboard text: {}", e)) + } +} + +#[tauri::command] +pub async fn set_clipboard(text: String) -> Result<(), String> { + use arboard::Clipboard; + match Clipboard::new() { + Ok(mut clipboard) => { + match clipboard.set_text(text) { + Ok(_) => Ok(()), + Err(e) => Err(format!("Failed to set clipboard text: {}", e)) + } + }, + Err(e) => Err(format!("Failed to create clipboard text: {}", e)) + } +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SendNotificationRequest { diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index 955dd52db0..db1df7d7a8 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -915,6 +915,8 @@ pub async fn _run() { get_watched_paths, get_clipboard_files, paste_files, + get_clipboard, + set_clipboard, get_config, get_configs, computer_use_get_status, diff --git a/src/apps/ohos/entry/src/main/module.json5 b/src/apps/ohos/entry/src/main/module.json5 index fbeec3341f..fbdf19a351 100644 --- a/src/apps/ohos/entry/src/main/module.json5 +++ b/src/apps/ohos/entry/src/main/module.json5 @@ -101,6 +101,16 @@ { "name": "ohos.permission.ALLOW_EXTERNAL_NATIVE_CODE", "reason": "$string:permission_external_native_code_reason" + }, + { + "name": "ohos.permission.READ_PASTEBOARD", + "reason": "$string:permission_read_pasteboard_reason", + "usedScene": { + "abilities": [ + "EntryAbility" + ], + "when": "always" + } } ], "extensionAbilities": [ diff --git a/src/apps/ohos/entry/src/main/resources/base/element/string.json b/src/apps/ohos/entry/src/main/resources/base/element/string.json index 5ecc2f4005..3dc48114b2 100644 --- a/src/apps/ohos/entry/src/main/resources/base/element/string.json +++ b/src/apps/ohos/entry/src/main/resources/base/element/string.json @@ -46,6 +46,10 @@ { "name": "permission_external_native_code_reason", "value": "Used to allow loading and executing external native code libraries" + }, + { + "name": "permission_read_pasteboard_reason", + "value": "Used to read clipboard contents for terminal copy and paste" } ] } \ No newline at end of file diff --git a/src/apps/ohos/entry/src/main/resources/zh_CN/element/string.json b/src/apps/ohos/entry/src/main/resources/zh_CN/element/string.json index 0727ba935d..e8b83812da 100644 --- a/src/apps/ohos/entry/src/main/resources/zh_CN/element/string.json +++ b/src/apps/ohos/entry/src/main/resources/zh_CN/element/string.json @@ -47,6 +47,10 @@ { "name": "permission_external_native_code_reason", "value": "用于允许加载和执行外部原生代码库" + }, + { + "name": "permission_read_pasteboard_reason", + "value": "用于读取剪贴板内容以进行终端复制和粘贴" } ] } \ No newline at end of file diff --git a/src/web-ui/src/shared/utils/textSelection.ts b/src/web-ui/src/shared/utils/textSelection.ts index ded097dce3..0197aa4912 100644 --- a/src/web-ui/src/shared/utils/textSelection.ts +++ b/src/web-ui/src/shared/utils/textSelection.ts @@ -105,7 +105,26 @@ export const copyTextToClipboard = async (text: string): Promise => { } }; - +export const readTextFromClipboard = async (): Promise => { + try { + if (typeof window !== 'undefined' && (window as any).__TAURI__) { + try { + const { systemAPI } = await import('@/infrastructure/api/service-api/SystemAPI'); + return await systemAPI.getClipboard(); + } catch (tauriErr) { + log.warn('Tauri clipboard failed, falling back to navigator.clipboard', tauriErr); + } + } + + if (navigator.clipboard && navigator.clipboard.readText) { + return await navigator.clipboard.readText(); + } + } catch (error) { + log.warn('Failed to read clipboard', error); + } + return '' +} + export const getElementText = (element: HTMLElement): string => { if (element.tagName === 'PRE' || element.tagName === 'CODE') { diff --git a/src/web-ui/src/tools/terminal/components/Terminal.tsx b/src/web-ui/src/tools/terminal/components/Terminal.tsx index 4e20407685..bb5b3f9b9f 100644 --- a/src/web-ui/src/tools/terminal/components/Terminal.tsx +++ b/src/web-ui/src/tools/terminal/components/Terminal.tsx @@ -22,6 +22,7 @@ import { createLogger } from '@/shared/utils/logger'; import { sendDebugProbe } from '@/shared/utils/debugProbe'; import { nowMs } from '@/shared/utils/timing'; import '@xterm/xterm/css/xterm.css'; +import { readTextFromClipboard } from '@/shared/utils/textSelection'; import './Terminal.scss'; const log = createLogger('Terminal'); @@ -691,7 +692,7 @@ const Terminal = forwardRef(({ } } - const text = await navigator.clipboard.readText(); + const text = await readTextFromClipboard(); if (!text) return; await pasteText(text); diff --git a/src/web-ui/src/tools/terminal/services/TerminalActionManager.ts b/src/web-ui/src/tools/terminal/services/TerminalActionManager.ts index afa5508f17..09d07f0fdb 100644 --- a/src/web-ui/src/tools/terminal/services/TerminalActionManager.ts +++ b/src/web-ui/src/tools/terminal/services/TerminalActionManager.ts @@ -8,6 +8,7 @@ import { Terminal as XTerm } from '@xterm/xterm'; import { globalEventBus } from '@/infrastructure/event-bus'; import { createLogger } from '@/shared/utils/logger'; import { resolveTerminalPaste } from '../utils'; +import { readTextFromClipboard } from '@/shared/utils/textSelection'; const log = createLogger('TerminalActionManager'); @@ -114,7 +115,9 @@ class TerminalActionManager { return; } - const terminalId = this.resolveTerminalIdForEvent(event); + const targetTerminalId = this.getTerminalIdFromElement(event.target instanceof Element ? event.target : null); + const activeTerminalId = this.getTerminalIdFromElement(typeof document !== 'undefined' && document.activeElement instanceof Element ? document.activeElement: null); + const terminalId = targetTerminalId || activeTerminalId; if (!terminalId) { return; } @@ -124,19 +127,24 @@ class TerminalActionManager { return; } - if (shortcutAction === 'paste' && handler.isReadOnly) { - return; - } - - this.stopKeyboardEvent(event); - if (shortcutAction === 'copy') { + const selectedText = this.getSelectedTextFromTerminal(handler); + if (!selectedText) { + return; + } + this.stopKeyboardEvent(event); void this.handleCopy({ - terminalId, - selectedText: this.getSelectedTextFromTerminal(handler), + terminalId, + selectedText, }); return; } + + if (shortcutAction === 'paste' && handler.isReadOnly) { + return; + } + + this.stopKeyboardEvent(event); if (shortcutAction === 'paste') { void this.handlePasteShortcut({ terminalId, handler }); @@ -151,7 +159,7 @@ class TerminalActionManager { return null; } - if (!event.ctrlKey || !event.shiftKey || event.altKey || event.metaKey) { + if (!event.ctrlKey || event.altKey || event.metaKey) { return null; } @@ -177,51 +185,6 @@ class TerminalActionManager { event.stopImmediatePropagation?.(); } - private resolveTerminalIdForEvent(event: KeyboardEvent): string | null { - const candidates: Array = [ - event.target instanceof Element ? event.target : null, - typeof document !== 'undefined' && document.activeElement instanceof Element ? document.activeElement : null, - this.getSelectionElement(), - ]; - - for (const candidate of candidates) { - const terminalId = this.getTerminalIdFromElement(candidate); - if (terminalId) { - return terminalId; - } - } - - let matchedTerminalId: string | null = null; - for (const [terminalId, handler] of this.handlers.entries()) { - const selectedText = this.getSelectedTextFromTerminal(handler); - if (!selectedText) { - continue; - } - - if (matchedTerminalId) { - return null; - } - - matchedTerminalId = terminalId; - } - - return matchedTerminalId; - } - - private getSelectionElement(): Element | null { - if (typeof window === 'undefined') { - return null; - } - - const selection = window.getSelection(); - const anchorNode = selection?.anchorNode; - if (!anchorNode) { - return null; - } - - return anchorNode instanceof Element ? anchorNode : anchorNode.parentElement; - } - private getTerminalIdFromElement(element: Element | null): string | null { if (!(element instanceof Element)) { return null; @@ -280,7 +243,7 @@ class TerminalActionManager { } try { - const text = await navigator.clipboard.readText(); + const text = await readTextFromClipboard(); if (!text) { return; }