如何解析游标 ANSI 转义码?
How to parse cursor ANSI escape codes?
我正在为 jQuery 终端编写用于处理光标的 ANSI 转义码的代码。但是有问题,不确定它应该如何工作,我得到了奇怪的结果。
我正在使用 ervy library 进行测试。
并使用此代码:
function scatter_plot() {
const scatterData = [];
for (let i = 1; i < 17; i++) {
i < 6 ? scatterData.push({ key: 'A', value: [i, i], style: ervy.fg('red', '*') })
: scatterData.push({ key: 'A', value: [i, 6], style: ervy.fg('red', '*') });
}
scatterData.push({ key: 'B', value: [2, 6], style: ervy.fg('blue', '# '), side: 2 });
scatterData.push({ key: 'C', value: [0, 0], style: ervy.bg('cyan', 2) });
var plot = ervy.scatter(scatterData, { legendGap: 18, width: 15 });
// same as Linux XTERM where 0 code is interpreted as 1.
var formatting = $.terminal.from_ansi(plot.replace(/\x1b\[0([A-D])/g, '\x1b[1'));
return formatting;
}
$.terminal.defaults.formatters = [];
var term = $('body').terminal();
term.echo(scatter_plot());
它应该看起来像 Linux Xterm:
但是看起来是这样的,看codepen demo
当我写问题时,移动光标时改变了几个 +1 和 -1(请参阅代码中处理 A-F ANSI 转义)给出了这个结果(代码片段有最新代码)。
第一行被空格覆盖,整幅图为上一右一(除了 0,0 青色点应位于“|”下方且宽度为 2 个字符,因此您应该看到它的右半部分,这个是正确的,但其余的不是)
这是我处理光标的新代码,我在处理颜色之前这样做,所以代码并不复杂。
// -------------------------------------------------------------------------------
var ansi_re = /(\x1B\[[0-9;]*[A-Za-z])/g;
var cursor_re = /(.*)\r?\n\x1b\[1A\x1b\[([0-9]+)C/;
var move_cursor_split = /(\x1b\[[0-9]+[A-G])/g;
var move_cursor_match = /^\x1b\[([0-9]+)([A-G])/;
// -------------------------------------------------------------------------------
function parse_ansi_cursor(input) {
/*
(function(log) {
console.log = function(...args) {
if (true || cursor.y === 11) {
return log.apply(console, args);
}
};
})(console.log);
*/
function length(text) {
return text.replace(ansi_re, '').length;
}
function get_index(text, x) {
var splitted = text.split(ansi_re);
var format = 0;
var count = 0;
var prev_count = 0;
for (var i = 0; i < splitted.length; i++) {
var string = splitted[i];
if (string) {
if (string.match(ansi_re)) {
format += string.length;
} else {
count += string.length;
if (count >= x) {
var rest = x - prev_count;
return format + rest;
}
prev_count = count;
}
}
}
return i;
}
// ansi aware substring, it just and add removed ansi escapes
// at the beginning we don't care if the were disabled with 0m
function substring(text, start, end) {
var result = text.substring(start, end);
if (start === 0 || !text.match(ansi_re)) {
return result;
}
var before = text.substring(0, start);
var match = before.match(ansi_re);
if (match) {
return before.match(ansi_re).join('') + result;
}
return result;
}
// insert text at cursor position
// result is array of splitted arrays that form single line
function insert(text) {
if (!text) {
return;
}
if (!result[cursor.y]) {
result[cursor.y] = [];
}
var index = 0;
var sum = 0;
var len, after;
function inject() {
index++;
if (result[cursor.y][index]) {
result[cursor.y].splice(index, 0, null);
}
}
if (cursor.y === 11) {
//debugger;
}
if (text == "[46m [0m") {
//debugger;
}
console.log({...cursor, text});
if (cursor.x === 0 && result[cursor.y][index]) {
source = result[cursor.y][0];
len = length(text);
var i = get_index(source, len);
if (length(source) < len) {
after = result[cursor.y][index + 1];
if (after) {
i = get_index(after, len - length(source));
after = substring(after, i);
result[cursor.y].splice(index, 2, null, after);
} else {
result[cursor.y].splice(index, 1, null);
}
} else {
after = substring(source, i);
result[cursor.y].splice(index, 1, null, after);
}
} else {
var limit = 100000; // infite loop guard
var prev_sum = 0;
// find in which substring to insert the text
while (index < cursor.x) {
if (!limit--) {
warn('[WARN] To many loops');
break;
}
var source = result[cursor.y][index];
if (!source) {
result[cursor.y].push(new Array(cursor.x - prev_sum).join(' '));
index++;
break;
}
if (sum === cursor.x) {
inject();
break;
}
len = length(source);
prev_sum = sum;
sum += len;
if (sum === cursor.x) {
inject();
break;
}
if (sum > cursor.x) {
var pivot = get_index(source, cursor.x - prev_sum);
var before = substring(source, 0, pivot);
var end = get_index(source, length(text));
after = substring(source, pivot + end);
if (!after.length) {
result[cursor.y].splice(index, 1, before);
} else {
result[cursor.y].splice(index, 1, before, null, after);
}
index++;
break;
} else {
index++;
}
}
}
cursor.x += length(text);
result[cursor.y][index] = text;
}
if (input.match(move_cursor_split)) {
var lines = input.split('\n').filter(Boolean);
var cursor = {x: 0, y: -1};
var result = [];
for (var i = 0; i < lines.length; ++i) {
console.log('-------------------------------------------------');
var string = lines[i];
cursor.x = 0;
cursor.y++;
var splitted = string.split(move_cursor_split).filter(Boolean);
for (var j = 0; j < splitted.length; ++j) {
var part = splitted[j];
console.log(part);
var match = part.match(move_cursor_match);
if (match) {
var ansi_code = match[2];
var value = +match[1];
console.log({code: ansi_code, value, ...cursor});
if (value === 0) {
continue;
}
switch (ansi_code) {
case 'A': // UP
cursor.y -= value;
break;
case 'B': // Down
cursor.y += value - 1;
break;
case 'C': // forward
cursor.x += value + 1;
break;
case 'D': // Back
cursor.x -= value + 1;
break;
case 'E': // Cursor Next Line
cursor.x = 0;
cursor.y += value - 1;
break;
case 'F': // Cursor Previous Line
cursor.x = 0;
cursor.y -= value + 1;
break;
}
if (cursor.x < 0) {
cursor.x = 0;
}
if (cursor.y < 0) {
cursor.y = 0;
}
} else {
insert(part);
}
}
}
return result.map(function(line) {
return line.join('');
}).join('\n');
}
return input;
}
代码中的result = [];
是行数组,在光标处插入文本时,单行可能会被拆分成多个子字符串,如果是字符串数组,代码可能会更简单。现在我只想修复光标位置。
这里是嵌入了from_ansi函数的codepen demo(里面有parse_ansi_cursor有问题)。对不起,代码很多,但解析ANSI转义码并不简单。
我不确定应该如何移动光标(现在它有 + 1 或 - 1,我不确定这个)我也不确定我是否应该增加 cursor.y 在每一行之前。我不是 100% 确定这应该如何工作。我查看了 Linux Xterm 代码,但没有找到任何线索。查看了 Xterm.js,但对于那些散点图,ervy 图完全被破坏了。
我的 from_ansi 函数的原始代码正在处理一些 ANSI 游标代码,如下所示:
input = input.replace(/\x1b\[([0-9]+)C/g, function(_, num) {
return new Array(+num + 1).join(' ');
});
仅 C,向前仅添加空格,它适用于 ANSI 艺术但不适用于 ervy 散点图。
我认为它不太宽泛,它只是关于使用 ANSI 转义码移动光标和处理换行符的问题。此外,它应该是简单的情况,光标应该只在单个字符串内移动,而不是像在真实终端中那样在外部移动(ervy plot 输出 ANSI 转义码那样)。
我对解释如何处理字符串以及如何移动光标的答案很好,但如果您能提供对代码的修复,我会很棒。我现在更喜欢修复我的代码,现在是全新的实现,除非它更简单并且它是一个函数 parse_ansi_cursor(input)
并且与其余代码的工作方式相同,但光标移动固定。
编辑:
我发现我的 input.split('\n').filter(Boolean)
是错误的应该是:
var lines = input.split('\n');
if (input.match(/^\n/)) {
lines.shift();
}
if (input.match(/\n$/)) {
lines.pop();
}
似乎一些旧的 ANSI 转义规范说 0 不是零,而是默认的占位符,即 1。它已从规范中删除,但 Xterm 仍在使用它。所以我添加了这一行用于解析代码,如果有 0A 或 A 的值为 1.
var value = match[1].match(/^0?$/) ? 1 : +match[1];
情节看起来好多了,但光标仍然存在问题。 (我认为是光标 - 我不是 100% 确定)。
我再次更改了 +1/-1,现在它更接近了(几乎与 XTerm 中的一样)。 Buss 仍然需要在我的代码中存在错误。
编辑:
@jerch 的回答我试过使用 node ansi parser,有同样的问题不知道如何处理光标:
var cursor = {x:0,y:0};
result = [];
var terminal = {
inst_p: function(s) {
var line = result[cursor.y];
if (!line) {
result[cursor.y] = s;
} else if (cursor.x === 0) {
result[cursor.y] = s + line.substring(s.length);
} else if (line.length < cursor.x) {
var len = cursor.x - (line.length - 1);
result[cursor.y] += new Array(len).join(' ') + s;
} else if (line.length === cursor.x) {
result[cursor.y] += s;
} else {
var before = line.substring(0, cursor.x);
var after = line.substring(cursor.x + s.length);
result[cursor.y] = before + s + after;
}
cursor.x += s.length;
console.log({s, ...cursor, line: result[cursor.y]});
},
inst_o: function(s) {console.log('osc', s);},
inst_x: function(flag) {
var code = flag.charCodeAt(0);
if (code === 10) {
cursor.y++;
cursor.x = 0;
}
},
inst_c: function(collected, params, flag) {
console.log({collected, params, flag});
var value = params[0] === 0 ? 1 : params[0];
switch(flag) {
case 'A': // UP
cursor.y -= value;
break;
case 'B': // Down
cursor.y += value - 1;
break;
case 'C': // forward
cursor.x += value;
break;
case 'D': // Back
cursor.x -= value;
break;
case 'E': // Cursor Next Line
cursor.x = 0;
cursor.y += value;
break;
case 'F': // Cursor Previous Line
cursor.x = 0;
cursor.y -= value;
break;
}
},
inst_e: function(collected, flag) {console.log('esc', collected, flag);},
inst_H: function(collected, params, flag) {console.log('dcs-Hook', collected, params, flag);},
inst_P: function(dcs) {console.log('dcs-Put', dcs);},
inst_U: function() {console.log('dcs-Unhook');}
};
var parser = new AnsiParser(terminal);
parser.parse(input);
return result.join('\n');
这只是一个忽略除换行符和光标移动之外的所有内容的简单示例。
这是输出:
更新:
似乎每个光标移动都应该只是 += value
或 -= value
,而我的 value - 1;
只是更正了 ervy 库中无法在 clear 终端上工作的错误。
首先 - 基于 Regexp 的方法不适合处理转义序列。这样做的原因是各种终端序列之间的复杂交互,因为一些中断了前者尚未关闭的一个,而另一些则在另一个中间继续工作(如一些控制代码)并且 "outer" 序列仍然会正确完成。您必须将所有这些边缘情况纳入每个正则表达式中(请参阅 https://github.com/xtermjs/xterm.js/issues/2607#issuecomment-562648768 了解说明)。
一般来说,解析转义序列非常棘手,我们甚至在 terminal-wg 中遇到了与此相关的问题。希望我们将来能够从中获得一些最低限度的解析要求。肯定不会是基于正则表达式的 ;)
综上所述,使用真正的解析器要容易得多,它可以处理所有边缘情况。 DEC 兼容解析器的一个很好的起点是 https://vt100.net/emu/dec_ansi_parser。对于游标处理,您必须使用所有操作至少处理这些状态:
- 地面
- 逃脱
- csi_entry
- csi_ignore
- csi_param
- csi_intermediate
加上所有其他州作为虚拟条目。控制代码也需要特别小心(操作 execute
),因为它们可能会随时干扰任何其他序列并产生不同的结果。
更糟糕的是,官方 ECMA-48 规范在某些方面与 DEC 解析器略有不同。现在使用的大多数仿真器仍然试图以 DEC VT100+ 兼容性为目标。
如果您不想自己编写解析器,您可以 use/modify 我的旧 parser or the one we have in xterm.js(后者可能更难集成,因为它在 UTF32 代码点上运行)。
我正在为 jQuery 终端编写用于处理光标的 ANSI 转义码的代码。但是有问题,不确定它应该如何工作,我得到了奇怪的结果。
我正在使用 ervy library 进行测试。
并使用此代码:
function scatter_plot() {
const scatterData = [];
for (let i = 1; i < 17; i++) {
i < 6 ? scatterData.push({ key: 'A', value: [i, i], style: ervy.fg('red', '*') })
: scatterData.push({ key: 'A', value: [i, 6], style: ervy.fg('red', '*') });
}
scatterData.push({ key: 'B', value: [2, 6], style: ervy.fg('blue', '# '), side: 2 });
scatterData.push({ key: 'C', value: [0, 0], style: ervy.bg('cyan', 2) });
var plot = ervy.scatter(scatterData, { legendGap: 18, width: 15 });
// same as Linux XTERM where 0 code is interpreted as 1.
var formatting = $.terminal.from_ansi(plot.replace(/\x1b\[0([A-D])/g, '\x1b[1'));
return formatting;
}
$.terminal.defaults.formatters = [];
var term = $('body').terminal();
term.echo(scatter_plot());
它应该看起来像 Linux Xterm:
但是看起来是这样的,看codepen demo
当我写问题时,移动光标时改变了几个 +1 和 -1(请参阅代码中处理 A-F ANSI 转义)给出了这个结果(代码片段有最新代码)。
第一行被空格覆盖,整幅图为上一右一(除了 0,0 青色点应位于“|”下方且宽度为 2 个字符,因此您应该看到它的右半部分,这个是正确的,但其余的不是)
这是我处理光标的新代码,我在处理颜色之前这样做,所以代码并不复杂。
// -------------------------------------------------------------------------------
var ansi_re = /(\x1B\[[0-9;]*[A-Za-z])/g;
var cursor_re = /(.*)\r?\n\x1b\[1A\x1b\[([0-9]+)C/;
var move_cursor_split = /(\x1b\[[0-9]+[A-G])/g;
var move_cursor_match = /^\x1b\[([0-9]+)([A-G])/;
// -------------------------------------------------------------------------------
function parse_ansi_cursor(input) {
/*
(function(log) {
console.log = function(...args) {
if (true || cursor.y === 11) {
return log.apply(console, args);
}
};
})(console.log);
*/
function length(text) {
return text.replace(ansi_re, '').length;
}
function get_index(text, x) {
var splitted = text.split(ansi_re);
var format = 0;
var count = 0;
var prev_count = 0;
for (var i = 0; i < splitted.length; i++) {
var string = splitted[i];
if (string) {
if (string.match(ansi_re)) {
format += string.length;
} else {
count += string.length;
if (count >= x) {
var rest = x - prev_count;
return format + rest;
}
prev_count = count;
}
}
}
return i;
}
// ansi aware substring, it just and add removed ansi escapes
// at the beginning we don't care if the were disabled with 0m
function substring(text, start, end) {
var result = text.substring(start, end);
if (start === 0 || !text.match(ansi_re)) {
return result;
}
var before = text.substring(0, start);
var match = before.match(ansi_re);
if (match) {
return before.match(ansi_re).join('') + result;
}
return result;
}
// insert text at cursor position
// result is array of splitted arrays that form single line
function insert(text) {
if (!text) {
return;
}
if (!result[cursor.y]) {
result[cursor.y] = [];
}
var index = 0;
var sum = 0;
var len, after;
function inject() {
index++;
if (result[cursor.y][index]) {
result[cursor.y].splice(index, 0, null);
}
}
if (cursor.y === 11) {
//debugger;
}
if (text == "[46m [0m") {
//debugger;
}
console.log({...cursor, text});
if (cursor.x === 0 && result[cursor.y][index]) {
source = result[cursor.y][0];
len = length(text);
var i = get_index(source, len);
if (length(source) < len) {
after = result[cursor.y][index + 1];
if (after) {
i = get_index(after, len - length(source));
after = substring(after, i);
result[cursor.y].splice(index, 2, null, after);
} else {
result[cursor.y].splice(index, 1, null);
}
} else {
after = substring(source, i);
result[cursor.y].splice(index, 1, null, after);
}
} else {
var limit = 100000; // infite loop guard
var prev_sum = 0;
// find in which substring to insert the text
while (index < cursor.x) {
if (!limit--) {
warn('[WARN] To many loops');
break;
}
var source = result[cursor.y][index];
if (!source) {
result[cursor.y].push(new Array(cursor.x - prev_sum).join(' '));
index++;
break;
}
if (sum === cursor.x) {
inject();
break;
}
len = length(source);
prev_sum = sum;
sum += len;
if (sum === cursor.x) {
inject();
break;
}
if (sum > cursor.x) {
var pivot = get_index(source, cursor.x - prev_sum);
var before = substring(source, 0, pivot);
var end = get_index(source, length(text));
after = substring(source, pivot + end);
if (!after.length) {
result[cursor.y].splice(index, 1, before);
} else {
result[cursor.y].splice(index, 1, before, null, after);
}
index++;
break;
} else {
index++;
}
}
}
cursor.x += length(text);
result[cursor.y][index] = text;
}
if (input.match(move_cursor_split)) {
var lines = input.split('\n').filter(Boolean);
var cursor = {x: 0, y: -1};
var result = [];
for (var i = 0; i < lines.length; ++i) {
console.log('-------------------------------------------------');
var string = lines[i];
cursor.x = 0;
cursor.y++;
var splitted = string.split(move_cursor_split).filter(Boolean);
for (var j = 0; j < splitted.length; ++j) {
var part = splitted[j];
console.log(part);
var match = part.match(move_cursor_match);
if (match) {
var ansi_code = match[2];
var value = +match[1];
console.log({code: ansi_code, value, ...cursor});
if (value === 0) {
continue;
}
switch (ansi_code) {
case 'A': // UP
cursor.y -= value;
break;
case 'B': // Down
cursor.y += value - 1;
break;
case 'C': // forward
cursor.x += value + 1;
break;
case 'D': // Back
cursor.x -= value + 1;
break;
case 'E': // Cursor Next Line
cursor.x = 0;
cursor.y += value - 1;
break;
case 'F': // Cursor Previous Line
cursor.x = 0;
cursor.y -= value + 1;
break;
}
if (cursor.x < 0) {
cursor.x = 0;
}
if (cursor.y < 0) {
cursor.y = 0;
}
} else {
insert(part);
}
}
}
return result.map(function(line) {
return line.join('');
}).join('\n');
}
return input;
}
代码中的result = [];
是行数组,在光标处插入文本时,单行可能会被拆分成多个子字符串,如果是字符串数组,代码可能会更简单。现在我只想修复光标位置。
这里是嵌入了from_ansi函数的codepen demo(里面有parse_ansi_cursor有问题)。对不起,代码很多,但解析ANSI转义码并不简单。
我不确定应该如何移动光标(现在它有 + 1 或 - 1,我不确定这个)我也不确定我是否应该增加 cursor.y 在每一行之前。我不是 100% 确定这应该如何工作。我查看了 Linux Xterm 代码,但没有找到任何线索。查看了 Xterm.js,但对于那些散点图,ervy 图完全被破坏了。
我的 from_ansi 函数的原始代码正在处理一些 ANSI 游标代码,如下所示:
input = input.replace(/\x1b\[([0-9]+)C/g, function(_, num) {
return new Array(+num + 1).join(' ');
});
仅 C,向前仅添加空格,它适用于 ANSI 艺术但不适用于 ervy 散点图。
我认为它不太宽泛,它只是关于使用 ANSI 转义码移动光标和处理换行符的问题。此外,它应该是简单的情况,光标应该只在单个字符串内移动,而不是像在真实终端中那样在外部移动(ervy plot 输出 ANSI 转义码那样)。
我对解释如何处理字符串以及如何移动光标的答案很好,但如果您能提供对代码的修复,我会很棒。我现在更喜欢修复我的代码,现在是全新的实现,除非它更简单并且它是一个函数 parse_ansi_cursor(input)
并且与其余代码的工作方式相同,但光标移动固定。
编辑:
我发现我的 input.split('\n').filter(Boolean)
是错误的应该是:
var lines = input.split('\n');
if (input.match(/^\n/)) {
lines.shift();
}
if (input.match(/\n$/)) {
lines.pop();
}
似乎一些旧的 ANSI 转义规范说 0 不是零,而是默认的占位符,即 1。它已从规范中删除,但 Xterm 仍在使用它。所以我添加了这一行用于解析代码,如果有 0A 或 A 的值为 1.
var value = match[1].match(/^0?$/) ? 1 : +match[1];
情节看起来好多了,但光标仍然存在问题。 (我认为是光标 - 我不是 100% 确定)。
我再次更改了 +1/-1,现在它更接近了(几乎与 XTerm 中的一样)。 Buss 仍然需要在我的代码中存在错误。
编辑:
@jerch 的回答我试过使用 node ansi parser,有同样的问题不知道如何处理光标:
var cursor = {x:0,y:0};
result = [];
var terminal = {
inst_p: function(s) {
var line = result[cursor.y];
if (!line) {
result[cursor.y] = s;
} else if (cursor.x === 0) {
result[cursor.y] = s + line.substring(s.length);
} else if (line.length < cursor.x) {
var len = cursor.x - (line.length - 1);
result[cursor.y] += new Array(len).join(' ') + s;
} else if (line.length === cursor.x) {
result[cursor.y] += s;
} else {
var before = line.substring(0, cursor.x);
var after = line.substring(cursor.x + s.length);
result[cursor.y] = before + s + after;
}
cursor.x += s.length;
console.log({s, ...cursor, line: result[cursor.y]});
},
inst_o: function(s) {console.log('osc', s);},
inst_x: function(flag) {
var code = flag.charCodeAt(0);
if (code === 10) {
cursor.y++;
cursor.x = 0;
}
},
inst_c: function(collected, params, flag) {
console.log({collected, params, flag});
var value = params[0] === 0 ? 1 : params[0];
switch(flag) {
case 'A': // UP
cursor.y -= value;
break;
case 'B': // Down
cursor.y += value - 1;
break;
case 'C': // forward
cursor.x += value;
break;
case 'D': // Back
cursor.x -= value;
break;
case 'E': // Cursor Next Line
cursor.x = 0;
cursor.y += value;
break;
case 'F': // Cursor Previous Line
cursor.x = 0;
cursor.y -= value;
break;
}
},
inst_e: function(collected, flag) {console.log('esc', collected, flag);},
inst_H: function(collected, params, flag) {console.log('dcs-Hook', collected, params, flag);},
inst_P: function(dcs) {console.log('dcs-Put', dcs);},
inst_U: function() {console.log('dcs-Unhook');}
};
var parser = new AnsiParser(terminal);
parser.parse(input);
return result.join('\n');
这只是一个忽略除换行符和光标移动之外的所有内容的简单示例。
这是输出:
更新:
似乎每个光标移动都应该只是 += value
或 -= value
,而我的 value - 1;
只是更正了 ervy 库中无法在 clear 终端上工作的错误。
首先 - 基于 Regexp 的方法不适合处理转义序列。这样做的原因是各种终端序列之间的复杂交互,因为一些中断了前者尚未关闭的一个,而另一些则在另一个中间继续工作(如一些控制代码)并且 "outer" 序列仍然会正确完成。您必须将所有这些边缘情况纳入每个正则表达式中(请参阅 https://github.com/xtermjs/xterm.js/issues/2607#issuecomment-562648768 了解说明)。
一般来说,解析转义序列非常棘手,我们甚至在 terminal-wg 中遇到了与此相关的问题。希望我们将来能够从中获得一些最低限度的解析要求。肯定不会是基于正则表达式的 ;)
综上所述,使用真正的解析器要容易得多,它可以处理所有边缘情况。 DEC 兼容解析器的一个很好的起点是 https://vt100.net/emu/dec_ansi_parser。对于游标处理,您必须使用所有操作至少处理这些状态:
- 地面
- 逃脱
- csi_entry
- csi_ignore
- csi_param
- csi_intermediate
加上所有其他州作为虚拟条目。控制代码也需要特别小心(操作 execute
),因为它们可能会随时干扰任何其他序列并产生不同的结果。
更糟糕的是,官方 ECMA-48 规范在某些方面与 DEC 解析器略有不同。现在使用的大多数仿真器仍然试图以 DEC VT100+ 兼容性为目标。
如果您不想自己编写解析器,您可以 use/modify 我的旧 parser or the one we have in xterm.js(后者可能更难集成,因为它在 UTF32 代码点上运行)。