/* jslint node: true */ 'use strict'; // // ANSI Terminal Support // // Resources: // * http://ansi-bbs.org/ // * http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/ansisys.txt // * http://en.wikipedia.org/wiki/ANSI_escape_code // * https://github.com/chjj/term.js/blob/master/src/term.js // * http://www.inwap.com/pdp10/ansicode.txt // var assert = require('assert'); var binary = require('binary'); var miscUtil = require('./misc_util.js'); var _ = require('lodash'); 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.deleteLine = deleteLine; exports.disableVT100LineWrapping = disableVT100LineWrapping; exports.setSyncTERMFont = setSyncTERMFont; exports.getSyncTERMFontFromAlias = getSyncTERMFontFromAlias; exports.setCursorStyle = setCursorStyle; exports.setEmulatedBaudRate = setEmulatedBaudRate; // // See also // https://github.com/TooTallNate/ansi.js/blob/master/lib/ansi.js var ESC_CSI = '\u001b['; var 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 // var 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 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 // var 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. // var 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) { var p1 = miscUtil.valueWithDefault(fontPage, 0); assert(p1 >= 0 && p1 <= 3); var 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, '_')]; } var 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) { var 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) { var code = CONTROL[name]; exports[name] = function() { var 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(function onSgrName(name) { var 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 ''; } var result = ''; // :TODO: this method needs a lot of cleanup! var args = Array.isArray(arguments[0]) ? arguments[0] : arguments; for(var i = 0; i < args.length; i++) { if(typeof args[i] === 'string') { if(args[i] in SGRValues) { if(result.length > 0) { result += ';'; } result += SGRValues[args[i]]; } } else if(typeof args[i] === 'number') { if(result.length > 0) { result += ';'; } result += args[i]; } } return ESC_CSI + result + 'm'; } // // Converts a Graphic Rendition object used elsewhere // to a ANSI SGR sequence. // function getSGRFromGraphicRendition(graphicRendition, initialReset) { var sgrSeq = []; var styleCount = 0; [ 'intensity', 'underline', 'blink', 'negative', 'invisible' ].forEach(function style(s) { if(graphicRendition[s]) { sgrSeq.push(graphicRendition[s]); ++styleCount; } }); if(!styleCount) { sgrSeq.push(0); } if(graphicRendition.fg) { sgrSeq.push(graphicRendition.fg); } if(graphicRendition.bg) { sgrSeq.push(graphicRendition.bg); } if(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 } // // Delete line(s) // This method acts like ESC[ p1 M but should work // for all terminals via using eraseLine and movement // /* function deleteLine(count) { count = count || 1; console.log(exports.eraseLine) var seq = exports.eraseLine(2); // 2 = entire line var i; for(i = 1; i < count; ++i) { seq += '\n' + // down a line exports.eraseLine(2); // erase it } // now, move back up any we lines we went down if(count > 1) { seq += exports.up(count - 1); } return seq; } */ // // See http://www.termsys.demon.co.uk/vtANSI_BBS.htm // function disableVT100LineWrapping() { return ESC_CSI + '7l'; } function setEmulatedBaudRate(rate) { var 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); }