From cbb6b87a37d0d63a4279d3a508fec96455ef5e8c Mon Sep 17 00:00:00 2001 From: Dimariqe Date: Sat, 20 Dec 2025 14:44:30 +0700 Subject: [PATCH] Add clipboard copy/paste support to terminal component (#822) Co-authored-by: cmcooper1980 <31871143+cmcooper1980@users.noreply.github.com> --- frontend/src/components/Terminal.vue | 131 +++++++++++++++++++++++++-- 1 file changed, 123 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/Terminal.vue b/frontend/src/components/Terminal.vue index deeae93..c25f6f6 100644 --- a/frontend/src/components/Terminal.vue +++ b/frontend/src/components/Terminal.vue @@ -101,6 +101,14 @@ export default { this.terminal.open(this.$refs.terminal); this.terminal.focus(); + // Add right-click context menu handler for paste + this.$refs.terminal.addEventListener('contextmenu', this.handleContextMenu); + + // Add selection handler for copy to clipboard + this.terminal.onSelectionChange(() => { + this.handleSelection(); + }); + // Notify parent component when data is received this.terminal.onCursorMove(() => { console.debug("onData triggered"); @@ -135,6 +143,7 @@ export default { window.removeEventListener("resize", this.onResizeEvent); // Remove the resize event listener from the window object. this.$root.unbindTerminal(this.name); this.terminal.dispose(); + this.$refs.terminal?.removeEventListener('contextmenu', this.handleContextMenu); }, methods: { @@ -163,6 +172,14 @@ export default { this.terminalInputBuffer = ""; }, + clearCurrentLine() { + // Move cursor to the beginning of the input and clear it + const backspaces = "\b".repeat(this.cursorPosition); + const spaces = " ".repeat(this.terminalInputBuffer.length); + const moreBackspaces = "\b".repeat(this.terminalInputBuffer.length); + this.terminal.write(backspaces + spaces + moreBackspaces); + }, + mainTerminalConfig() { this.terminal.onKey(e => { console.debug("Encode: " + JSON.stringify(e.key)); @@ -183,12 +200,24 @@ export default { }); } else if (e.key === "\u007F") { // Backspace if (this.cursorPosition > 0) { - const trimmedTextBeforeCursor = this.terminalInputBuffer.slice(0, this.cursorPosition - 1); - const textAfterCursor = this.terminalInputBuffer.slice(this.cursorPosition); - const clearAfterCursor = " ".repeat(textAfterCursor.length) + "\b \b".repeat(textAfterCursor.length + 1); - this.terminalInputBuffer = trimmedTextBeforeCursor + textAfterCursor; - this.terminal.write(clearAfterCursor + textAfterCursor + "\b".repeat(textAfterCursor.length)); + // Remove character to the left of cursor + const beforeCursor = this.terminalInputBuffer.slice(0, this.cursorPosition - 1); + const afterCursor = this.terminalInputBuffer.slice(this.cursorPosition); + this.terminalInputBuffer = beforeCursor + afterCursor; this.cursorPosition--; + + // Redraw the line + this.terminal.write("\b" + afterCursor + " \b".repeat(afterCursor.length + 1)); + } + } else if (e.key === "\u001B\u005B\u0033\u007E") { // Delete key + if (this.cursorPosition < this.terminalInputBuffer.length) { + // Remove character to the right of cursor + const beforeCursor = this.terminalInputBuffer.slice(0, this.cursorPosition); + const afterCursor = this.terminalInputBuffer.slice(this.cursorPosition + 1); + this.terminalInputBuffer = beforeCursor + afterCursor; + + // Redraw the line from cursor position + this.terminal.write(afterCursor + " \b".repeat(afterCursor.length + 1)); } } else if (e.key === "\u001B\u005B\u0041" || e.key === "\u001B\u005B\u0042") { // UP OR DOWN // Do nothing @@ -206,6 +235,8 @@ export default { console.debug("Ctrl + C"); this.$root.emitAgent(this.endpoint, "terminalInput", this.name, e.key); this.removeInput(); + } else if (e.key === "\u0016" || (e.domEvent?.ctrlKey && e.key.toLowerCase() === "v")) { // Ctrl + V + this.handlePaste(); } else if (e.key === "\u0009" || e.key.startsWith("\u001B")) { // TAB or other special keys // Do nothing } else { @@ -214,14 +245,18 @@ export default { this.terminalInputBuffer = textBeforeCursor + e.key + textAfterCursor; this.terminal.write(e.key + textAfterCursor + "\b".repeat(textAfterCursor.length)); this.cursorPosition++; - this.terminalInputBuffer += e.key; - this.terminal.write(e.key); } }); }, interactiveTerminalConfig() { this.terminal.onKey(e => { + // Handle Ctrl+V for paste + if (e.key === "\u0016" || (e.ctrlKey && e.key === "v")) { + this.handlePaste(); + return; + } + this.$root.emitAgent(this.endpoint, "terminalInput", this.name, e.key, (res) => { if (!res.ok) { this.$root.toastRes(res); @@ -252,7 +287,87 @@ export default { let rows = this.terminal.rows; let cols = this.terminal.cols; this.$root.emitAgent(this.endpoint, "terminalResize", this.name, rows, cols); - } + }, + + /** + * Handle clipboard paste operation + */ + async handlePaste() { + try { + const text = await navigator.clipboard.readText(); + if (text) { + this.pasteText(text); + } + } catch (error) { + console.error("Failed to read from clipboard:", error); + } + }, + + /** + * Paste text into the terminal based on current mode + */ + pasteText(text) { + if (this.mode === "mainTerminal") { + // For main terminal, insert text at current cursor position + const beforeCursor = this.terminalInputBuffer.slice(0, this.cursorPosition); + const afterCursor = this.terminalInputBuffer.slice(this.cursorPosition); + + // Update the buffer with inserted text + this.terminalInputBuffer = beforeCursor + text + afterCursor; + + // Clear the current line and rewrite it + this.clearCurrentLine(); + this.terminal.write(this.terminalInputBuffer); + + // Move cursor to the correct position (after the pasted text) + this.cursorPosition += text.length; + const backspaces = "\b".repeat(afterCursor.length); + this.terminal.write(backspaces); + + } else if (this.mode === "interactive") { + // For interactive terminal, send directly to server + this.$root.emitAgent(this.endpoint, "terminalInput", this.name, text, (res) => { + if (!res.ok) { + this.$root.toastRes(res); + } + }); + } + }, + + /** + * Handle right-click context menu for paste operation + */ + handleContextMenu(event) { + // Prevent default context menu + event.preventDefault(); + + // Only handle paste for modes that support input + if (this.mode === "mainTerminal" || this.mode === "interactive") { + this.handlePaste(); + } + }, + + /** + * Handle text selection in terminal - copy to clipboard + */ + handleSelection() { + const selectedText = this.terminal.getSelection(); + if (selectedText && selectedText.length > 0) { + this.copyToClipboard(selectedText); + } + }, + + /** + * Copy text to clipboard + */ + async copyToClipboard(text) { + try { + await navigator.clipboard.writeText(text); + console.debug("Text copied to clipboard:", text); + } catch (error) { + console.error("Failed to copy to clipboard:", error); + } + }, } };