512 lines
13 KiB
JavaScript
512 lines
13 KiB
JavaScript
/* jslint node: true */
|
|
'use strict';
|
|
|
|
//
|
|
// ANSI Terminal Support Resources
|
|
//
|
|
// ANSI-BBS
|
|
// * http://ansi-bbs.org/
|
|
//
|
|
// CTerm / SyncTERM
|
|
// * https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt
|
|
//
|
|
// BananaCom
|
|
// * http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt
|
|
//
|
|
// ANSI.SYS
|
|
// * http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/ansisys.txt
|
|
// * http://academic.evergreen.edu/projects/biophysics/technotes/program/ansi_esc.htm
|
|
//
|
|
// Modern Windows (Win10+)
|
|
// * https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
|
|
//
|
|
// VT100
|
|
// * http://www.noah.org/python/pexpect/ANSI-X3.64.htm
|
|
//
|
|
// VTX
|
|
// * https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt
|
|
//
|
|
// General
|
|
// * http://en.wikipedia.org/wiki/ANSI_escape_code
|
|
// * http://www.inwap.com/pdp10/ansicode.txt
|
|
// * Excellent information with many standards covered (for hterm):
|
|
// https://chromium.googlesource.com/apps/libapps/+/master/hterm/doc/ControlSequences.md
|
|
//
|
|
// Other Implementations
|
|
// * https://github.com/chjj/term.js/blob/master/src/term.js
|
|
//
|
|
//
|
|
// For a board, we need to support the semi-standard ANSI-BBS "spec" which
|
|
// is bastardized mix of DOS ANSI.SYS, cterm.txt, bansi.txt and a little other.
|
|
// This gives us NetRunner, SyncTERM, EtherTerm, most *nix terminals, compatibilitiy
|
|
// with legit oldschool DOS terminals, and so on.
|
|
//
|
|
|
|
// ENiGMA½
|
|
const miscUtil = require('./misc_util.js');
|
|
|
|
// deps
|
|
const assert = require('assert');
|
|
const _ = require('lodash');
|
|
|
|
exports.getFullMatchRegExp = getFullMatchRegExp;
|
|
exports.getFGColorValue = getFGColorValue;
|
|
exports.getBGColorValue = getBGColorValue;
|
|
exports.sgr = sgr;
|
|
exports.getSGRFromGraphicRendition = getSGRFromGraphicRendition;
|
|
exports.clearScreen = clearScreen;
|
|
exports.resetScreen = resetScreen;
|
|
exports.normal = normal;
|
|
exports.goHome = goHome;
|
|
exports.disableVT100LineWrapping = disableVT100LineWrapping;
|
|
exports.setSyncTermFont = setSyncTermFont;
|
|
exports.getSyncTermFontFromAlias = getSyncTermFontFromAlias;
|
|
exports.setSyncTermFontWithAlias = setSyncTermFontWithAlias;
|
|
exports.setCursorStyle = setCursorStyle;
|
|
exports.setEmulatedBaudRate = setEmulatedBaudRate;
|
|
exports.vtxHyperlink = vtxHyperlink;
|
|
|
|
//
|
|
// See also
|
|
// https://github.com/TooTallNate/ansi.js/blob/master/lib/ansi.js
|
|
|
|
const ESC_CSI = '\u001b[';
|
|
|
|
const CONTROL = {
|
|
up: 'A',
|
|
down: 'B',
|
|
|
|
forward: 'C',
|
|
right: 'C',
|
|
|
|
back: 'D',
|
|
left: 'D',
|
|
|
|
nextLine: 'E',
|
|
prevLine: 'F',
|
|
horizAbsolute: 'G',
|
|
|
|
//
|
|
// CSI [ p1 ] J
|
|
// Erase in Page / Erase Data
|
|
// Defaults: p1 = 0
|
|
// Erases from the current screen according to the value of p1
|
|
// 0 - Erase from the current position to the end of the screen.
|
|
// 1 - Erase from the current position to the start of the screen.
|
|
// 2 - Erase entire screen. As a violation of ECMA-048, also moves
|
|
// the cursor to position 1/1 as a number of BBS programs assume
|
|
// this behaviour.
|
|
// Erased characters are set to the current attribute.
|
|
//
|
|
// Support:
|
|
// * SyncTERM: Works as expected
|
|
// * NetRunner: Always clears a screen *height* (e.g. 25) regardless of p1
|
|
// and screen remainder
|
|
//
|
|
eraseData: 'J',
|
|
|
|
eraseLine: 'K',
|
|
insertLine: 'L',
|
|
|
|
//
|
|
// CSI [ p1 ] M
|
|
// Delete Line(s) / "ANSI" Music
|
|
// Defaults: p1 = 1
|
|
// Deletes the current line and the p1 - 1 lines after it scrolling the
|
|
// first non-deleted line up to the current line and filling the newly
|
|
// empty lines at the end of the screen with the current attribute.
|
|
// If "ANSI" Music is fully enabled (CSI = 2 M), performs "ANSI" music
|
|
// instead.
|
|
// See "ANSI" MUSIC section for more details.
|
|
//
|
|
// Support:
|
|
// * SyncTERM: Works as expected
|
|
// * NetRunner:
|
|
//
|
|
// General Notes:
|
|
// See also notes in bansi.txt and cterm.txt about the various
|
|
// incompatibilities & oddities around this sequence. ANSI-BBS
|
|
// states that it *should* work with any value of p1.
|
|
//
|
|
deleteLine: 'M',
|
|
ansiMusic: 'M',
|
|
|
|
scrollUp: 'S',
|
|
scrollDown: 'T',
|
|
setScrollRegion: 'r',
|
|
savePos: 's',
|
|
restorePos: 'u',
|
|
queryPos: '6n',
|
|
queryScreenSize: '255n', // See bansi.txt
|
|
goto: 'H', // row Pr, column Pc -- same as f
|
|
gotoAlt: 'f', // same as H
|
|
|
|
blinkToBrightIntensity: '?33h',
|
|
blinkNormal: '?33l',
|
|
|
|
emulationSpeed: '*r', // Set output emulation speed. See cterm.txt
|
|
|
|
hideCursor: '?25l', // Nonstandard - cterm.txt
|
|
showCursor: '?25h', // Nonstandard - cterm.txt
|
|
|
|
queryDeviceAttributes: 'c', // Nonstandard - cterm.txt
|
|
|
|
// :TODO: see https://code.google.com/p/conemu-maximus5/wiki/AnsiEscapeCodes
|
|
// apparently some terms can report screen size and text area via 18t and 19t
|
|
};
|
|
|
|
//
|
|
// Select Graphics Rendition
|
|
// See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt
|
|
//
|
|
const SGRValues = {
|
|
reset: 0,
|
|
bold: 1,
|
|
dim: 2,
|
|
blink: 5,
|
|
fastBlink: 6,
|
|
negative: 7,
|
|
hidden: 8,
|
|
|
|
normal: 22, //
|
|
steady: 25,
|
|
positive: 27,
|
|
|
|
black: 30,
|
|
red: 31,
|
|
green: 32,
|
|
yellow: 33,
|
|
blue: 34,
|
|
magenta: 35,
|
|
cyan: 36,
|
|
white: 37,
|
|
|
|
blackBG: 40,
|
|
redBG: 41,
|
|
greenBG: 42,
|
|
yellowBG: 43,
|
|
blueBG: 44,
|
|
magentaBG: 45,
|
|
cyanBG: 46,
|
|
whiteBG: 47,
|
|
};
|
|
|
|
function getFullMatchRegExp(flags = 'g') {
|
|
// :TODO: expand this a bit - see strip-ansi/etc.
|
|
// :TODO: \u009b ?
|
|
return new RegExp(
|
|
/[\u001b][[()#;?]*([0-9]{1,4}(?:;[0-9]{0,4})*)?([0-9A-ORZcf-npqrsuy=><])/,
|
|
flags
|
|
); // eslint-disable-line no-control-regex
|
|
}
|
|
|
|
function getFGColorValue(name) {
|
|
return SGRValues[name];
|
|
}
|
|
|
|
function getBGColorValue(name) {
|
|
return SGRValues[name + 'BG'];
|
|
}
|
|
|
|
// See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt
|
|
// :TODO: document
|
|
// :TODO: Create mappings for aliases... maybe make this a map to values instead
|
|
// :TODO: Break this up in to two parts:
|
|
// 1) FONT_AND_CODE_PAGES (e.g. SyncTERM/cterm)
|
|
// 2) SAUCE_FONT_MAP: Sauce name(s) -> items in FONT_AND_CODE_PAGES.
|
|
// ...we can then have getFontFromSAUCEName(sauceFontName)
|
|
// Also, create a SAUCE_ENCODING_MAP: SAUCE font name -> encodings
|
|
|
|
//
|
|
// An array of CTerm/SyncTERM font/encoding values. Each entry's index
|
|
// corresponds to it's escape sequence value (e.g. cp437 = 0)
|
|
//
|
|
// See https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt
|
|
//
|
|
const SYNCTERM_FONT_AND_ENCODING_TABLE = [
|
|
'cp437',
|
|
'cp1251',
|
|
'koi8_r',
|
|
'iso8859_2',
|
|
'iso8859_4',
|
|
'cp866',
|
|
'iso8859_9',
|
|
'haik8',
|
|
'iso8859_8',
|
|
'koi8_u',
|
|
'iso8859_15',
|
|
'iso8859_4',
|
|
'koi8_r_b',
|
|
'iso8859_4',
|
|
'iso8859_5',
|
|
'ARMSCII_8',
|
|
'iso8859_15',
|
|
'cp850',
|
|
'cp850',
|
|
'cp885',
|
|
'cp1251',
|
|
'iso8859_7',
|
|
'koi8-r_c',
|
|
'iso8859_4',
|
|
'iso8859_1',
|
|
'cp866',
|
|
'cp437',
|
|
'cp866',
|
|
'cp885',
|
|
'cp866_u',
|
|
'iso8859_1',
|
|
'cp1131',
|
|
'c64_upper',
|
|
'c64_lower',
|
|
'c128_upper',
|
|
'c128_lower',
|
|
'atari',
|
|
'pot_noodle',
|
|
'mo_soul',
|
|
'microknight_plus',
|
|
'topaz_plus',
|
|
'microknight',
|
|
'topaz',
|
|
];
|
|
|
|
//
|
|
// A map of various font name/aliases such as those used
|
|
// in SAUCE records to SyncTERM/CTerm names
|
|
//
|
|
// This table contains lowercased entries with any spaces
|
|
// replaced with '_' for lookup purposes.
|
|
//
|
|
const FONT_ALIAS_TO_SYNCTERM_MAP = {
|
|
cp437: 'cp437',
|
|
ibm_vga: 'cp437',
|
|
ibmpc: 'cp437',
|
|
ibm_pc: 'cp437',
|
|
pc: 'cp437',
|
|
cp437_art: 'cp437',
|
|
ibmpcart: 'cp437',
|
|
ibmpc_art: 'cp437',
|
|
ibm_pc_art: 'cp437',
|
|
msdos_art: 'cp437',
|
|
msdosart: 'cp437',
|
|
pc_art: 'cp437',
|
|
pcart: 'cp437',
|
|
|
|
ibm_vga50: 'cp437',
|
|
ibm_vga25g: 'cp437',
|
|
ibm_ega: 'cp437',
|
|
ibm_ega43: 'cp437',
|
|
|
|
topaz: 'topaz',
|
|
amiga_topaz_1: 'topaz',
|
|
'amiga_topaz_1+': 'topaz_plus',
|
|
topazplus: 'topaz_plus',
|
|
topaz_plus: 'topaz_plus',
|
|
amiga_topaz_2: 'topaz',
|
|
'amiga_topaz_2+': 'topaz_plus',
|
|
topaz2plus: 'topaz_plus',
|
|
|
|
pot_noodle: 'pot_noodle',
|
|
p0tnoodle: 'pot_noodle',
|
|
'amiga_p0t-noodle': 'pot_noodle',
|
|
|
|
mo_soul: 'mo_soul',
|
|
mosoul: 'mo_soul',
|
|
"mo'soul": 'mo_soul',
|
|
amiga_mosoul: 'mo_soul',
|
|
|
|
amiga_microknight: 'microknight',
|
|
'amiga_microknight+': 'microknight_plus',
|
|
|
|
atari: 'atari',
|
|
atarist: 'atari',
|
|
};
|
|
|
|
function setSyncTermFont(name, fontPage) {
|
|
const p1 = miscUtil.valueWithDefault(fontPage, 0);
|
|
|
|
assert(p1 >= 0 && p1 <= 3);
|
|
|
|
const p2 = SYNCTERM_FONT_AND_ENCODING_TABLE.indexOf(name);
|
|
if (p2 > -1) {
|
|
return `${ESC_CSI}${p1};${p2} D`;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
function getSyncTermFontFromAlias(alias) {
|
|
return FONT_ALIAS_TO_SYNCTERM_MAP[alias.toLowerCase().replace(/ /g, '_')];
|
|
}
|
|
|
|
function setSyncTermFontWithAlias(nameOrAlias) {
|
|
nameOrAlias = getSyncTermFontFromAlias(nameOrAlias) || nameOrAlias;
|
|
return setSyncTermFont(nameOrAlias);
|
|
}
|
|
|
|
const DEC_CURSOR_STYLE = {
|
|
'blinking block': 0,
|
|
default: 1,
|
|
'steady block': 2,
|
|
'blinking underline': 3,
|
|
'steady underline': 4,
|
|
'blinking bar': 5,
|
|
'steady bar': 6,
|
|
};
|
|
|
|
function setCursorStyle(cursorStyle) {
|
|
const ps = DEC_CURSOR_STYLE[cursorStyle];
|
|
if (ps) {
|
|
return `${ESC_CSI}${ps} q`;
|
|
}
|
|
return '';
|
|
}
|
|
|
|
// Create methods such as up(), nextLine(),...
|
|
Object.keys(CONTROL).forEach(function onControlName(name) {
|
|
const code = CONTROL[name];
|
|
|
|
exports[name] = function () {
|
|
let c = code;
|
|
if (arguments.length > 0) {
|
|
// arguments are array like -- we want an array
|
|
c = Array.prototype.slice.call(arguments).map(Math.round).join(';') + code;
|
|
}
|
|
return `${ESC_CSI}${c}`;
|
|
};
|
|
});
|
|
|
|
// Create various color methods such as white(), yellowBG(), reset(), ...
|
|
Object.keys(SGRValues).forEach(name => {
|
|
const code = SGRValues[name];
|
|
|
|
exports[name] = function () {
|
|
return `${ESC_CSI}${code}m`;
|
|
};
|
|
});
|
|
|
|
function sgr() {
|
|
//
|
|
// - Allow an single array or variable number of arguments
|
|
// - Each element can be either a integer or string found in SGRValues
|
|
// which in turn maps to a integer
|
|
//
|
|
if (arguments.length <= 0) {
|
|
return '';
|
|
}
|
|
|
|
let result = [];
|
|
const args = Array.isArray(arguments[0]) ? arguments[0] : arguments;
|
|
|
|
for (let i = 0; i < args.length; ++i) {
|
|
const arg = args[i];
|
|
if (_.isString(arg) && arg in SGRValues) {
|
|
result.push(SGRValues[arg]);
|
|
} else if (_.isNumber(arg)) {
|
|
result.push(arg);
|
|
}
|
|
}
|
|
|
|
return `${ESC_CSI}${result.join(';')}m`;
|
|
}
|
|
|
|
//
|
|
// Converts a Graphic Rendition object used elsewhere
|
|
// to a ANSI SGR sequence.
|
|
//
|
|
function getSGRFromGraphicRendition(graphicRendition, initialReset) {
|
|
let sgrSeq = [];
|
|
let styleCount = 0;
|
|
|
|
['intensity', 'underline', 'blink', 'negative', 'invisible'].forEach(s => {
|
|
if (graphicRendition[s]) {
|
|
sgrSeq.push(graphicRendition[s]);
|
|
++styleCount;
|
|
}
|
|
});
|
|
|
|
if (graphicRendition.fg) {
|
|
sgrSeq.push(graphicRendition.fg);
|
|
}
|
|
|
|
if (graphicRendition.bg) {
|
|
sgrSeq.push(graphicRendition.bg);
|
|
}
|
|
|
|
if (0 === styleCount || initialReset) {
|
|
sgrSeq.unshift(0);
|
|
}
|
|
|
|
return sgr(sgrSeq);
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Shortcuts for common functions
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
function clearScreen() {
|
|
return exports.eraseData(2);
|
|
}
|
|
|
|
function resetScreen() {
|
|
return `${exports.reset()}${exports.eraseData(2)}${exports.goHome()}`;
|
|
}
|
|
|
|
function normal() {
|
|
return sgr(['normal', 'reset']);
|
|
}
|
|
|
|
function goHome() {
|
|
return exports.goto(); // no params = home = 1,1
|
|
}
|
|
|
|
//
|
|
// Disable auto line wraping @ termWidth
|
|
//
|
|
// See:
|
|
// http://stjarnhimlen.se/snippets/vt100.txt
|
|
// https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt
|
|
//
|
|
// WARNING:
|
|
// * Not honored by all clients
|
|
// * If it is honored, ANSI's that rely on this (e.g. do not have \r\n endings
|
|
// and use term width -- generally 80 columns -- will display garbled!
|
|
//
|
|
function disableVT100LineWrapping() {
|
|
return `${ESC_CSI}?7l`;
|
|
}
|
|
|
|
function setEmulatedBaudRate(rate) {
|
|
const speed =
|
|
{
|
|
unlimited: 0,
|
|
off: 0,
|
|
0: 0,
|
|
300: 1,
|
|
600: 2,
|
|
1200: 3,
|
|
2400: 4,
|
|
4800: 5,
|
|
9600: 6,
|
|
19200: 7,
|
|
38400: 8,
|
|
57600: 9,
|
|
76800: 10,
|
|
115200: 11,
|
|
}[rate] || 0;
|
|
return 0 === speed ? exports.emulationSpeed() : exports.emulationSpeed(1, speed);
|
|
}
|
|
|
|
function vtxHyperlink(client, url, len) {
|
|
if (!client.terminalSupports('vtx_hyperlink')) {
|
|
return '';
|
|
}
|
|
|
|
len = len || url.length;
|
|
|
|
url = url
|
|
.split('')
|
|
.map(c => c.charCodeAt(0))
|
|
.join(';');
|
|
return `${ESC_CSI}1;${len};1;1;${url}\\`;
|
|
}
|