/* 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 // // 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_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}\\`; }