Add clipboard copy/paste support to terminal component (#822)

Co-authored-by: cmcooper1980 <31871143+cmcooper1980@users.noreply.github.com>
This commit is contained in:
Dimariqe
2025-12-20 14:44:30 +07:00
committed by GitHub
parent 98cba39004
commit cbb6b87a37

View File

@@ -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);
}
},
}
};
</script>