Preface
When introducing this article, let me talk about some background of this article. The author is based on the company's basic construction Doraemon (Doraemon) some functional background to write this article, students who do not understand or are interested can go to kangaroo cloud github Let’s learn about the treasure Doraemon . Agents can be configured in Doraemon. Under the configuration details of the configuration center, we can find the nginx configuration file or other files corresponding to the host, which can be edited here, but the Execute shell under this functional module is actually just a Input box, which will create a kind for users, this input box is an illusion of 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 against this background that the author started the investigation of Web Terminal and wrote this article.
This article named how to build a simple Web Terminal, mainly around this topic, combined with Doraemon to describe, and gradually derived the points involved, some of the author's thinking. Of course, there may be many ways to implement Web Terminal. The author is also in the process of research. At the same time, the time of writing this article is relatively short, and there are many points involved. If there is something wrong with this article, please feel free to point it out. The author must correct it in time.
Xterm.js
First of all, we need a component to help us quickly build the basic framework of Web Terminal, which is --Xterm.js. So what is Xterm.js, the official explanation is as follows
Xterm.js is a front-end component written in TypeScript, which allows applications to bring users a fully functional terminal in the browser. It is used by popular projects such as VS Code, Hyper, and Theia.
Because this article mainly revolves around building a Web Terminal, so the detailed API related to Xterm.js will not be introduced, just a brief introduction to the basic API, everyone now only needs to know that it is a component, we need to use it It, interested students can click official document to read.
Basic API
- Terminal
Constructor, can generate Terminal instance
import { Terminal } from 'xterm';
const term = new Terminal();
- onKey、onData
Function to monitor input events on Terminal instance
- write
Method of writing text on Terminal instance
- loadAddon
Method of loading plugins on Terminal instance
- attach, fit plugin
The fit plug-in can adapt and adjust the size of the Terminal so that it fits the parent element of the Terminal
The attach plugin provides a method to attach the terminal to the WebSocket stream. The following is an example of the official website
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);
Basic use
As a component, we need to first understand its basic usage and how to quickly build the basic framework of Web Terminal. The following uses the code of Doraemon 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 on the 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'))
// terminal 的尺寸与父元素匹配
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
// style.scss
.c-webTerminal__container {
width: 600px;
height: 350px;
}
As shown in the figure below, we can get a shelf of Web Terminal. In the above code, we need to introduce the xterm-addon-fit module, and use it to match 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 an instance of this terminal generated, but if we want to implement a Web terminal, this is far from enough. Next, we need to gradually contribute to it.
Input operation
When we tried to input, some students should have discovered that this shelf cannot be used to input fields. We also need to add terminal instance objects to handle input operations. The following introduces the processing of input operations. The idea of processing the input operations of this Terminal is also very simple, that is, we need to add monitoring events to the newly generated Terminal instance. When the input operation with the keyboard is captured, according to the input Values are processed corresponding to different numbers.
Due to the rush of time, we will roughly write some of the more common operations for processing, such as the most basic letter or number input, delete operations, and cursor up, down, left, and right operations.
Basic input
The first is the most basic input operation, the code is as follows
// webTerminal.tsx
...
const WebTerminal: React.FC = () => {
const [terminal, setTerminal] = useState(null)
const prefix = 'admin $ '
let inputText = '' // 输入字符
const onKeyAction = () => {
terminal.onKey(e => {
const { key, domEvent } = e
const { keyCode, altKey, altGraphKey, ctrlKey, metaKey } = domEvent
const printAble = !(altKey || altGraphKey || ctrlKey || metaKey) // 禁止相关按键
const totalOffsetLength = inputText.length + prefix.length // 总偏移量
const currentOffsetLength = terminal._core.buffer.x // 当前x偏移量
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)}`) // 在当前的坐标写上 key 和坐标后面的字符
terminal.write(cursorOffSetLength) // 移动停留在当前位置的光标
inputText = inputText.slice(0, currentOffsetLength) + key + inputText.slice(totalOffsetLength - currentOffsetLength)
}
})
}
useEffect(() => {
if (terminal) {
onKeyAction()
}
}, [terminal])
...
...
}
// const.ts
export const TERMINAL_INPUT_KEY = {
BACK: 8, // 退格删除键
ENTER: 13, // 回车键
UP: 38, // 方向盘上键
DOWN: 40, // 方向盘键
LEFT: 37, // 方向盘左键
RIGHT: 39 // 方向盘右键
}
Among them, '\x1b[D' and '\x1b[?K' are the special characters of the terminal, which are the special characters of the terminal to move the cursor to the end of the line and erase the current cursor. Characters, special characters, because I don’t know much about them, so I won’t explain them. Among them, if you enter directly at the end of the text, the characters are spliced and written into the text. If you enter characters at a position other than the end, the main process is as follows
Before explaining, let’s talk about this currentOffsetLength, which is the value of terminal._core.buffer.x. When we move from left to right, it increases from 0. When we move from right to left, it is in On the basis of +1, it is decremented successively, and decremented to 0, which is used to mark the current cursor position
Assuming that there are two characters in the input character, and the cursor is in the third position, the main steps are as follows:
1. Move the cursor to the second position, press the keyboard to enter the character s
2. Delete the character from the cursor position to the end of the character
3. Concatenate the input characters and the characters from the cursor position of the original character text to the end of the line to write
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') // 保留原来光标位置
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
...
Among them, input directly at the end of the text to delete the character at the cursor position. If the operation of deleting a character text is performed at a position other than the end, the main process is as follows
Assuming there are three characters abc, and the cursor is in the second position, when it performs a delete operation, the process is as follows:
1. Move the cursor to the second position, press the keyboard to delete the character
2. Clear the characters from the current cursor position to the end
3. Splice the remaining characters according to the offset
3. Move the cursor to the original input position
Carriage return 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
...
After pressing the Enter key, you need to store the entered character text in the array and record the current text position for subsequent 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
}
...
The main steps are as follows
Compared with others, pressing up or down is to take out the previously stored characters, delete them all first, and then write them.
Operate 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
...
Point to be perfected
1. Connect to websocket to realize communication between server and client
2. Access to ssh, currently only the input operation of the terminal has been added, our ultimate goal is to allow it to log in to the server
The envisioned final effect should be like this
The author also has socket.io access to the current code. Doraemon's words are based on the egg framework. You can use this egg.socket.io to establish socket communication. The author lists the general steps here. , But it is prepared as a supplement to this article and will be improved in the next article.
to sum up
First of all, this terminal has not finished writing here. Due to time, it has not been finished yet. Some points to be improved are also listed above, and the author will add the second or third part of this article later, and continue to supplement and improve. The author also tried to access the socket this week, but there are still some problems and it is not perfect, so I finally decided that this article will focus on the processing of some input operations. Finally, if you have any doubts about this article, you are welcome to speak.
More
- Official document: https://xtermjs.org/
- Socket.IO document: https://eggjs.org/zh-cn/tutorials/socketio.html
- Terminal special characters: https://blog.csdn.net/sunjiajiang/article/details/8513215
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。