From ea631bd02343e4e23857665184075ad3a864bcef Mon Sep 17 00:00:00 2001 From: LukeGus Date: Sun, 23 Mar 2025 02:37:24 -0500 Subject: [PATCH] Optimized pasting --- src/apps/ssh/Terminal.jsx | 271 +++++++++++++++++++++++++++++--------- 1 file changed, 211 insertions(+), 60 deletions(-) diff --git a/src/apps/ssh/Terminal.jsx b/src/apps/ssh/Terminal.jsx index 7a254b2f..41f0c2d9 100644 --- a/src/apps/ssh/Terminal.jsx +++ b/src/apps/ssh/Terminal.jsx @@ -146,78 +146,108 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde event.preventDefault(); - // Check if clipboard API is available - if (navigator.clipboard && navigator.clipboard.readText) { - navigator.clipboard.readText().then((text) => { - text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); - - if (socketRef.current && socketRef.current.connected) { - const processedText = text.replace(/\n/g, "\r"); - socketRef.current.emit("data", processedText); - - setTimeout(() => { - isPasting = false; - }, 50); - } else { - isPasting = false; + // Use a multi-layered approach for clipboard access + const pasteFromClipboard = async () => { + try { + // Try modern Clipboard API first + if (navigator.clipboard && navigator.clipboard.readText) { + try { + const text = await navigator.clipboard.readText(); + if (text && socketRef.current?.connected) { + const processedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r"); + socketRef.current.emit("data", processedText); + return true; + } + } catch (clipboardErr) { + console.warn("Clipboard API failed:", clipboardErr); + // Continue to fallbacks + } } - }).catch((err) => { - console.error("Failed to read clipboard contents:", err); - // Try to handle paste manually using execCommand for fallback - tryFallbackPaste(); - }); - } else { - // Fallback for browsers where clipboard API is not yet available - tryFallbackPaste(); - } + // Try execCommand fallback + if (document.queryCommandSupported && document.queryCommandSupported('paste')) { + const textarea = document.createElement('textarea'); + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.focus(); + + try { + const successful = document.execCommand('paste'); + if (successful) { + const text = textarea.value; + if (text && socketRef.current?.connected) { + const processedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r"); + socketRef.current.emit("data", processedText); + document.body.removeChild(textarea); + return true; + } + } + } catch (execErr) { + console.warn("execCommand paste failed:", execErr); + } + document.body.removeChild(textarea); + } + + // Show permissions warning and instructions + terminalInstance.current.write("\r\n*** To paste: Right-click in terminal and select Paste from context menu ***\r\n"); + return false; + } finally { + setTimeout(() => { + isPasting = false; + }, 100); + } + }; + + pasteFromClipboard(); return false; } return true; }); - // Add fallback paste method using execCommand or input element - const tryFallbackPaste = () => { - try { - // Create temporary textarea for paste operation - const textarea = document.createElement('textarea'); - textarea.style.position = 'absolute'; - textarea.style.left = '-9999px'; - textarea.style.top = '0px'; - document.body.appendChild(textarea); - textarea.focus(); - - // Try execCommand paste (works in some browsers) - const successful = document.execCommand('paste'); - if (successful) { - const text = textarea.value; - if (text && socketRef.current && socketRef.current.connected) { - const processedText = text.replace(/\r\n/g, "\r").replace(/\n/g, "\r"); - socketRef.current.emit("data", processedText); - } - } else { - console.log("Fallback paste failed, clipboard permissions may be needed"); - terminalInstance.current.write("\r\n*** Paste failed: Please try again or grant clipboard permissions ***\r\n"); - } - - // Clean up - document.body.removeChild(textarea); - setTimeout(() => { - isPasting = false; - }, 50); - } catch (err) { - console.error("Fallback paste failed:", err); - terminalInstance.current.write("\r\n*** Paste failed: Try clicking in the terminal first ***\r\n"); - isPasting = false; - } - }; - terminalInstance.current.onKey(({ domEvent }) => { if (domEvent.key === "c" && (domEvent.ctrlKey || domEvent.metaKey)) { const selection = terminalInstance.current.getSelection(); if (selection) { - navigator.clipboard.writeText(selection); + // Use a try-catch to handle clipboard failures + try { + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(selection) + .catch(err => { + console.warn("Clipboard write failed:", err); + terminalInstance.current.write("\r\n*** Copy failed: Text copied to internal buffer ***\r\n"); + // Store selection in a variable as fallback + window.termixInternalClipboard = selection; + }); + } else { + // Fallback for browsers without clipboard API + const textarea = document.createElement('textarea'); + textarea.value = selection; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + + try { + const successful = document.execCommand('copy'); + if (!successful) { + terminalInstance.current.write("\r\n*** Copy failed: Text copied to internal buffer ***\r\n"); + window.termixInternalClipboard = selection; + } + } catch (err) { + console.warn("execCommand copy failed:", err); + terminalInstance.current.write("\r\n*** Copy failed: Text copied to internal buffer ***\r\n"); + window.termixInternalClipboard = selection; + } + + document.body.removeChild(textarea); + } + } catch (err) { + console.error("Copy failed:", err); + terminalInstance.current.write("\r\n*** Copy failed: Text copied to internal buffer ***\r\n"); + window.termixInternalClipboard = selection; + } } } }); @@ -254,6 +284,127 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde socketRef.current.on("pong", () => {}); + // Add right-click context menu for paste + const element = terminalInstance.current.element; + if (element) { + element.addEventListener('contextmenu', (event) => { + event.preventDefault(); + + // Create and show context menu + const contextMenu = document.createElement('div'); + contextMenu.className = 'terminal-context-menu'; + contextMenu.style.position = 'fixed'; + contextMenu.style.left = `${event.clientX}px`; + contextMenu.style.top = `${event.clientY}px`; + contextMenu.style.backgroundColor = '#1e1e1e'; + contextMenu.style.border = '1px solid #555'; + contextMenu.style.borderRadius = '4px'; + contextMenu.style.padding = '4px 0'; + contextMenu.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)'; + contextMenu.style.zIndex = '1000'; + + // Create copy option + const copyOption = document.createElement('div'); + copyOption.innerText = 'Copy'; + copyOption.className = 'terminal-context-menu-item'; + copyOption.style.padding = '6px 12px'; + copyOption.style.cursor = 'pointer'; + copyOption.style.color = 'white'; + copyOption.style.fontSize = '14px'; + copyOption.onmouseover = () => { + copyOption.style.backgroundColor = '#3a3a3a'; + }; + copyOption.onmouseout = () => { + copyOption.style.backgroundColor = 'transparent'; + }; + + // Handle copy action + copyOption.onclick = () => { + const selection = terminalInstance.current.getSelection(); + if (selection) { + // Try to copy using clipboard API + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(selection) + .catch(err => { + console.warn("Clipboard write failed:", err); + window.termixInternalClipboard = selection; + terminalInstance.current.write("\r\n*** Copied to internal clipboard ***\r\n"); + }); + } else { + // Store in internal clipboard + window.termixInternalClipboard = selection; + terminalInstance.current.write("\r\n*** Copied to internal clipboard ***\r\n"); + } + } + document.body.removeChild(contextMenu); + }; + + // Create paste option + const pasteOption = document.createElement('div'); + pasteOption.innerText = 'Paste'; + pasteOption.className = 'terminal-context-menu-item'; + pasteOption.style.padding = '6px 12px'; + pasteOption.style.cursor = 'pointer'; + pasteOption.style.color = 'white'; + pasteOption.style.fontSize = '14px'; + pasteOption.onmouseover = () => { + pasteOption.style.backgroundColor = '#3a3a3a'; + }; + pasteOption.onmouseout = () => { + pasteOption.style.backgroundColor = 'transparent'; + }; + + // Handle paste action + pasteOption.onclick = async () => { + try { + // Try clipboard API first + if (navigator.clipboard && navigator.clipboard.readText) { + try { + const text = await navigator.clipboard.readText(); + if (text && socketRef.current?.connected) { + const processedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r"); + socketRef.current.emit("data", processedText); + } + } catch (err) { + // Use fallback or internal clipboard + if (window.termixInternalClipboard) { + const processedText = window.termixInternalClipboard.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r"); + socketRef.current.emit("data", processedText); + } else { + terminalInstance.current.write("\r\n*** Paste failed: No clipboard content available ***\r\n"); + } + } + } else if (window.termixInternalClipboard) { + // Use internal clipboard if available + const processedText = window.termixInternalClipboard.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r"); + socketRef.current.emit("data", processedText); + } else { + terminalInstance.current.write("\r\n*** Paste failed: No clipboard content available ***\r\n"); + } + } finally { + document.body.removeChild(contextMenu); + } + }; + + // Add options to menu + contextMenu.appendChild(copyOption); + contextMenu.appendChild(pasteOption); + document.body.appendChild(contextMenu); + + // Remove menu when clicking elsewhere + const removeMenu = (e) => { + if (!contextMenu.contains(e.target)) { + document.body.removeChild(contextMenu); + document.removeEventListener('click', removeMenu); + } + }; + + setTimeout(() => { + document.addEventListener('click', removeMenu); + }, 0); + }); + } + return () => { clearInterval(pingInterval); if (terminalInstance.current) {