This article has participated in the call for good writing activities, click to view: back end, big front end double track submission, 20,000 yuan prize pool waiting for you to challenge!

preface

While introducing this article, let me give you some background. The author is based on the company’s infrastructure Doraemon (Doraemon) some functional background to write this article, do not know, interested students can go to kangaroo cloud github to know about the 100 treasure box Doraemon. In Doraemon, you can configure the agent. Under configuration details in configuration center, you can find the corresponding Nginx configuration file and other files, and you can edit them here. However, Execute Shell in this function module is just an input box, which creates A kind of, This input box is an illusion of a Web Terminal. Therefore, in order to solve this problem, we plan to make a simple version of Web Terminal to solve this problem. It is under this background that the author started the research on Web Terminal and wrote this article.

The title of this article is how to build A simple Web Terminal. It will mainly focus on this theme and tell the story based on Doraemon to gradually derive the points involved and some points the author thinks about. Of course, there may be many ways to achieve Web Terminal, and the author is also in the research process. Meanwhile, the time for writing this article is also ina hurry, and many points are involved. If there are any mistakes in this paper, students are welcome to point out, and the author will correct them in time.

Xterm.js

First, we need a component to help us quickly build the basic framework of Web Terminal, it is — xterm.js. So what is xterm.js, the official explanation is as follows

Xterm.js is a TypeScript front-end component that enables applications to deliver fully functional terminals to users in the browser. It is used by popular projects such as VS Code, Hyper and Theia.

Since this article mainly focuses on building a Web Terminal, the detailed API related to Xterm.js will not be introduced, but the basic API will be briefly introduced. Now we only need to know that xterm.js is a component and we need to use it. Those who are interested can click on the official document to read it.

Basic API

  • Terminal

Constructor to generate a Terminal instance

import { Terminal } from 'xterm';

const term = new Terminal();
Copy the code
  • OnKey, onData

A function on the Terminal instance that listens for input events

  • write

A method for writing text to a Terminal instance

  • loadAddon

A method to load a plug-in on a Terminal instance

  • Attach and FIT plug-ins

The FIT plug-in can be adapted to resize a Terminal to fit its parent element

The Attach plugin provides a way to attach a terminal to a WebSocket stream

import { Terminal } from 'xterm';
import { AttachAddon } from 'xterm-addon-attach';

const term = new Terminal();
const socket = new WebSocket('wss://docker.example.com/containers/mycontainerid/attach/ws');
const attachAddon = new AttachAddon(socket);

// Attach the socket to term
term.loadAddon(attachAddon);
Copy the code

The basic use

As a component, we need to first understand its basic use and how to quickly build the basic framework of Web Terminal. The following uses Doraemon’s code as an example

1. The first step is to install Xterm

npm install xterm / yarn add xterm

2. Use xterm to generate a Terminal instance object and mount it to a DOM element

// webTerminal.tsx
import React, { useEffect, useState } from 'react'
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'
import Loading from '@/components/loading'

import './style.scss';
import 'xterm/css/xterm.css'

const WebTerminal: React.FC = () = > {
    const [terminal, setTerminal] = useState(null)

    const initTerminal = () = > {
        const prefix = 'admin $ '
        const fitAddon = new FitAddon()
        const terminal: any = new Terminal({ cursorBlink: true })

        terminal.open(document.getElementById('terminal-container'))
        // The size of terminal matches the parent element
        terminal.loadAddon(fitAddon)
        fitAddon.fit()

        terminal.writeln('\x1b[1;1;32mwellcom to web terminal!\x1b[0m')
        terminal.write(prefix)
        setTerminal(terminal)
    }

    useEffect(() = > { initTerminal() }, [])

    return (
        <Loading>
            <div id="terminal-container" className='c-webTerminal__container'></div>
        </Loading>)}export default WebTerminal
Copy the code
// style.scss
.c-webTerminal__container {
    width: 600px;
    height: 350px;
}
Copy the code

As shown in the figure below, we can get a Shelf for Web Terminal. In the code above, we need to introduce the xterm-addon-fit module, which matches the size of the generated Terminal object with the size of its parent element.

The above is the most basic use of Xterm. At this time, we have the generated instance of the terminal, but if we want to realize a Web terminal, this is far from enough. Next, we need to gradually add building blocks to it.

Input operation

When we try to input, as some of you may have noticed, this shelf does not allow input fields, and we also need to add the Terminal instance object to handle input operations. The idea of processing the input operation of Terminal is also very simple. That is, we need to add listening events to the instance of Terminal just generated. When the input operation with keyboard is captured, different numbers are processed according to the input value.

Due to the shortage of time, we will roughly write some common operations for processing, such as the most basic letter or number input, delete operation, cursor up and down around operation processing.

Basic input

The first is the basic input operation, which looks like this

// webTerminal.tsx.const WebTerminal: React.FC = () = > {
    const [terminal, setTerminal] = useState(null)
    const prefix = 'admin $ '
    
    let inputText = ' ' // Enter characters
    
    const onKeyAction = () = > {
        terminal.onKey(e= > {
            const { key, domEvent } = e
            const { keyCode, altKey, altGraphKey, ctrlKey, metaKey } = domEvent

            constprintAble = ! (altKey || altGraphKey || ctrlKey || metaKey)// Disable related keys
            const totalOffsetLength = inputText.length + prefix.length   // Total offset
            const currentOffsetLength = terminal._core.buffer.x     // The current x offset

            switch(keyCode) {
            ...
            default:
                if(! printAble)break
                if (totalOffsetLength >= terminal.cols)  break
                if (currentOffsetLength >= totalOffsetLength) {
                    terminal.write(key)
                    inputText += key
                    break
                }
                const cursorOffSetLength = getCursorOffsetLength(totalOffsetLength - currentOffsetLength, '\x1b[D')
                terminal.write('\x1b[?K' + `${key}${inputText.slice(currentOffsetLength - prefix.length)}`) // Write the key and the character after the coordinate in the current coordinate
                terminal.write(cursorOffSetLength)  // Move the cursor that stays in the current position
                inputText = inputText.slice(0, currentOffsetLength) + key + inputText.slice(totalOffsetLength - currentOffsetLength)
            }
        })
    }

    useEffect(() = > {
        if(terminal) { onKeyAction() } }, [terminal]) ... . }// const.ts
export const TERMINAL_INPUT_KEY = {
    BACK: 8.// Backspace delete key
    ENTER: 13./ / enter key
    UP: 38.// On the steering wheel
    DOWN: 40.// Steering button
    LEFT: 37.// Left button on the steering wheel
    RIGHT: 39 // Right click on the wheel
}
Copy the code

Where, ‘\x1b[D’ and ‘\x1b[?K’ in the code are special characters of the terminal, which respectively represent the characters that move the cursor to the left and erase the current cursor to the end of the line. Because the author does not know much about special characters, I will not expand the description. If a character is entered in a non-trailing position, the main process is as follows

CurrentOffsetLength, terminal._core.buffer. X, increases from 0 when we go left to right, and decreases to 0 when we go right to left. Used to mark the current cursor position

Assuming that the input character has two characters and the cursor is in the third digit, the following steps occur:

1. Move the cursor to the second position and press the keyboard to enter the character S

2. Delete the character whose cursor position is at the end of the character

3. Concatenate the input character with the cursor position of the original character text to the end of the line

4. Move the cursor to the original input position

Delete operation
// webTerminal.tsx.const getCursorOffsetLength = (offsetLength: number, subString: string = ' ') = > {
    let cursorOffsetLength = ' '
    for (let offset = 0; offset < offsetLength; offset++) {
        cursorOffsetLength += subString
    }
    return cursorOffsetLength
}

...
case TERMINAL_INPUT_KEY.BACK:
    if (currentOffsetLength > prefix.length) {
        const cursorOffSetLength = getCursorOffsetLength(totalOffsetLength - currentOffsetLength, '\x1b[D') // Keep the cursor position

        terminal._core.buffer.x = currentOffsetLength - 1
        terminal.write('\x1b[?K' + inputText.slice(currentOffsetLength-prefix.length))
        terminal.write(cursorOffSetLength)
        inputText = `${inputText.slice(0, currentOffsetLength - prefix.length - 1)}${inputText.slice(currentOffsetLength - prefix.length)}`
    }
    break.Copy the code

To delete the cursor position character, enter the cursor position at the end of the text. To delete the cursor position character text at the non-end of the text, the main process is as follows

Suppose there are three characters ABC, where the cursor is in the second position, when it deletes the operation, the process is as follows:

1. Move the cursor to the second position and press the keyboard to delete the character

2, clear the current cursor position to the end of the character

3. Concatenate the remaining characters according to the offset

3. Move the cursor to the original input position

Enter the operation
// webTerminal.tsx.let inputText = ' '
let currentIndex = 0
let inputTextList = []


const handleInputText = () = > {
    terminal.write('\r\n')
    if(! inputText.trim()) { terminal.prompt()return
    }

    if (inputTextList.indexOf(inputText) === -1) {
        inputTextList.push(inputText)
        currentIndex = inputTextList.length
    }

    terminal.prompt()
}

...
case TERMINAL_INPUT_KEY.ENTER:
    handleInputText()
    inputText = ' '
    break.Copy the code

After pressing the Enter key, you need to store the entered character text into the array to record the current text position for future use

Up/down operation
// webTerminal.tsx.case TERMINAL_INPUT_KEY.UP: {
    if(! inputTextList[currentIndex -1]) break

    const offsetLength = getCursorOffsetLength(inputText.length, '\x1b[D')

    inputText = inputTextList[currentIndex - 1]
    terminal.write(offsetLength + '\x1b[?K' )
    terminal.write(inputTextList[currentIndex - 1])
    terminal._core.buffer.x = totalOffsetLength
    currentIndex--

    break}...Copy the code

The main steps are as follows

The up or down button, in contrast, takes out previously stored characters, deletes them all, and then writes them.

Operation left/right
// webTerminal.tsx.case TERMINAL_INPUT_KEY.LEFT:
    if (currentOffsetLength > prefix.length) {
        terminal.write(key) // '\x1b[D'
    }
    break

case TERMINAL_INPUT_KEY.RIGHT:
    if (currentOffsetLength < totalOffsetLength) {
        terminal.write(key) // '\x1b[C'
    }
    break.Copy the code

Points to be improved

1. Access websocket to realize the communication between the server and the client

2, SSH access, currently only add terminal input operation, our final goal is to enable it to log in to the server

The desired result would be something like this

IO is based on the egg framework, which can be used to establish socket communication. The author lists the general steps here, but as A supplement to this article, This will be improved in the next article.

conclusion

First of all, this terminal is not finished up here, because of time, it’s not finished yet. Some points to be improved are also listed above, and the author will add the second or third part of this paper to make further improvements. The author also tried to access socket this week, but there were still some problems, not perfect, so finally decided to focus on the description of some input operation processing. Finally, if you have any questions about this article, please feel free to comment.

More and more

  • Official document: xtermjs.org/
  • Socket.IO Documentation: eggjs.org/zh-cn/tutor…
  • Terminal special characters: blog.csdn.net/sunjiajiang…