2023-11-11 22:18:37 +08:00
< template >
< div class = "shadow-box" >
< div v -pre ref = "terminal" class = "main-terminal" > < / div >
< / div >
< / template >
< script >
2023-12-16 20:57:21 +11:00
import { Terminal } from "@xterm/xterm" ;
import { FitAddon } from "@xterm/addon-fit" ;
2023-12-26 04:12:44 +08:00
import { TERMINAL _COLS , TERMINAL _ROWS } from "../../../common/util-common" ;
2023-11-11 22:18:37 +08:00
export default {
/ * *
* @ type { Terminal }
* /
terminal : null ,
components : {
} ,
props : {
name : {
type : String ,
require : true ,
} ,
2023-12-26 04:12:44 +08:00
endpoint : {
type : String ,
require : true ,
} ,
2023-11-11 22:18:37 +08:00
// Require if mode is interactive
stackName : {
type : String ,
} ,
// Require if mode is interactive
serviceName : {
type : String ,
} ,
// Require if mode is interactive
shell : {
type : String ,
default : "bash" ,
} ,
rows : {
type : Number ,
default : TERMINAL _ROWS ,
} ,
cols : {
type : Number ,
default : TERMINAL _COLS ,
} ,
// Mode
// displayOnly: Only display terminal output
// mainTerminal: Allow input limited commands and output
// interactive: Free input and output
mode : {
type : String ,
default : "displayOnly" ,
}
} ,
emits : [ "has-data" ] ,
data ( ) {
return {
first : true ,
terminalInputBuffer : "" ,
cursorPosition : 0 ,
} ;
} ,
created ( ) {
} ,
mounted ( ) {
let cursorBlink = true ;
if ( this . mode === "displayOnly" ) {
cursorBlink = false ;
}
this . terminal = new Terminal ( {
2023-11-12 11:22:31 +03:30
fontSize : 14 ,
fontFamily : "'JetBrains Mono', monospace" ,
2023-11-11 22:18:37 +08:00
cursorBlink ,
cols : this . cols ,
rows : this . rows ,
} ) ;
if ( this . mode === "mainTerminal" ) {
this . mainTerminalConfig ( ) ;
} else if ( this . mode === "interactive" ) {
this . interactiveTerminalConfig ( ) ;
}
//this.terminal.loadAddon(new WebLinksAddon());
// Bind to a div
this . terminal . open ( this . $refs . terminal ) ;
this . terminal . focus ( ) ;
// Notify parent component when data is received
this . terminal . onCursorMove ( ( ) => {
console . debug ( "onData triggered" ) ;
if ( this . first ) {
this . $emit ( "has-data" ) ;
this . first = false ;
}
} ) ;
this . bind ( ) ;
// Create a new Terminal
if ( this . mode === "mainTerminal" ) {
2023-12-26 04:12:44 +08:00
this . $root . emitAgent ( this . endpoint , "mainTerminal" , this . name , ( res ) => {
2023-11-11 22:18:37 +08:00
if ( ! res . ok ) {
this . $root . toastRes ( res ) ;
}
} ) ;
} else if ( this . mode === "interactive" ) {
console . debug ( "Create Interactive terminal:" , this . name ) ;
2023-12-26 04:12:44 +08:00
this . $root . emitAgent ( this . endpoint , "interactiveTerminal" , this . stackName , this . serviceName , this . shell , ( res ) => {
2023-11-11 22:18:37 +08:00
if ( ! res . ok ) {
this . $root . toastRes ( res ) ;
}
} ) ;
}
2023-12-16 20:57:21 +11:00
// Fit the terminal width to the div container size after terminal is created.
this . updateTerminalSize ( ) ;
2023-11-11 22:18:37 +08:00
} ,
unmounted ( ) {
2023-12-16 20:57:21 +11:00
window . removeEventListener ( "resize" , this . onResizeEvent ) ; // Remove the resize event listener from the window object.
2023-11-11 22:18:37 +08:00
this . $root . unbindTerminal ( this . name ) ;
this . terminal . dispose ( ) ;
} ,
methods : {
2023-12-26 04:12:44 +08:00
bind ( endpoint , name ) {
2023-11-11 22:18:37 +08:00
// Workaround: normally this.name should be set, but it is not sometimes, so we use the parameter, but eventually this.name and name must be the same name
if ( name ) {
this . $root . unbindTerminal ( name ) ;
2023-12-26 04:12:44 +08:00
this . $root . bindTerminal ( endpoint , name , this . terminal ) ;
2023-11-11 22:18:37 +08:00
console . debug ( "Terminal bound via parameter: " + name ) ;
} else if ( this . name ) {
this . $root . unbindTerminal ( this . name ) ;
2023-12-26 04:12:44 +08:00
this . $root . bindTerminal ( this . endpoint , this . name , this . terminal ) ;
2023-11-11 22:18:37 +08:00
console . debug ( "Terminal bound: " + this . name ) ;
} else {
console . debug ( "Terminal name not set" ) ;
}
} ,
removeInput ( ) {
const backspaceCount = this . terminalInputBuffer . length ;
const backspaces = "\b \b" . repeat ( backspaceCount ) ;
this . cursorPosition = 0 ;
this . terminal . write ( backspaces ) ;
this . terminalInputBuffer = "" ;
} ,
mainTerminalConfig ( ) {
this . terminal . onKey ( e => {
const code = e . key . charCodeAt ( 0 ) ;
console . debug ( "Encode: " + JSON . stringify ( e . key ) ) ;
if ( e . key === "\r" ) {
// Return if no input
if ( this . terminalInputBuffer . length === 0 ) {
return ;
}
const buffer = this . terminalInputBuffer ;
// Remove the input from the terminal
this . removeInput ( ) ;
2023-12-26 04:12:44 +08:00
this . $root . emitAgent ( this . endpoint , "terminalInput" , this . name , buffer + e . key , ( err ) => {
2023-11-11 22:18:37 +08:00
this . $root . toastError ( err . msg ) ;
} ) ;
} else if ( code === 127 ) { // Backspace
if ( this . cursorPosition > 0 ) {
this . terminal . write ( "\b \b" ) ;
this . cursorPosition -- ;
this . terminalInputBuffer = this . terminalInputBuffer . slice ( 0 , - 1 ) ;
}
} else if ( e . key === "\u001B\u005B\u0041" || e . key === "\u001B\u005B\u0042" ) { // UP OR DOWN
// Do nothing
} else if ( e . key === "\u001B\u005B\u0043" ) { // RIGHT
// TODO
} else if ( e . key === "\u001B\u005B\u0044" ) { // LEFT
// TODO
} else if ( e . key === "\u0003" ) { // Ctrl + C
console . debug ( "Ctrl + C" ) ;
2023-12-26 04:12:44 +08:00
this . $root . emitAgent ( this . endpoint , "terminalInput" , this . name , e . key ) ;
2023-11-11 22:18:37 +08:00
this . removeInput ( ) ;
} else {
this . cursorPosition ++ ;
this . terminalInputBuffer += e . key ;
console . log ( this . terminalInputBuffer ) ;
this . terminal . write ( e . key ) ;
}
} ) ;
} ,
interactiveTerminalConfig ( ) {
this . terminal . onKey ( e => {
2023-12-26 04:12:44 +08:00
this . $root . emitAgent ( this . endpoint , "terminalInput" , this . name , e . key , ( res ) => {
2023-11-11 22:18:37 +08:00
if ( ! res . ok ) {
this . $root . toastRes ( res ) ;
}
} ) ;
} ) ;
2023-12-16 20:57:21 +11:00
} ,
/ * *
* Update the terminal size to fit the container size .
*
* If the terminalFitAddOn is not created , creates it , loads it and then fits the terminal to the appropriate size .
* It then addes an event listener to the window object to listen for resize events and calls the fit method of the terminalFitAddOn .
* /
updateTerminalSize ( ) {
if ( ! Object . hasOwn ( this , "terminalFitAddOn" ) ) {
this . terminalFitAddOn = new FitAddon ( ) ;
this . terminal . loadAddon ( this . terminalFitAddOn ) ;
window . addEventListener ( "resize" , this . onResizeEvent ) ;
}
this . terminalFitAddOn . fit ( ) ;
} ,
/ * *
* Handles the resize event of the terminal component .
* /
onResizeEvent ( ) {
this . terminalFitAddOn . fit ( ) ;
let rows = this . terminal . rows ;
let cols = this . terminal . cols ;
2023-12-26 04:12:44 +08:00
this . $root . emitAgent ( this . endpoint , "terminalResize" , this . name , rows , cols ) ;
2023-11-11 22:18:37 +08:00
}
}
} ;
< / script >
< style scoped lang = "scss" >
. main - terminal {
height : 100 % ;
overflow - x : scroll ;
}
< / style >
< style lang = "scss" >
. terminal {
2024-10-13 21:11:20 +08:00
background - color : black ! important ;
2023-11-11 22:18:37 +08:00
height : 100 % ;
}
< / style >