参考出处:https://www.cnblogs.com/wzs5800/p/13221344.html (作者:Ashin5800)
模块包源码地址:https://github.com/AshinWu/webterminal
项目需要模拟terminal连接服务器调用命令操作,以下代码是xterm.js的使用,包含键盘keyCode操作的判断,websocket功能未添加,使用也就是调用命令即可
注:keyCode的源码文件做了一点点修改,下载源码之后的路径为:\node_modules\xterm\lib\core\input\Keyboard.js,在302行做了修改,修改代码如下:
if(ev.keyCode == 67){
result.key = EscapeSequences_1.C0.ESC + '[67;';
}else if(ev.keyCode == 86){
result.key = EscapeSequences_1.C0.ESC + '[86;';
}else{
result.key = String.fromCharCode(ev.keyCode - 64);
}
注:兼容中英文输入与其他操作
<template>
<div class="hello" style="width:555px;">
<div id="terminal-container"></div>
</div>
</template>
<script>
import "xterm/dist/xterm.css";
// import 'xterm/dist/xterm'
import * as fit from "xterm/dist/addons/fit/fit";
import * as attach from "xterm/dist/addons/attach/attach";
import { Terminal } from "xterm";
export default {
name: "HelloWorld",
data() {
return {
terminal: Object,
termOptions: {
rows: 16,
scrollback: 800,
},
input: "",
prefix: "ashin$ ", // 每行开头,可自行修改
// 当前历史指令
histIndex: 0,
histCommandList: [], // 记录历史命令
currentOffset: Number,
beginIndex: 0,
rowsNum: [],
inputData: {}, // 存放输入的字符串,单行/多行
beginY: null, // 开始的行数
};
},
mounted() {
this.terminal = this.initTerm();
},
methods: {
initTerm() {
// 初始化
let term = new Terminal({
rendererType: "canvas",
cursorBlink: true,
convertEol: true,
scrollback: this.termOptions.scrollback,
rows: this.termOptions.rows,
cursorStyle: "underline", //光标样式
theme: {
foreground: "yellow",
background: "#060101",
cursor: "help", //设置光标
},
fontSize: 16
});
let terminalContainer = document.querySelector("#terminal-container");
term.open(terminalContainer);
Terminal.applyAddon(fit);
Terminal.applyAddon(attach);
term.fit();
term.focus();
term.writeln(`Hello from web terminal`);
term.prompt = () => {
term.write(this.prefix);
};
// 实际需要使用socket来交互
if ("WebSocket" in window) {
term.writeln("\x1b[1;1;32mThe Browser supports websocket!\x1b[0m");
// term.prompt();
term.write("\n");
// socket监听事件
} else {
term.writeln(
"\x1b[1;1;31mThe Browser does not support websocket!\x1b[0m"
);
}
term.write(this.prefix);
setTimeout(function(){
this.beginY = term._core.buffer.y;
},0);
let specialKeycode1 = ['\x1b[86;',];
let specialKeycode2 = [
'\x1b[15~','\x1b[17~','\x1b[18~','\x1b[19~',
'\x1b[20~','\x1b[21~','\x1b[23~','\x1b[24~'
];
// 若需要中文输入, 使用on data监听
term.onData(
function (e) {
let cols = term.cols;
let y = term._core.buffer.y;
let x = term._core.buffer.x;
let subObj;
switch (e) {
case "\r": // Enter
if(this.input.length > 0){
let temp = [];
for(let key in this.inputData){
temp.push(key);
}
term._core.buffer.y = temp[temp.length-1];
}
this.handleInput();
this.input = "";
for(let key in this.inputData){
delete this.inputData[key];
}
this.beginY = null;
y = term._core.buffer.y;
break;
case "\t": // tab
break;
case "\x1b[A": // up
if(this.histCommandList[this.histIndex - 1]){
this.backspaceRow(term, cols);
this.input = this.histCommandList[this.histIndex - 1];
term.write(this.input);
this.histIndex--;
this.updateInputData(this.input,cols,term._core.buffer.y);
}
break;
case "\x1b[B": // down
if (this.histCommandList.length) {
if (this.histCommandList[this.histIndex + 1]) {
this.backspaceRow(term, cols);
this.input = this.histCommandList[this.histIndex + 1];
term.write(this.input);
this.histIndex++;
this.updateInputData(this.input,cols,term._core.buffer.y);
}else{
if(this.histIndex == this.histCommandList.length-1){
this.backspaceRow(term, cols);
this.input = '';
term.write(this.input);
this.updateInputData(this.input,cols,term._core.buffer.y);
this.histIndex = this.histCommandList.length;
}
}
}
break;
case "\x1b[D": // left
subObj = this.posStr(this.inputData[y], x, 'l');
if((this.inputData[y]!=undefined && this.inputData[y].indexOf(this.prefix) > -1)
|| (subObj.index == null && subObj.x == this.prefix.length)
){
if(x <= this.prefix.length){
return;
}
}
if(this.stringType(subObj.str)){
term.write(e);
term.write(e);
}else{
term.write(e);
}
if(x == 0){
term._core.buffer.y--;
let lastStr = this.inputData[term._core.buffer.y][this.inputData[term._core.buffer.y].length-1];
if(this.stringType(lastStr)){
if( this.strLen(this.inputData[term._core.buffer.y]) != cols){
term._core.buffer.x = cols-2;
}else{
term._core.buffer.x = cols-1;
}
}else{
term._core.buffer.x = cols+1;
}
}
break;
case "\x1b[C": // right
subObj = null;
subObj = this.posStr(this.inputData[y], x+2, 'r');
if(subObj.x != null && this.inputData[y]!=undefined
&& subObj.x <= this.strLen(this.inputData[y])){
if(this.inputData[y+1]!=undefined && subObj.index == this.strLen(this.inputData[y])){
term._core.buffer.x = 0;
term._core.buffer.y++;
}else{
if(this.stringType(subObj.str)){
term.write(e);
term.write(e);
}else{
term.write(e);
}
}
}else{
if(this.inputData[y]!=undefined && subObj.x > this.strLen(this.inputData[y])
&& subObj.x-1 == this.strLen(this.inputData[y])
){
if(this.inputData[y+1]!=undefined && subObj.index == this.strLen(this.inputData[y])){
term._core.buffer.x = 0;
term._core.buffer.y++;
}else{
term.write(e);
}
}
}
break;
case "\u007F": // Backspace (DEL) 区分中英文字符
if(x==0){
y--;
term._core.buffer.y--;
}
if(this.inputData[y] != undefined){
if(this.inputData[y]!=undefined&&this.inputData[y].length == 0){
delete this.inputData[y];
term._core.buffer.y--;
y--;
term._core.buffer.x = cols;
x = term._core.buffer.x;
}else{
if(x == 0){
term._core.buffer.x = cols;
x = term._core.buffer.x;
}
}
let subObj = this.posStr(this.inputData[y], term._core.buffer.x,'l');
let isStr = this.stringType(subObj.str);
if(this.inputData[y].indexOf(this.prefix) > -1){
if(x <= this.prefix.length){
return;
}
}
if (isStr) {
term._core.buffer.x = subObj.x - 2;
} else {
term._core.buffer.x = subObj.x - 1;
}
// 当前行光标位置插入空格
term.write("\x1b[?K" + this.inputData[y].slice(subObj.i + 1));
const cursor = this.bulidData(this.strLen(this.inputData[y].slice(subObj.i + 1)),"\x1b[D");
term.write(cursor);
this.inputData[y] = `${this.inputData[y].slice(0,subObj.i)}${this.inputData[y].slice(subObj.i + 1)}`;
let _this = this;
_this.input = '';
for(var key in _this.inputData){
if(key == term._core.buffer.y){
_this.delFun(key, cols, term);
}
_this.input += _this.inputData[key];
}
_this.input = _this.input.slice(_this.prefix.length);
}
break;
case "\x1b[3~": // 46 Del
break;
case "\x1b[2~": // 45 Ins
break;
case "\x1b[5~": // 33 PgUp
break;
case "\x1b[6~": // 34 PgDn
break;
case "\x1b[H": // 36 Home
break;
case "\x1b[67;": // ctrl+c
term.write("^C");
this.handleInput();
this.input = "";
for(let key in this.inputData){
delete this.inputData[key];
}
this.beginY = null;
break;
case "\x1bOP": // F1
break;
case "\x1bOQ": //F2
break;
case "\x1bOR": // F3
break;
case "\x1bOS": // F4
break;
default:
if(specialKeycode1.indexOf(e) > -1){
e = '^V';
}
if(specialKeycode2.indexOf(e) > -1){
e = '~';
}
this.histIndex = this.histCommandList.length;
if(!this.beginY){
this.beginY = term._core.buffer.y;
}
setTimeout(function(){
let y = term._core.buffer.y;
let x = term._core.buffer.x;
if(this.inputData[y]){
let _this = this;
_this.input = '';
for(var key in _this.inputData){
if(key == term._core.buffer.y){
_this.addFun(key , cols, term, e, x);
break;
}
}
for(var key1 in _this.inputData){
_this.input += _this.inputData[key1];
}
_this.input = _this.input.slice(_this.prefix.length);
}else{
term.write(e);
this.input += e;
this.updateInputData(this.input, cols, this.beginY);
}
}.bind(this),0);
}
}.bind(this)
);
this.term = term;
return term;
},
// 在这里处理自定义输入...
handleInput() {
// 判断空值
this.terminal.write("\r\n");
if (this.input.trim() && this.input.replace("^C", "") != "") {
// 记录历史命令 相同命令也需要记录
this.histCommandList.push(this.input);
this.histIndex = this.histCommandList.length;
this.input = this.input.replace("^C", "");
const command = this.input.trim().split(" ");
// 可限制可用命令
// 这里进行socket交互
switch (command[0]) {
case "help":
this.terminal.writeln(
"\x1b[40;33;1m\nthis is a web terminal demo based on xterm!\x1b[0m\n此demo模拟shell上下左右和退格键效果\n"
);
break;
default:
// 发送数据
break;
}
}
this.terminal.prompt();
},
bulidData(length, subString) {
let cursor = "";
for (let i = 0; i < length; i++) {
cursor += subString;
}
return cursor;
},
// 计算中英文混合字符串长度
strLen(str){
if(!str) return 0;
let strLen = 0;
for(let i=0;i<str.length;i++){
if(/^[\u4e00-\u9fa5]/.test(str[i])){
strLen = strLen + 2;
}else{
strLen ++;
}
}
return strLen;
},
// 查找中英文混合字符串指定位置字符
posStr(str, x, flag) {
if(!str) return { str: "", index: null, i: null, x: x, isLast:isLast };
let index = 0;
let len = str.length;
let isLast = false;
if(this.strLen(str) == this.term.cols-1){
if(x > this.term.cols-1){
x = this.strLen(str);
}
}
for (let i = 0; i < len; i++) {
if (/^[\u4e00-\u9fa5]/.test(str[i])) {
index = index + 2;
} else {
index = index + 1;
}
if(i == len-1){
isLast = true;
}
if(flag == 'r'){
if (/^[\u4e00-\u9fa5]/.test(str[i])) {
if (index == x) {
return { str: str[i], index: index, i: i, x: x, isLast:isLast };
}
}else{
if (index == x-1) {
return { str: str[i], index: index, i: i, x: x, isLast:isLast };
}
}
}else{
if (index == x) {
return { str: str[i], index: index, i: i, x: x, isLast:isLast };
}
}
}
return { str: "", index: null, i: null, x: x, isLast:isLast };
},
// 判断字符串类型
stringType(str) {
if (/^[\u4e00-\u9fa5]/.test(str)) {
return true;
} else {
return false;
}
},
// 删除字符串函数
delFun(y,cols,term){
let _this = this;
y=parseInt(y);
let delstr = this.delStr(y, cols);
if(!isNaN(delstr.charCodeAt(0))){
let len = this.strLen(this.inputData[y]);
this.inputData[y] += delstr;
delstr = delstr.split('');
for(let i=0;i<delstr.length;i++){
setTimeout(function(){
// 当前行删除后在尾部添加下一行字符串
term._core.buffer.lines.get(y).setCellFromCodePoint(
len+(i==0?0:_this.strLen(delstr[i-1])),
delstr[i].charCodeAt(0),
1,0,0
);
term._core.blur();
term._core.focus();
term._core.updateRange(y);
term._core.refresh(0, cols);
},0);
_this.inputData[y+1] = _this.inputData[y+1].substr(delstr.length);
if(_this.inputData[y+1] !=undefined){
term._core.buffer.lines.get(y+1).deleteCells(0, _this.strLen(delstr), delstr);
term._core.updateRange(y+1);
term._core.refresh(0, cols);
_this.delFun(y+1, cols, term);
}
}
}
for(let key in _this.inputData){
if(_this.inputData[key].length == 0){
delete _this.inputData[key];
}
}
},
// 删除输入的字符串
delStr(y, cols){
let _str = '';
if(this.inputData[y+1]){
let len = cols - this.strLen(this.inputData[y]);
let str = this.inputData[y+1];
let strlen = str.length;
let index = 0;
for(let i=0;i<strlen;i++){
if (/^[\u4e00-\u9fa5]/.test(str[i])) {
index = index + 2;
} else {
index = index + 1;
}
if(index <= len){
_str += str[i];
}
}
}
return _str;
},
// 插入字符串函数
addFun(y, cols, term, str, x){
let _this = this;
y=parseInt(y);
let start = _this.posInputDataX(_this.inputData[y], x); // 开始位置
let startStr = _this.inputData[y].slice(0,start); // 开始位置截取的前段字符串长度
let endStr = _this.inputData[y].slice(start); // 开始位置截取的后段字符串长度
let startStrLen = _this.strLen(startStr);
let endStrLen = _this.strLen(endStr);
let inputStrLen = _this.strLen(str);
let preStr = '';
let nextStr = '';
let allStr = '';
let copyX = parseInt(JSON.parse(JSON.stringify(x)));
let beginX = 0;
let flag = false;
if(x != 0){
// 当输入的字符串加当前行,长度小于等于cols
if(startStrLen+inputStrLen+endStrLen <= cols){
for(let i=0;i<str.length;i++){
start = _this.posInputDataX(_this.inputData[y], x);
x = _this.addHandle(y, cols, term, str[i], x, start);
if(x >= cols){ // 当前大于等于cols
y++;
if(!_this.inputData[y]){
_this.inputData[y] = '';
}
}
}
}
// 当输入的字符串加当前行,长度大于cols
else{
for(let j=0;j<endStr.length;j++){
term._core.buffer.lines.get(y).deleteCells(x, _this.strLen(endStr[j]), endStr[j]);
_this.inputData[y] = _this.inputData[y].slice(0,_this.inputData[y].length-1);
}
let counted = cols-startStrLen;
let endS = '';
if(counted>0){
allStr = str + endStr;
for(let k=0;k<allStr.length;k++){
if(_this.strLen(endS)+_this.strLen(allStr[k]) <= counted){
endS += allStr[k];
}else{
break;
}
}
}
preStr = allStr.slice(0,endS.length);
nextStr = allStr.slice(endS.length);
if(preStr.length>0){
for(let l=0;l<preStr.length;l++){
start = _this.posInputDataX(_this.inputData[y], x);
x = _this.addHandle(y, cols, term, preStr[l], x, start);
}
}
_this.updateNextRow(y+1,cols,term, nextStr,0);
}
}else{
// 处理当前行的上一行的末尾是否还能写入当前字符
for(let i=0;i<str.length;i++){
if(_this.inputData[y-1] && _this.strLen(_this.inputData[y-1]) + _this.strLen(str[i]) <= cols ){
// 若可以写入
copyX = _this.strLen(_this.inputData[y-1]);
x = _this.addHandle(y-1, cols, term, str[i], cols-1, _this.strLen(_this.inputData[y-1]));
term._core.buffer.y --;
y--;
}else{
// 若不能写入
start = _this.posInputDataX(_this.inputData[y], x);
x = _this.addHandle(y, cols, term, str[i], x, start);
}
}
}
// 更新x和y
for(let h=0;h<str.length;h++){
if(copyX + _this.strLen(str[h]) <= cols){
copyX += _this.strLen(str[h]);
}else{
flag = true;
beginX += _this.strLen(str[h]);
}
}
if(flag){
term._core.buffer.x = beginX;
term._core.buffer.y ++;
}else{
if(copyX == cols || (copyX == _this.strLen(_this.inputData[y]) && nextStr.length>0)){
term._core.buffer.x = 0;
term._core.buffer.y ++;
}else{
term._core.buffer.x = copyX;
}
}
copyX = null;
},
// 插入字符后,循环更新下一行首尾
updateNextRow(y, cols, term, str, x){
let _this = this;
let counted = 0;
let endStr = '';
y = parseInt(y);
if(!_this.inputData[y]){
_this.inputData[y] = '';
}
let start = _this.strLen(_this.inputData[y]) - _this.strLen(str);
_this.inputData[y] = `${str}${_this.inputData[y]}`;
if(_this.strLen(_this.inputData[y]) > cols){
for(let i =0;i<=_this.inputData[y].length;i++){
if(counted + _this.strLen(_this.inputData[y][i]) <= cols){
counted += _this.strLen(_this.inputData[y][i]);
}else{
endStr += _this.inputData[y][i];
}
}
if(endStr.length>0){
_this.inputData[y] = _this.inputData[y].slice(0,_this.inputData[y].length-endStr.length);
for(let j=0;j<endStr.length;j++){
start+=_this.strLen(endStr[j])*(j);
term._core.buffer.lines.get(y).deleteCells(start, _this.strLen(endStr[j]), endStr[j]);
}
}
}
for(let i=0;i<str.length;i++){
term._core.buffer.lines.get(y).insertCells(x, _this.strLen(str[i]), str[i]); // 当前x位置插入字符宽度
// 写入字符
term._core.buffer.lines.get(y).setCellFromCodePoint(
x, str[i].charCodeAt(0),1,0,0
);
term._core.blur(); // 失焦
term._core.focus(); // 聚焦
term._core.updateRange(y); // 更新当前行
term._core.refresh(0, cols); // 刷新当前行
x = x+_this.strLen(str[i]); // 当前x加上字符宽度
}
if(endStr.length>0){
_this.updateNextRow(y+1, cols, term, endStr, 0);
}
},
addHandle(y, cols, term, str, x, start){
let _this = this;
term._core.buffer.lines.get(y).insertCells(x, _this.strLen(str), str); // 当前x位置插入字符宽度
// 更新当前_this.inputData[y]字符串
_this.inputData[y] = `${_this.inputData[y].slice(0,start)}${str}${_this.inputData[y].slice(start)}`;
// 写入字符
term._core.buffer.lines.get(y).setCellFromCodePoint(
x, str.charCodeAt(0),1,0,0
);
term._core.blur(); // 失焦
term._core.focus(); // 聚焦
term._core.updateRange(y); // 更新当前行
term._core.refresh(0, cols); // 刷新当前行
x = x+_this.strLen(str); // 当前x加上字符宽度
return x;
},
// 更新inputData
updateInputData(str, cols, y){
let that = this;
str = this.prefix + str;
let strLen = this.strLen(str);
let col = Math.ceil(strLen/cols);
let _j = 0;
for(let i=0;i<col;i++){
that.inputData[y+i] = '';
for(let j=_j;j<str.length;j++){
if(that.strLen(that.inputData[y+i]) + that.strLen(str[j]) <= cols){
that.inputData[y+i] += str[j];
}else{
_j = j;
break;
}
}
}
},
// 在某行某列插入字符时返回当前行插入的x坐标
posInputDataX(str, x){
if(x == 0 || !str) return 0;
let total = 0;
for(let i=0;i<str.length;i++){
total ++;
if(/^[\u4e00-\u9fa5]/.test(str[i])){
total ++;
}
if(total == x){
return i+1;
}
}
return null;
},
// 更新input字符串
updateInput(str, s, pos){
str = str.slice(0, pos+1) + s + str.slice(pos+1);
return str.slice(this.prefix.length);
},
backspaceRow(term, cols){
if(this.input.length > 0){
let temp = [];
for(let key in this.inputData){
temp.push(key);
delete this.inputData[key];
}
for(let i=0;i<temp.length;i++){
term.write('\x1b[2K\r');
term.write('\x1b[A');
}
term.write("\r\n");
term.write(this.prefix);
}
}
},
};
</script>
/**
* 绑定socket到插件
* var socket = new WebSocket('wss://docker.example.com/containers/mycontainerid/attach/ws');
term.attach(socket)
// socket socoket实例
// bidirectional 终端是否向套接字发送数据
// bufferred 终端是否缓冲输出获得更好的性能
attach(socket: WebSocket, bidirectional: Boolean, bufferred: Boolean)
// 分离当前终端和scoket
detach(socket)
出处作者:Ashin5800
出处:https://www.cnblogs.com/wzs5800/p/13221344.html
源码地址:https://github.com/AshinWu/webterminal
*/
</script>
以上为xterm前端操作部分keyCode功能代码,如有bug,请留言,谢谢!