WebGL Tutorial
and more

xterm.js

撰写时间:2024-11-06

修订时间:2024-11-24

xterm.js可以将终端搬至Web上。它得到了许多大厂商的广泛应用。

基本用法

<html> <head> <link rel="stylesheet" href="https://unpkg.com/xterm/css/xterm.css" /> <script type="module" src="https://unpkg.com/xterm/lib/xterm.js"></script> </head> <body> <div id="terminal"></div> <script type="module"> let term = new Terminal(); term.open(document.getElementById('terminal')); term.writeln("This is \x1B[1;3;31mxterm.js\x1B[0m console."); term.write('And it supports \x1b[1;35mcolor\x1B[0m strings.'); </script> </body> </html>

CDN unpkg.com加载xterm.cssxterm.js两个文件。在开发阶段,建议将这两个文件都下载至本地服务器中,以节省网络传输时间。这两个文件均很小。

其效果如下:

我们所加载的xterm.js并不是一个JavaScript module文件,但并不妨碍我们在模块环境中使用。

先创建Terminal的一个实例,并赋值于变量term。然后在一个宿主标签的下面中添加xterm的DOM标签。使用termwritelnwrite方法在终端中输出相应的字符串。使用相应的转义字符来生成不同颜色、样式的字体。这些颜色值在设置主题样式部分的Interface: ITheme中定义。

刷黑选中终端中的内容,就会看到一大块灰色区域,这即是显示字符的区域,由term的只读属性rowscols控制。可以发现,其cols属性不能很好地匹配父标签。也即,如果一行内容太长时,将导致字符出现在父标签的范围之外,从而破坏了整个页面的和谐布局。而且,rows属性值太大了。下面谈到如何解决这些问题。

xterm.js使用了一些辅助标签来计算与定位字符,因此,使用我们的CSS样式来自定义其样式,效果不明显,甚至会破坏其内部运行机制。相反,我们应通过传递term的构造参数及设置其相应属性值来实现目标。

构造参数

初始化Terminal时,可使用一个对象字面符来传递多个构造参数。

let options = { fontSize: 15, lineHeight: 1.5, rows: 5, cols: 50, theme: { foreground: "#DDD", background: '#111' } }; let term = new Terminal(options); term.writeln("These are some demo texts.");

效果:

更多参数详见:Interface: ITerminalOptions。其中,rowscols属性属于只读属性,只能在构造函数中设置。但可通过resize方法来设置。

DOM层级结构

当在一个idterminaldiv下面挂载Terminal时,其DOM层级结构如下图:

  • div id="terminal"
    • div class="terminal xterm xterm-dom-renderer-owner-<n>"
      • div class="xterm-viewport"
        • div class="xterm-scroll-area"
      • div class="xterm-screen"
        • div class="xterm-helpers"
        • div class="xterm-rows"
        • div class="xterm-selection"
        • div class="xterm-decoration-container"

代码term.element返回上面div class="terminal xterm xterm-dom-renderer-owner-<n>"的节点。

对于各个divclientWidth属性值,div#terminal, div.xtermdiv.xterm-viewport是一样的,都来自于最顶层的div#terminalclientWidth属性值。而div.xterm-screenclientWidth属性值会根据termcols值来设置。

手工设置行数与列数

可调用resize(cols, rows)来设置列数与行数。

设有以下代码:

term.writeln("Uint8Array (3)"); term.writeln(" 0 1"); term.writeln(" 1 2"); term.writeln(" 2 3"); term.resize(60, 5); term.selectAll();

我们希望将终端的宽度值设置为其父标签的宽度,具体细节下面谈到,这里暂定为60。至于行数,上面使用了4writeln语句,最后的代码也会产生新行,因此实际上共有5行。因此,行数可直接设置为5

自动设置列数

xterm.js的cols实际上是计算英文字符的数量。其文本格式使用等宽的字体,默认情况下,1列对应于1个英文字符。但在我们进行了其他字体设置后,这种等量关系可能不再存在。

共有2种方法可自动设置其宽度值。

自动设置cols的第1种方法

第一步,先设置一个初始值,如100,然后查看全选的灰色区域是否与终端父标签的宽度对齐。修改初始值,直到两者基本齐平为止。记下终端此时的cols属性值。假设此值现为76。而父标签的宽度可以使用代码:

let containerWidth = term.element.parentElement.clientWidth;

来自动获取。这样,一行内可显示多少个字符就确定下来了。

第二步,编写自动获取宽度的代码:

let getAutoCols = function(_defaultChars = 76) { const INITIAL_CONTAINER_WIDTH = term.element.parentElement.clientWidth; return function() { return Math.trunc(term.element.parentElement.clientWidth * _defaultChars / INITIAL_CONTAINER_WIDTH); } }();

这是一个自调用的函数。因为默认的字符数与初始的父标签的宽度有关,且只需设置一次,因此将其设为函数的默认参数。因此,在经多次而调整获取实际字符数后,可直接修改此函数参数值即可。

常量INITIAL_CONTAINER_WIDTH记下了父窗口的初始值。它与参数_defaultChars一样,都是在开始时设置或获取后,就不再改变了。之后,再根据每次实际调用该函数时父容器的实际宽度来求出终端的实际宽度。

第三步,编写自动设置大小的代码:

let autoSize = function(_defaultRows = 5) { function _inner(rows) { let autoCols = getAutoCols(); term.resize(autoCols, rows); } let _rows = _defaultRows; window.addEventListener('resize', () => { _inner(_rows); }, false); return function(rows = _defaultRows) { _rows = rows; _inner(rows); } }(); autoSize();

因需要同时响应resize事件及手工调用,因此上述代码将这两种需求都打包进一个函数autoSize函数中。

效果如下:

动态调整浏览器大小,灰色区域应能自动随之而改变,不多也不少。

自动设置cols的第2种方法

在我们实例化一个term后,可通过以下方法来获取当前设置下一个字符的宽度:

let charWidth = term._core._charSizeService.width;

然后可使用父容器的clientWidth属性值来除以其值,即可求出应设置的cols属性值。

term.writeln("Auto setting 'cols' value using _charSizeService object."); let charWidth = term._core._charSizeService.width; let parentDiv = term.element.parentNode; let preferedCols = Math.trunc(parentDiv.clientWidth / charWidth); term.resize(preferedCols, 5); term.selectAll();

这种方式无需调试,且在不同访问者的浏览器上均能获取统一的效果,更加简便。

Safari浏览器当打开其终端时,若对同一网页刷新多次,则每次所获取的clientWidth属性值可能不一样;如果关闭终端,则每次所获取的clientWidth属性值都一样。这应是Safari的bug. Chrome浏览器就没有这种情况。

自动设置行数

let src = '\nLine1\nLine2\nLine3\n'; let splittedLines = src.split('\n'); let lines = splittedLines.map(element => { element = element.trim(); return element ?? ''; }); console.log(lines); lines = lines.filter(element => { if (element) { return element; } }); console.log(lines); lines.forEach((line) => { term.writeln(line); }); autoSize(); function autoSize() { let charWidth = term._core._charSizeService.width; let parentDiv = term.element.parentNode; let preferedCols = Math.trunc(parentDiv.clientWidth / charWidth); term.resize(preferedCols, lines.length + 1); }

首先,xterm.js的APIs数量不多,尤其是缺少对输入源的读取。第二,各种事件相互牵连,且响应事件的顺序有时很出乎意料之外。我从缓冲区及DOM等多个方面进行了尝试,但效果不佳。其经多次调试,上面的方法应是最便捷了。

上面代码中,src变量的内容看着较怪。其对应于下面的形式:

let src = ` Line1 Line2 Line3 `;

本页面使用了自动读取文本并自动运行相应的脚本的技术,若以上面的形式来编写则导致src.split('\n')的结果只有一行,与本例的演示目标不符。因此先便捷地改为原代码所采用的方式。而使用第2种方式的可独立运行的例子在此,无任何问题。

调用map方法后,结果为:

["", "Line1", "Line2", "Line3", ""];

map可改变返回数组的元素,但不能改变数组的大小。

再调用filter方法。filter可改变返回数组的大小,但不能改变数组的元素。此时结果为:

["Line1", "Line2", "Line3"];

然后,将其分别投喂给termwritelnresize方法即可。

input方法

input不会直接输出文本,相反,它能让我们在输出文本之前,通过响应onData事件,有机会对文本进行进一步的加工。

term.onData(data => { console.log(data); term.write(data); }); term.input("First line\nSecond Line\nThird Line");

上面的使用了\n换行符,但为何每一行都有一个缩进?

\n产生新行,上面的代码达到了此目的。但上面代码没有回车。在ASCII编码发布之时,其主要为当时的机械打字机服务。当换行后,我们需要将打印针头回复至新行的第一个字符的位置,这叫回车ASCII转义符为\r。现在,许多环境只要有\n就可视为回车、换行,但xterm.js由于需要精细对待各种转义字符,因此它严格解析并执行各个ASCII转义符,不作扩大解释。

因此,若要回车的效果,上面相应代码应改为:

term.onData(data => { term.write(data); }); term.input("First line\r\nSecond Line\r\nThird Line");

onData还将产生另外的效果:我们可以在终端中移动光标了!

这有什么好奇怪的?textarea不是支持光标的移动吗?首先,xterm.js确实是最先使用textarea来存储文本内容,但它接下来将此textarea隐藏起来了,而是换用了新的div.xterm-rows > span来显示每一行。那它是如何使用光标的移动的?答案是:

term.onData(data => { term.write(data); });

当我们移动光标时,上面的参数data的类型是一个字符串。当按下上光标键时,data的值由3个ASCII值构成:[27, 91, 65]ASCII码表值详见:键盘事件)。而write方法知道如何正确应对这些字符系列

第一个字符是ASCII码表中序号为27ESC,是不可打印的控制字符,浏览器的终端无法正常打印,若要强行打印,则输出:

十进制27,其十六进制为0x1B。在JavaScript中,若要使用字符串的形式来表现ASCII值为0x1B的字符,有两种方式。第一种为:

let asciiVal = 0x1B; let str = String.fromCodePoint(asciiVal); // or, using String.fromCharCode(asciiVal)

第二种表示方式为:

let str = "\x1B";

在字符串中使用\x加后面的十六进制数值来表示。很明显,第2种很直接了当。但需注意,此时\x之间不能带有0

因此对于参数data的值,第一个字符是\x1B。第二个字符是[,第三个字符是A。将这3个字符连起来,则参数data的值就成了\x1B[A

同样,下光标键对应的参数值为\x1B[B左光标键对应的参数值为\x1B[D右光标键对应的参数值为\x1B[C

为何有这种字符系列组合?查看ASCII控制编码,我们发现表中竟然没有定义上下左右光标键的ASCII值!

因此,xterm.js就得自行定义相应的字符系列来表示各类行为。在其Supported Terminal Sequences中,先定义了:

ESC: sequence starting with ESC (\x1B) CSI - Control Sequence Introducer: sequence starting with ESC [ (7bit) or ...

即,CSI控制系列生成器)的格式为:\x1B + [ + 7位

接着,继续定义:

Mnemonic Name Sequence Short Description ---------------------------------------------------------------------------------- CUU Cursor Up CSI Ps A Move cursor Ps times up (default=1). CUD Cursor Down CSI Ps B Move cursor Ps times down (default=1). CUF Cursor Forward CSI Ps C Move cursor Ps times forward (default=1). CUB Cursor Backward CSI Ps D Move cursor Ps backward (default=1).

首先,我们看懂了这个表。表中的格式与我们上面的代码\x1B[A等的格式是一样的,因此从此表中我们也得出验证,\x1B[A的作用是向上移动光标。

其次,仅从我们上面自己摸索还不知道的是,\x1B[A还可以指定移动次数,如\x1B[3A则向上移动光标3次。

于是,我们可以编写以下代码:

term.writeln("First line\r\nSecond Line\r\nThird Line"); term.write("\x1B[2A");

在终端中点击鼠标,使其获得输入焦点,就可看到光标。程序运行后,光标原位于第4行,然后,我们向上移动光标2次,光标最终来到了第2行。

再探文本颜色的奥秘

在第1节中,我们使用下面的代码:

term.writeln("This is \x1B[1;3;31mxterm.js\x1B[0m console.");

有了上节的基础,现在,我们看能否搞懂这些难看的字符系列。

观察上面的代码格式,产生红色的相应代码为:

\x1B[1;3;31mxterm.js

xterm.js为要设置字符颜色的字符串对象,剥离它,则代码简化为:

\x1B[1;3;31m

分离格式:

\x1B[ + ... + m

可以看出,这个属性属于上面所谈到的CSI系列,我们在Supported Terminal Sequences的网页中查找上面的格式,则在CSI一节中找到了:

SGR Select Graphic Rendition CSI Pm m Set/Reset various text attributes. more Partial

SGR意为选择图像渲染方式CSI Pm m对应于上面的格式,用于设置/重置各种文本属性。点击上面的more文本,则可看到更多的隐藏的参数。这些参数以数字作为参数名,一直排到107

查表,参数1表示粗体3表示斜体31表示红色前景颜色。跟上面的效果完全对上了。CSI Pm m中的参数Pm可支持同时设置最多32个参数,参数间使用;来分隔。

同理,对于\x1B[0m,查表,参数0表示重置前面的所有属性值。这就是为何上面的字符串中consle.又恢复了白色的原因。

好,现准备将上面的字符串改为绿色。查表,32表示绿色前景颜色

term.writeln("This is \x1B[1;3;32mxterm.js\x1B[0m console.");

参考资源

General

  1. npm Home
  2. unpkg Home

xterm

  1. xterm.js Home
  2. Interface: ITheme
  3. xterm.js (Github)
  4. xterm src (unpkg)