sbbs/load/session/viewdata.js

651 lines
18 KiB
JavaScript

const SESSION_VIEWDATA = (1<<2);
var SESSION_EXT = 'vtx';
var FRAME_WIDTH = 40;
var FRAME_HEIGHT = 22;
var FRAME_PROVIDER_LENGTH = 23;
var FRAME_PAGE_LENGTH = 11;
var FRAME_COST_LENGTH = 6;
var FRAME_ATTR_LENGTH = 1; // Space that an attribute takes
const VIEWDATA_LEFT = '\x08';
const VIEWDATA_RIGHT = '\x09';
const VIEWDATA_DOWN = '\x0a'; // \n
const VIEWDATA_UP = '\x0b';
const VIEWDATA_CLS = '\x0c';
const VIEWDATA_CR = '\x0d'; // \r
const VIEWDATA_CON = '\x11';
const VIEWDATA_COFF = '\x14';
const VIEWDATA_HOME = '\x1e';
const VIEWDATA_BLINK = '\x48';
const VIEWDATA_STEADY = '\x49';
const VIEWDATA_NORMAL = '\x4c';
const VIEWDATA_DOUBLE = '\x4d';
const VIEWDATA_CONCEAL = '\x58';
const VIEWDATA_BLOCKS = '\x59';
const VIEWDATA_SEPARATED = '\x5a';
const VIEWDATA_BLACKBACK = '\x5c';
const VIEWDATA_NEWBACK = '\x5d';
const VIEWDATA_HOLD = '\x5e';
const VIEWDATA_REVEAL = '\x5f';
const VIEWDATA_RED = '\x41';
const VIEWDATA_GREEN = '\x42';
const VIEWDATA_YELLOW = '\x43'; // C
const VIEWDATA_BLUE = '\x44';
const VIEWDATA_MAGENTA = '\x45';
const VIEWDATA_CYAN = '\x46';
const VIEWDATA_WHITE = '\x47';
const VIEWDATA_MOSIAC_RED = '\x51';
const VIEWDATA_MOSIAC_GREEN = '\x52';
const VIEWDATA_MOSIAC_YELLOW = '\x53';
const VIEWDATA_MOSIAC_BLUE = '\x54';
const VIEWDATA_MOSIAC_MAGENTA = '\x55';
const VIEWDATA_MOSIAC_CYAN = '\x56';
const VIEWDATA_MOSIAC_WHITE = '\x57'; // W
/* BINARY DUMP LEVEL 1 ATTRIBUTES */
const VIEWDATA_BIN_RED = '\x01';
const VIEWDATA_BIN_GREEN = '\x02';
const VIEWDATA_BIN_YELLOW = '\x03';
const VIEWDATA_BIN_BLUE = '\x04';
const VIEWDATA_BIN_MAGENTA = '\x05';
const VIEWDATA_BIN_CYAN = '\x06';
const VIEWDATA_BIN_WHITE = '\x07';
/**
* ViewData characters are 7bit (0x00-0x7f)
*
* Chars 0x00-0x1f are control characters (display attributes) and are sent to the terminal with 0x1b
* + 0x00-0x07 are foreground colors
* + 0x08-0x09 flash/steady
* + 0x0a-0x0b end/start box (?) *
* + 0x0c-0x0d normal/double height
* + 0x0e-0x0f double width (?) *
* + 0x10-0x17 are foreground graphics (mosiac) colors
* + 0x18/0x1f conceal/reveal
* + 0x19-0x1a solid/seperated graphics
* + 0x1b unused
* + 0x1c-1x1d Black/New Background (new background converts color foreground to background)
* + 0x1e-0x1f graphics hold/release (enables changing color and repeats previous graphics char)
* Chars 0x20-0x7f are normal printed ASCII chars
* Chars 0x20-0x3f & 0x60-0x7f when activated with a MOSIAC color sends a 2x3 pixel character
*
* We can map these into cga_defs with the following amendments:
* 0x00-0x0f = foreground/background colors (4 bits) (8 foreground/8 background colors)
* 0x10 - mosiac (bit 4)
* 0x20 - conceal (bit 5)
* 0x40 - seperated graphics (bit 6)
* 0x80 - flash (bit 7)
* 0x100 - double height (bit 8)
* 0x200 - hold (bit 9)
* 0x400 - new background (bits 10/11)
* 0x800 - black background (bits 10/11)
* 0xc00 - unused (bits 10/11)
* bits (12-15) unused
*
* @type {number}
*/
var MOSIAC = 0x10;
// Toggles
var CONCEAL = 0x20;
var REVEAL = 0x2000; // @temp Turns off Conceal
var SEPARATED = 0x40;
var BLOCKS = 0x4000; // @temp Turns off Separated
var STEADY = 0x8000; // @temp (turn off flash)
var DOUBLE = 0x100;
var NORMAL = 0x1000; // @temp Turns off Double Height
var HOLD = 0x200;
var RELEASE = 0x20000; // @temp turns off Hold
var NEWBACK = 0x400;
var BLACKBACK = 0x800;
/**
* This function converts ANSI text into an array of attributes
*
* @param contents - Our ANSI content to convert
* @param width - The width before wrapping to the next line
* @param yoffset - fields offset as discovered
* @param xoffset - fields offset as discovered
* @param debug - Enable debug mode
*/
function rawtoattrs(contents,width,yoffset,xoffset,debug) {
if (debug)
writeln('DEBUG active: '+debug);
lines = (''+contents).split(/\r\n/);
var i = 0;
var bg = BG_BLACK;
var fg = LIGHTGRAY;
var attr = fg + bg + i;
// Attribute state on a new line
var new_line = attr;
var y = 0;
var frame = {
content: [],
dynamic_fields: [],
input_fields: [],
};
// @todo temp hack, rework ansi variable - perhaps have a function that converts an attribute back into an ANSI sequence
var ansi = { i: 0, f: 37, b: 40 };
while (lines.length > 0) {
var x = 0;
var line = lines.shift();
if ((debug !== undefined) && (y > debug)) {
exit(1);
}
if (debug) {
log(LOG_DEBUG,'y:'+y);
log(LOG_DEBUG,'line:'+line);
write('y:'+y+', line:'+line);
}
while (line.length > 0) {
if (x >= width) {
x = 0;
// Each new line, we reset the attrs
attr = new_line;
y++;
}
//writeln('next ch:'+line[0].charCodeAt(0));
/* parse control codes */
var m = line.match(/^([\x00-\x1f])/);
if (m !== null) {
line = line.substr(m[0].length);
attr = 0;
match = m.shift().charCodeAt(0);
/*
writeln('- match:'+match);
writeln('- match 0x0f:'+(match & 0x0f));
if (match & 0x10) {
writeln(' - got mosiac');
attr += MOSIAC;
}
*/
//if (match < 0x0f) {
//switch(match & 0x07) {
switch(match) {
case 0x00:
attr += BLACK;
break;
case 0x01:
attr += RED;
break;
case 0x02:
attr += GREEN;
break;
case 0x03:
attr += YELLOW;
break;
case 0x04:
attr += BLUE;
break;
case 0x05:
attr += MAGENTA;
break;
case 0x06:
attr += CYAN;
break;
case 0x07:
attr += LIGHTGRAY;
break;
case 0x08:
attr = BLINK;
break;
case 0x09:
attr = STEADY;
break;
/*
case 0x0a:
//attr = ENDBOX; // End Box (Unused?)
break;
case 0x0b:
//attr = STARTBOX; // Start Box (Unused?)
break;
*/
case 0x0c:
//attr &= ~DOUBLE;
attr = NORMAL;
break;
case 0x0d:
attr = DOUBLE;
break;
case 0x0e:
attr = NORMAL; // @todo Double Width (Unused)?
break;
case 0x0f:
attr = NORMAL; // @todo Double Width (Unused?)
break;
case 0x10:
attr = MOSIAC|BLACK;
break;
case 0x11:
attr += MOSIAC|RED;
break;
case 0x12:
attr += MOSIAC|GREEN;
break;
case 0x13:
attr += MOSIAC|YELLOW;
break;
case 0x14:
attr += MOSIAC|BLUE;
break;
case 0x15:
attr += MOSIAC|MAGENTA;
break;
case 0x16:
attr += MOSIAC|CYAN;
break;
case 0x17:
attr += MOSIAC|LIGHTGRAY;
break;
case 0x18:
attr = CONCEAL;
break;
case 0x19:
attr = BLOCKS;
break;
case 0x1a:
attr = SEPARATED;
break;
/*
case 0x1b:
//attr = NORMAL; // CSI
break;
*/
case 0x1c:
attr = BLACKBACK; // Black Background
break;
case 0x1d:
attr = NEWBACK; // New Background
break;
case 0x1e:
attr = HOLD; // Mosiac Hold
break;
case 0x1f:
attr = RELEASE; // Mosiac Release
break;
// Catch all for other codes
default:
attr = 0xff00;
}
if (debug)
writeln(' - got control code:'+attr+'['+y+','+x+'] - length:'+attr.length);
store(x++,y,null,attr);
attr = undefined;
continue;
}
/* parse an input field */
// Input field 'FIELD;valueTYPE;input char'
// @todo remove the trailing ESC \ to end the field, just use a control code ^B \x02 (Start of Text) and ^C \x03
var m = line.match(/^\x1b_(([A-Z]+;[0-9a-z]+)([;]?.+)?)\x1b\\/);
if (m !== null) {
log(LOG_DEBUG,'Got input field: '+JSON.stringify(m));
log(LOG_DEBUG,'ansi:'+JSON.stringify(ansi));
// full string that matched
match = m.shift();
// thus, the rest of the line
line = line.substr(match.length);
//writeln('rest of line:'+JSON.stringify(line));
// We are interested in our field match
var sos = m.shift().split(';');
//writeln('sos:'+JSON.stringify(sos));
for (var num in sos) {
switch (num) {
// First value is the field name
case '0':
field = sos[num];
break;
// Second value is the length/type of the field, nnX nn=size in chars, T=type (lower case)
case '1':
var c = sos[num].match(/([0-9]+)([a-z])/);
if (! c) {
log(LOG_ERROR,'SOS FAILED PARSING FIELD LENGTH/TYPE. ['+r+'x'+c+'] '+sos[num]);
break;
}
//log(LOG_DEBUG,'SOS ['+r+'x'+c+'] NUM CHARS: '+x[1]+', TYPE: '+x[2]);
fieldlen = c[1];
fieldtype = c[2];
break;
// Third field is the char to to use
case '2':
fieldchar = sos[num];
break;
default:
log(LOG_ERROR,'IGNORING ADDITIONAL SOS FIELDS. ['+r+'x'+c+'] '+sos[num]);
}
}
// If we are padding our field with a char, we need to add that back to line
// @todo validate if this goes beyond our width (and if scrolling not enabled)
if (fieldlen)
line = fieldchar.repeat(fieldlen)+line;
frame.input_fields.push({
type: fieldtype,
length: Number(fieldlen),
char: fieldchar,
name: field,
attribute: JSON.parse(JSON.stringify(ansi)),
x: Number(x+(xoffset !== undefined ? xoffset : 0)),
y: Number(y+(yoffset !== undefined ? yoffset : 0)),
value: '',
});
log(LOG_DEBUG,'input_field:'+JSON.stringify(frame.input_fields.last));
}
/* parse dynamic value field */
// @todo remove the trailing ESC \ to end the field, just use a control code ie: ^E \x05 (Enquiry) or ^Z \x26 (Substitute)
var m = line.match(/^\x1bX(([a-zA-Z._:^;]+[0-9]?;-?[0-9^;]+)([;]?[^;]+)?)\x1b\\/);
if (m !== null) {
// full string that matched
match = m.shift();
// thus, the rest of the line
line = line.substr(match.length);
//writeln('rest of line:'+JSON.stringify(line));
// We are interested in our field match
var df = m.shift().split(';');
log(LOG_DEBUG,'- DF found at ['+x+'x'+y+'], Field: '+df[0]+', Length: '+df[1]+', Pad:'+df[2]);
// If we are padding our field with a char, we need to add that back to line
// @todo validate if this goes beyond our width (and if scrolling not enabled)
line = (df[2] ? df[2] : '_').repeat(Math.abs(df[1]))+line;
frame.dynamic_fields.push({
name: df[0],
length: df[1],
pad: df[2],
x: x+(xoffset !== undefined ? xoffset : 0),
y: y+(yoffset !== undefined ? yoffset : 0),
value: undefined,
});
}
/* set character and attribute */
var ch = line[0];
line = line.substr(1);
if (debug && (debug === y)) {
writeln('y:'+y+', x:'+x+', ch:'+ch);
}
/* validate position */
if (y < 0)
y = 0;
if (x < 0)
x = 0;
store(x,y,ch,undefined);
x++;
}
// Each new line, we reset the attrs
attr = undefined;
y++;
}
return frame;
function store(x,y,ch,attr) {
/* set character and attribute */
if (! frame.content[y+1])
frame.content[y+1]=[];
frame.content[y+1][x+1] = new Char(ch,attr,SESSION_EXT);
}
}
load('ansitex/load/session.js');
// Our frame object
function SessionProtocol() {
Session.apply(this,arguments);
this.settings.MSG_SENDORNOT = ascii(27)+'BKEY 1 TO SEND, 2 NOT TO SEND';
this.settings.MSG_LOGON = ascii(27)+'BKEY 1 TO LOGON, 2 TO RETURN';
this.settings.MSG_SENT = ascii(27)+'BMESSAGE SENT - KEY _ TO CONTINUE';
this.settings.MSG_NOTSENT = ascii(27)+'BMESSAGE NOT SENT - KEY _ TO CONTINUE';
this.settings.ERR_NO_PARENT = ascii(27)+'APARENT FRAME DOESNT EXIST';
this.settings.ERR_NOT_IMPLEMENTED = ascii(27)+'ANOT IMPLEMENTED YET?';
this.settings.ERR_ROUTE = ascii(27)+'GMISTAKE?'+ascii(27)+'BTRY AGAIN OR TELL US ON *08';
this.settings.ERR_METHOD_NOT_EXIST = ascii(27)+'GMISTAKE?'+ascii(27)+'BTRY AGAIN OR TELL US ON *08';
this.settings.ACCESS_DENIED = ascii(27)+'AACCESS DENIED.';
this.settings.ALREADY_MEMBER = ascii(27)+'AALREADY MEMBER OF CUG'
this.settings.INACTIVITY = ascii(27)+'AINACTIVITY ALERT, DISCONNECT PENDING...';
this.settings.INACTIVE = ascii(27)+'AINACTIVITY DISCONNECT';
this.settings.NOACTION = ascii(27)+'ANO ACTION PERFORMED';
this.settings.BASESTAR = ascii(27)+'B*';
this.settings.INVALID_CODE = ascii(27)+'AINVAID CODE, PLEASE TRY AGAIN **';
this.settings.TOKEN_EMAIL = ascii(27)+'ATOKEN EMAILED TO YOU...';
this.settings.TOKEN_SENT = ascii(27)+'ATOKEN SENT, PLEASE ENTER TOKEN';
this.settings.INVALID_EMAIL = ascii(27)+'AINVAID EMAIL, PLEASE TRY AGAIN *00';
this.settings.INVALID_UID = ascii(27)+'AINVAID USER ID, PLEASE TRY AGAIN *00';
this.settings.CANNOT_SEND_TOKEN = ascii(27)+'ACANNOT SEND VALIDATION CODE, PLEASE TRY AGAIN *00';
this.settings.USER_EXISTS = ascii(27)+'AERROR USER EXISTS, PLEASE TRY AGAIN *00';
this.settings.USER_CREATE_ERROR = ascii(27)+'AERROR CREATING USER, PLEASE TRY AGAIN *00';
this.settings.LOGIN_ERROR = ascii(27)+'AERROR LOGGING IN, PLEASE TRY AGAIN *00';
this.settings.CANCEL_MSG = ascii(27)+'BPRESS 2 TO CANCEL';
this.settings.SYS_ERROR = ascii(27)+'ASYS ERR, TRY AGAIN OR TELL US ON *08';
this.settings.LOADING = ascii(27)+'Cloading...';
this.settings.PROCESSING = ESC+VIEWDATA_YELLOW+'processing...';
var blp = 0; // Length of data on the bottom line
/**
* Set the attribute at the current position
*/
this.attr = function(field) {
//NOOP - the terminal takes care of this
}
this.baselineClear = function(reposition) {
msg = '';
log(LOG_DEBUG,'- Clear Bottom Line ['+blp+'] - reposition ['+reposition+']');
write_raw(VIEWDATA_HOME+VIEWDATA_UP+msg+
((blp > msg.length)
? (' '.repeat(blp-msg.length)+(reposition ? VIEWDATA_HOME+VIEWDATA_UP+VIEWDATA_RIGHT.repeat(msg.length) : ''))
: '')
);
blp = msg.length;
}
/**
* Send a message to the baseline.
*
* @param text
* @param reposition
*/
this.baselineSend = function(text,reposition) {
var msg = this.getMessage(text);
var x = this.strlen(msg);
log(LOG_DEBUG,'- Bottom Line ['+msg+'] ('+x+') - reposition ['+reposition+'] BLP:'+blp);
write_raw(VIEWDATA_HOME+VIEWDATA_UP+msg+
((blp > x)
? (' '.repeat(blp-x)+(reposition ? VIEWDATA_HOME+VIEWDATA_UP+VIEWDATA_RIGHT.repeat(x) : ''))
: '')
);
blp = x;
}
/**
* Turn off the cursor
*/
this.cursorOff = function() {
write_raw(VIEWDATA_COFF);
}
/**
* Turn on cursor
* @param x
* @param y
*/
this.cursorOn = function(x,y) {
write_raw(VIEWDATA_CON);
if (x && y)
this.gotoxy(x,y);
}
// Field backspace, that leaves the field filler char
this.fieldbs = function(char) {
log(LOG_DEBUG,'- Field backspace with char:'+char);
write_raw(VIEWDATA_LEFT+char+VIEWDATA_LEFT);
}
this.gotoxy = function(x,y) {
log(LOG_DEBUG,'- Moving cursor to y:'+y+', x:'+x);
// @todo This could be optimised to go the shortest route
write_raw(VIEWDATA_HOME);
if (x > 0)
write_raw(VIEWDATA_RIGHT.repeat(x));
if (y > 0)
write_raw(VIEWDATA_DOWN.repeat(y));
}
this.strlen = function(str) {
return str.replace(/\x1b/g,'').length;
};
this.qrcode = function(qr) {
// Render the body
var qrcode = VIEWDATA_HOME+VIEWDATA_DOWN.repeat(5);
var offset = this.settings.FRAME_WIDTH-Math.ceil(qr.size/2)-1;
for (var x = -1; x < qr.size; x=x+3) {
var line = VIEWDATA_RIGHT.repeat(offset ? offset-1 : 0)+ESC+VIEWDATA_MOSIAC_WHITE;
for (var y = -1; y < qr.size; y=y+2) {
var char = 0;
//TL
char |= ((x===-1) || (y===-1) || ! qr.getModule(x,y)) ? (1<<0) : (0<<0);
//TR
char |= ((x===-1) || (y === qr.size-1) || ! qr.getModule(x,y+1)) ? (1<<1) : (0<<1);
//ML
char |= ((y===-1) || ! qr.getModule(x+1,y)) ? (1<<2) : (0<<2);
//MR
char |= ((y === qr.size-1) || ! qr.getModule(x+1,y+1)) ? (1<<3) : (0<<3);
//BL
char |= ((x===qr.size-2) || (y===-1) || ! qr.getModule(x+2,y)) ? (1<<4) : (0<<4);
//BR
char |= ((x===qr.size-2) || (y === qr.size-1) || ! qr.getModule(x+2,y+1)) ? (1<<5) : (0<<5);
char += 0x20;
if (char > 0x3f)
char += 0x20;
line += ascii(char);
}
// Render the right column
if (y%2)
line += '\x35';
repeat_count = this.settings.FRAME_WIDTH-Math.ceil(qr.size/2)-offset-(offset ? 1 : 2)-(y%2 === 1 ? 0 : 1);
qrcode += line+' '.repeat(repeat_count > 0 ? repeat_count : 0);
// To fix some terminals where moving right from col 40 doesnt advance to col 1 on the next line
qrcode +=VIEWDATA_LEFT+VIEWDATA_CR+VIEWDATA_DOWN;
}
log(LOG_DEBUG,'WIDTH:'+this.settings.FRAME_WIDTH);
log(LOG_DEBUG,'QR :'+(Math.ceil(qr.size/2)+1));
log(LOG_DEBUG,'OFF :'+offset);
log(LOG_DEBUG,'Y :'+(y%2 ? 0 : 1));
log(LOG_DEBUG,'X :'+(x%3 ? 0 : 1));
// Render the bottom
if (x%3) {
line = VIEWDATA_RIGHT.repeat(offset ? offset-1 : 0)+ESC+VIEWDATA_MOSIAC_WHITE;
for (var y = 0; y < qr.size; y=y+2) {
line += '\x23';
}
// Render the right column
if (y%2 === 0) {
line += '\x21';
}
qrcode += line+' '.repeat(repeat_count > 0 ? repeat_count : 0);
}
write_raw(qrcode);
};
/*
this.save=function() {
file = system.mods_dir+'ansitex/text/'+this.page+'.tex';
w = new File(file);
if (! w.open('w')) {
log(LOG_ERROR,'! ERROR: Unable to create TEX file for '+this.page);
exit(1);
}
w.write(JSON.stringify(this));
w.close();
log(LOG_DEBUG,'Saved file: '+this.page+'.tex');
}
*/
}
function videotex(data) {
var output = '';
//$output .= ($byte < 32) ? ESC.chr($byte+64) : chr($byte);
for (var i = 0; i < data.length; i++) {
output += (data.charCodeAt(i) < 32) ? "\x1b"+String.fromCharCode(data.charCodeAt(i)+64) : String.fromCharCode(data.charCodeAt(i));
}
return output;
}
SessionProtocol.prototype = Session.prototype;
SessionProtocol.prototype.constructor = SessionProtocol;