UNPKG

node-ncurses

Version:

library for ncurses-style GUI rendering in node.js

1,360 lines (1,313 loc) 33.3 kB
var ansi = require('node-ansi'); var screen_rows = 24; var screen_columns = 80; function Frame(x,y,width,height,attr,parent) { /* private properties */ var properties = { x:undefined, y:undefined, width:undefined, height:undefined, attr:undefined, display:undefined, data:[], open:false, curr_attr:undefined, id:0 }; var settings = { v_scroll:true, h_scroll:false, scrollbars:false, lf_strict:true, checkbounds:true, transparent:false, word_wrap:false }; var relations = { parent:undefined, child:[] }; var position = { cursor:new Cursor(0,0,this), offset:new Offset(0,0,this), stored:new Cursor(0,0,this) }; /* protected properties */ this.__defineGetter__("child", function() { return relations.child; }); this.__defineSetter__("child", function(frame) { if(frame instanceof Frame) relations.child.push(frame); else throw("child not an instance of Frame()"); }); this.__defineGetter__("attr", function() { return properties.attr; }); this.__defineSetter__("attr", function(attr) { if(attr !== undefined && attr[0] !== '\033') throw("invalid attribute: " + attr); properties.attr = attr; }); this.__defineGetter__("x", function() { if(properties.x == undefined) return properties.display.x; return properties.x; }); this.__defineSetter__("x", function(x) { if(x == undefined) return; if(!checkX(x)) throw("invalid x coordinate: " + x); properties.x = Number(x); }); this.__defineGetter__("y", function() { if(properties.y == undefined) return properties.display.y; return properties.y; }); this.__defineSetter__("y", function(y) { if(y == undefined) return; if(!checkY(y)) throw("invalid y coordinate: " + y); properties.y = Number(y); }); this.__defineGetter__("width", function() { if(properties.width == undefined) return properties.display.width; return properties.width; }); this.__defineSetter__("width", function(width) { if(width == undefined) return; if(!checkWidth(this.x,Number(width))) throw("invalid width: " + width); properties.width = Number(width); }); this.__defineGetter__("height", function() { if(properties.height == undefined) return properties.display.height; return properties.height; }); this.__defineSetter__("height", function(height) { if(height == undefined) return; if(!checkHeight(this.y,Number(height))) throw("invalid height: " + height); properties.height = Number(height); }); /* read-only properties */ this.__defineGetter__("cursor",function() { return position.cursor; }); this.__defineGetter__("offset",function() { return position.offset; }); this.__defineGetter__("id", function() { return properties.id; }); this.__defineGetter__("parent", function() { return relations.parent; }); this.__defineGetter__("display", function() { return properties.display; }); this.__defineGetter__("data_height", function() { return properties.data.length; }); this.__defineGetter__("data_width", function() { return properties.data[0].length; }); this.__defineGetter__("data", function() { return properties.data; }); /* protected settings */ this.__defineGetter__("checkbounds", function() { return settings.checkbounds; }); this.__defineSetter__("checkbounds", function(bool) { if(typeof bool == "boolean") settings.checkbounds=bool; else throw("non-boolean checkbounds: " + bool); }); this.__defineGetter__("transparent", function() { return settings.transparent; }); this.__defineSetter__("transparent", function(bool) { if(typeof bool == "boolean") settings.transparent=bool; else throw("non-boolean transparent: " + bool); }); this.__defineGetter__("lf_strict", function() { return settings.lf_strict; }); this.__defineSetter__("lf_strict", function(bool) { if(typeof bool == "boolean") settings.lf_strict=bool; else throw("non-boolean lf_strict: " + bool); }); this.__defineGetter__("scrollbars", function() { return settings.scrollbars; }); this.__defineSetter__("scrollbars", function(bool) { if(typeof bool == "boolean") settings.scrollbars=bool; else throw("non-boolean scrollbars: " + bool); }); this.__defineGetter__("v_scroll", function() { return settings.v_scroll; }); this.__defineSetter__("v_scroll", function(bool) { if(typeof bool == "boolean") settings.v_scroll=bool; else throw("non-boolean v_scroll: " + bool); }); this.__defineGetter__("word_wrap", function() { return settings.word_wrap; }); this.__defineSetter__("word_wrap", function(bool) { if(typeof bool == "boolean") settings.word_wrap=bool; else throw("non-boolean word_wrap: " + bool); }); this.__defineGetter__("h_scroll", function() { return settings.h_scroll; }); this.__defineSetter__("h_scroll", function(bool) { if(typeof bool == "boolean") settings.h_scroll=bool; else throw("non-boolean h_scroll: " + bool); }); this.__defineGetter__("is_open",function() { return properties.open; }); /* public methods */ this.getData = function(x,y,use_offset) { var px = x; var py = y; if(use_offset) { px += position.offset.x; py += position.offset.y; } // if(!properties.data[py] || !properties.data[py][px]) // throw("Frame.getData() - invalid coordinates: " + px + "," + py); if(!properties.data[py] || !properties.data[py][px]) return new Char(); return properties.data[py][px]; } this.setData = function(x,y,ch,attr,use_offset) { var px = x; var py = y; if(use_offset) { px += position.offset.x; py += position.offset.y; } //I don't remember why I did this, but it was probably important at the time //if(!properties.data[py] || !properties.data[py][px]) // throw("Frame.setData() - invalid coordinates: " + px + "," + py); if(!properties.data[py]) properties.data[py] = []; if(!properties.data[py][px]) properties.data[py][px] = {}; if(properties.data[py][px].ch == ch && properties.data[py][px].attr == attr) return; if(ch) properties.data[py][px].ch = ch; if(attr) properties.data[py][px].attr = attr; if(properties.open) properties.display.updateChar(this,x,y); } this.clearData = function(x,y,use_offset) { var px = x; var py = y; if(use_offset) { px += position.offset.x; py += position.offset.y; } if(!properties.data[py] || !properties.data[py][px]) return; else if(properties.data[py][px].ch == undefined && properties.data[py][px].attr == undefined) return; properties.data[py][px].ch = undefined; properties.data[py][px].attr = undefined; if(properties.open) properties.display.updateChar(this,x,y); } this.bottom = function() { if(properties.open) { for(var c in relations.child) relations.child[c].bottom(); properties.display.bottom(this); } } this.top = function() { if(properties.open) { properties.display.top(this); for(var c in relations.child) relations.child[c].top(); } } this.open = function() { if(!properties.open) { properties.display.open(this); properties.open = true; } for(var c in relations.child) { relations.child[c].open(); } } this.refresh = function() { if(properties.open) { properties.display.updateFrame(this); for(var c in relations.child) relations.child[c].refresh(); } } this.close = function() { for(var c in relations.child) relations.child[c].close(); if(properties.open) { properties.display.close(this); properties.open = false; } } this.delete = function(id) { if(id == undefined) { this.close(); if(relations.parent) { relations.parent.delete(this.id); } } else { for(var c=0;c<relations.child.length;c++) { if(relations.child[c].id == id) { relations.child.splice(c--,1); break; } } } } this.move = function(x,y) { var nx = undefined; var ny = undefined; if(checkX(this.x+x) && checkWidth(this.x+x,this.width)) nx = this.x+x; if(checkY(this.y+y) && checkHeight(this.y+y,this.height)) ny = this.y+y; if(nx == undefined && ny == undefined) return; properties.display.updateFrame(this); if(nx !== undefined) this.x=nx; if(ny !== undefined) this.y=ny; properties.display.updateFrame(this); for (var c in relations.child) relations.child[c].move(x,y); } this.moveTo = function(x,y) { for(var c in relations.child) { var cx = (x + (relations.child[c].x-this.x)); var cy = (y + (relations.child[c].y-this.y)); relations.child[c].moveTo(cx,cy); } var nx = undefined; var ny = undefined; if(checkX(x)) nx = x; if(checkY(y)) ny = y; if(nx == undefined && ny == undefined) return; properties.display.updateFrame(this); if(nx !== undefined) this.x=nx; if(ny !== undefined) this.y=ny; properties.display.updateFrame(this); } this.draw = function() { if(properties.open) this.refresh(); else this.open(); this.cycle(); } this.load = function(filename,width,height) { var f=new File(filename); switch(file_getext(filename).substr(1).toUpperCase()) { case "ANS": if(!(f.open("r",true,4096))) return(false); var lines=f.readAll(4096); f.close(); var attr = this.attr; var bg = ansi.bg.black; var fg = ansi.fg.lightgray; var i = 0; var y = 0; var saved = {}; while(lines.length > 0) { var x = 0; var line = lines.shift(); while(line.length > 0) { /* parse an attribute sequence*/ var m = line.match(/^\x1b\[(\d*);?(\d*);?(\d*)m/); if(m !== null) { line = line.substr(m.shift().length); if(m[0] == 0) { bg = ansi.bg.black; fg = ansi.fg.lightgray; i = 0; m.shift(); } if(m[0] == 1) { /* TODO: i = HIGH; */ m.shift(); } if(m[0] >= 40) { switch(Number(m.shift())) { case 40: bg = ansi.bg.black; break; case 41: bg = ansi.bg.red; break; case 42: bg = ansi.bg.green; break; case 43: bg = ansi.bg.brown; break; case 44: bg = ansi.bg.blue; break; case 45: bg = ansi.bg.magenta; break; case 46: bg = ansi.bg.cyan; break; case 47: bg = ansi.bg.lightgray; break; } } if(m[0] >= 30) { switch(Number(m.shift())) { case 30: fg = ansi.fg.black; break; case 31: fg = ansi.fg.red; break; case 32: fg = ansi.fg.green; break; case 33: fg = ansi.fg.brown; break; case 34: fg = ansi.fg.blue; break; case 35: fg = ansi.fg.magenta; break; case 36: fg = ansi.fg.cyan; break; case 37: fg = ansi.fg.lightgray; break; } } attr = bg + fg + i; continue; } /* parse absolute character position */ var m = line.match(/^\x1b\[(\d*);?(\d*)[Hf]/); if(m !== null) { line = line.substr(m.shift().length); if(m.length==0) { x=0; y=0; } else { if(m[0]) y = Number(m.shift())-1; if(m[0]) x = Number(m.shift())-1; } continue; } /* ignore a bullshit sequence */ var n = line.match(/^\x1b\[\?7h/); if(n !== null) { line = line.substr(n.shift().length); continue; } /* parse an up positional sequence */ var n = line.match(/^\x1b\[(\d*)A/); if(n !== null) { line = line.substr(n.shift().length); var chars = n.shift(); if(chars < 1) y-=1; else y-=Number(chars); continue; } /* parse a down positional sequence */ var n = line.match(/^\x1b\[(\d*)B/); if(n !== null) { line = line.substr(n.shift().length); var chars = n.shift(); if(chars < 1) y+=1; else y+=Number(chars); continue; } /* parse a forward positional sequence */ var n = line.match(/^\x1b\[(\d*)C/); if(n !== null) { line = line.substr(n.shift().length); var chars = n.shift(); if(chars < 1) x+=1; else x+=Number(chars); continue; } /* parse a backward positional sequence */ var n = line.match(/^\x1b\[(\d*)D/); if(n !== null) { line = line.substr(n.shift().length); var chars = n.shift() if(chars < 1) x-=1; else x-=Number(chars); continue; } /* parse a clear screen sequence */ var n = line.match(/^\x1b\[2J/); if(n !== null) { line = line.substr(n.shift().length); continue; } /* parse save cursor sequence */ var n = line.match(/^\x1b\[s/); if(n !== null) { line = line.substr(n.shift().length); saved.x = x; saved.y = y; continue; } /* parse restore cursor sequence */ var n = line.match(/^\x1b\[u/); if(n !== null) { line = line.substr(n.shift().length); x = saved.x; y = saved.y; continue; } /* set character and attribute */ var ch = line[0]; line = line.substr(1); /* validate position */ if(y<0) y=0; if(x<0) x=0; if(x>=this.width) { x=0; y++; } /* set character and attribute */ if(!properties.data[y]) properties.data[y]=[]; properties.data[y][x]=new Char(ch,attr); x++; } y++; } break; case "BIN": if(width == undefined || height == undefined) throw("unknown graphic dimensions"); if(!(f.open("rb",true,4096))) return(false); for(var y=0; y<height; y++) { for(var x=0; x<width; x++) { var c = new Char(); if(f.eof) return(false); c.ch = f.read(1); if(f.eof) return(false); c.attr = f.readBin(1); c.id = this.id; if(!properties.data[y]) properties.data[y]=[]; properties.data[y][x] = c; } } f.close(); break; case "TXT": if(!(f.open("r",true,4096))) return(false); var lines=f.readAll(4096); f.close(); while(lines.length > 0) this.putmsg(lines.shift() + "\r\n"); break; default: throw("unsupported filetype"); break; } } this.scroll = function(x,y) { var update = false; /* default: add a new line to the data matrix */ if(x == undefined && y == undefined) { if(settings.v_scroll) { var newrow = []; for(var x = 0;x<this.width;x++) { for(var y = 0;y<this.height;y++) properties.display.updateChar(this,x,y); newrow.push(new Char()); } properties.data.push(newrow); position.offset.y++; update = true; } } /* otherwise, adjust the x/y offset */ else { if(typeof x == "number" && x !== 0 && settings.h_scroll) { position.offset.x += x; update = true; } if(typeof y == "number" && y !== 0 && settings.v_scroll) { position.offset.y += y; update = true; } if(update) this.refresh(); } return update; } this.scrollTo = function(x,y) { var update = false; if(typeof x == "number") { if(settings.h_scroll) { position.offset.x = x; update = true; } } if(typeof y == "number") { if(settings.v_scroll) { position.offset.y = y; update = true; } } if(update) this.refresh(); } this.insertLine = function(y) { if(properties.data[y]) properties.data.splice(y,0,[]); else properties.data[y] = []; this.refresh(); return properties.data[y]; } this.deleteLine = function(y) { var l = undefined; if(properties.data[y]) { l = properties.data.splice(y,1); this.refresh(); } return l; } this.screenShot = function(file,append) { return properties.display.screenShot(file,append); } this.invalidate = function() { properties.display.invalidate(); this.refresh(); } /* ansi method emulation */ this.home = function() { position.cursor.x = 0; position.cursor.y = 0; return true; } this.end = function() { position.cursor.x = this.width-1; position.cursor.y = this.height-1; return true; } this.pagedown = function() { position.offset.y += this.height-1; if(position.offset.y >= this.data_height) position.offset.y = this.data_height - this.height; this.refresh(); } this.pageup = function() { position.offset.y -= this.height-1; if(position.offset.y < 0) position.offset.y = 0; this.refresh(); } this.clear = function(attr) { if(attr == undefined) attr = this.attr; for(var y=0;y<properties.data.length;y++) { if(!properties.data[y]) continue; for(var x=0;x<properties.data[y].length;x++) { if(properties.data[y][x]) { properties.data[y][x].ch = undefined; properties.data[y][x].attr = attr; } } } for(var y=0;y<this.height;y++) { for(var x=0;x<this.width;x++) { properties.display.updateChar(this,x,y); } } this.home(); return true; } this.clearline = function(attr) { if(attr == undefined) attr = this.attr; if(!properties.data[position.cursor.y]) return false; for(var x=0;x<properties.data[position.cursor.y].length;x++) { if(properties.data[position.cursor.y][x]) { properties.data[position.cursor.y][x].ch = undefined; properties.data[position.cursor.y][x].attr = attr; } } for(var x=0;x<this.width;x++) { properties.display.updateChar(this,x,position.cursor.y); } return true; } this.cleartoeol = function(attr) { if(attr == undefined) attr = this.attr; if(!properties.data[position.cursor.y]) return false; for(var x=position.cursor.x;x<properties.data[position.cursor.y].length;x++) { if(properties.data[position.cursor.y][x]) { properties.data[position.cursor.y][x].ch = undefined; properties.data[position.cursor.y][x].attr = attr; } } for(var x=position.cursor.x;x<this.width;x++) { properties.display.updateChar(this,x,position.cursor.y); } return true; } this.crlf = function() { position.cursor.x = 0; if(position.cursor.y < this.height-1) position.cursor.y += 1; else {} } this.write = function(str,attr) { if(str == undefined) return; if(settings.word_wrap) str = word_wrap(str,this.width); str = str.toString().split(''); if(attr) properties.curr_attr = attr; else properties.curr_attr = this.attr; var pos = position.cursor; while(str.length > 0) { var ch = str.shift(); putChar.call(this,ch,properties.curr_attr); pos.x++; } } this.putmsg = function(str,attr) { if(str == undefined) return; // if(settings.word_wrap) // str = word_wrap(str,this.width); str = str.toString().split(''); if(attr) properties.curr_attr = attr; else properties.curr_attr = this.attr; var pos = position.cursor; while(str.length > 0) { /* handle ansi */ var ch = str.shift(); switch(ch) { case '\033': var cReg = /^\[\d+(;\d\d?)?m/; var aReg = /^\[(s|u|\d+[ABCDJKLMPSTXZn])/; var s = str.join(''); var match = s.match(cReg); if(match) { properties.curr_attr += ch + match[0]; } else { match = s.match(aReg); } if(match) { str = str.slice(match[0].length); } break; case '\7': /* Beep */ break; case '\x7f': /* DELETE */ break; case '\b': if(pos.x > 0) { pos.x--; putChar.call(this," ",properties.curr_attr); } break; case '\r': pos.x=0; break; case '\t': pos.x += 4; break; case '\n': pos.y++; if(settings.lf_strict && pos.y >= this.height) { this.scroll(); pos.y--; } break; default: putChar.call(this,ch,properties.curr_attr); pos.x++; break; } } } this.center = function(str,attr) { position.cursor.x = Math.ceil(this.width/2) - Math.ceil(ansi.strlen(str)/2); if(position.cursor.x < 0) position.cursor.x = 0; this.putmsg(str,attr); } this.gotoxy = function(x,y) { if(typeof x == "object" && x.x && x.y) { if(validateCursor.call(this,x.x-1,x.y-1)) { position.cursor.x = x.x-1; position.cursor.y = x.y-1; return true; } return false; } if(validateCursor.call(this,x-1,y-1)) { position.cursor.x = x-1; position.cursor.y = y-1; return true; } return false; } this.getxy = function() { return { x:position.cursor.x+1, y:position.cursor.y+1 }; } this.pushxy = function() { position.stored.x = position.cursor.x; position.stored.y = position.cursor.y; } this.popxy = function() { position.cursor.x = position.stored.x; position.cursor.y = position.stored.y; } this.up = function(n) { if(position.cursor.y == 0 && position.offset.y == 0) return false; if(isNaN(n)) n = 1; while(n > 0) { if(position.cursor.y > 0) { position.cursor.y--; n--; } else break; } if(n > 0) this.scroll(0,-(n)); return true; } this.down = function(n) { if(position.cursor.y == this.height-1 && position.offset.y == this.data_height - this.height) return false; if(isNaN(n)) n = 1; while(n > 0) { if(position.cursor.y < this.height - 1) { position.cursor.y++; n--; } else break; } if(n > 0) this.scroll(0,n); return true; } this.left = function(n) { if(position.cursor.x == 0 && position.offset.x == 0) return false; if(isNaN(n)) n = 1; while(n > 0) { if(position.cursor.x > 0) { position.cursor.x--; n--; } else break; } if(n > 0) this.scroll(-(n),0); return true; } this.right = function(n) { if(position.cursor.x == this.width-1 && position.offset.x == this.data_width - this.width) return false; if(isNaN(n)) n = 1; while(n > 0) { if(position.cursor.x < this.width - 1) { position.cursor.x++; n--; } else break; } if(n > 0) this.scroll(n,0); return true; } /* private functions */ function checkX(x) { if( isNaN(x) || (settings.checkbounds && (x > properties.display.x + properties.display.width || x < properties.display.x))) return false; return true; } function checkY(y) { if( isNaN(y) || (settings.checkbounds && (y > properties.display.y + properties.display.height || y < properties.display.y))) return false; return true; } function checkWidth(x,width) { if( width < 1 || isNaN(width) || (settings.checkbounds && x + width > properties.display.x + properties.display.width)) return false; return true; } function checkHeight(y,height) { if( height < 1 || isNaN(height) || (settings.checkbounds && y + height > properties.display.y + properties.display.height)) return false; return true; } function validateCursor(x,y) { if(x >= this.width) return false; if(y >= this.height) return false; return true; } function putChar(ch,attr) { if(position.cursor.x >= this.width) { position.cursor.x=0; position.cursor.y++; } if(position.cursor.y >= this.height) { this.scroll(); position.cursor.y--; } this.setData(position.cursor.x,position.cursor.y,ch,attr,true); } function init(x,y,width,height,attr,parent) { if(parent instanceof Frame) { properties.id = parent.display.nextID; properties.display = parent.display; settings.checkbounds = parent.checkbounds; relations.parent = parent; parent.child = this; } else { properties.display = new Display(x,y,width,height); } this.x = x; this.y = y; this.width = width; this.height = height; this.attr = attr; } init.apply(this,arguments); } /* frame reference object */ function Canvas(frame,display) { this.frame = frame; this.display = display; this.__defineGetter__("xoff",function() { return this.frame.x - this.display.x; }); this.__defineGetter__("yoff",function() { return this.frame.y - this.display.y; }); this.hasData = function(x,y) { var xpos = x-this.xoff; var ypos = y-this.yoff; if(xpos < 0 || ypos < 0) return undefined; if(xpos >= this.frame.width || ypos >= this.frame.height) return undefined; if(this.frame.transparent && this.frame.getData(xpos,ypos).ch == undefined) return undefined; return true; } } /* object representing screen positional and dimensional limits and canvas stack */ function Display(x,y,width,height) { /* private properties */ var properties = { x:undefined, y:undefined, width:undefined, height:undefined, canvas:{}, update:{}, buffer:{}, count:0 } /* protected properties */ this.__defineGetter__("x", function() { return properties.x; }); this.__defineSetter__("x", function(x) { if(x == undefined) properties.x = 1; else if(isNaN(x)) throw("invalid x coordinate: " + x); else properties.x = Number(x); }); this.__defineGetter__("y", function() { return properties.y; }); this.__defineSetter__("y", function(y) { if(y == undefined) properties.y = 1; else if(isNaN(y) || y < 1 || y > screen_rows) throw("invalid y coordinate: " + y); else properties.y = Number(y); }); this.__defineGetter__("width", function() { return properties.width; }); this.__defineSetter__("width", function(width) { if(width == undefined) properties.width = screen_columns; else if(isNaN(width) || (this.x + Number(width) - 1) > (screen_columns)) throw("invalid width: " + width); else properties.width = Number(width); }); this.__defineGetter__("height", function() { return properties.height; }); this.__defineSetter__("height", function(height) { if(height == undefined) properties.height = screen_rows; else if(isNaN(height) || (this.y + Number(height) - 1) > (screen_rows)) throw("invalid height: " + height); else properties.height = Number(height); }); this.__defineGetter__("nextID",function() { return ++properties.count; }); /* public methods */ this.cycle = function() { var updates = getUpdateList(); if(updates.length > 0) { var lasty = undefined; var lastx = undefined; var lastf = undefined; for(var update in updates) { var u = updates[update]; if(lasty !== u.y || lastx == undefined || (u.x - lastx) !== 1) ansi.gotoxy(u.px,u.py); if(lastf !== u.id) ansi.attributes = undefined; drawChar(u.ch,u.attr,u.px,u.py); lastx = u.x; lasty = u.y; lastf = u.id; } ansi.attributes=undefined; return true; } return false; } this.draw = function() { for(var y = 0;y<this.height;y++) { for(var x = 0;x<this.width;x++) { updateChar(x,y); } } this.cycle(); } this.open = function(frame) { var canvas = new Canvas(frame,this); properties.canvas[frame.id] = canvas; this.updateFrame(frame); } this.close = function(frame) { this.updateFrame(frame); delete properties.canvas[frame.id]; } this.top = function(frame) { var canvas = properties.canvas[frame.id]; delete properties.canvas[frame.id]; properties.canvas[frame.id] = canvas; this.updateFrame(frame); } this.bottom = function(frame) { for(var c in properties.canvas) { if(c == frame.id) continue; var canvas = properties.canvas[c]; delete properties.canvas[c]; properties.canvas[c] = canvas; } this.updateFrame(frame); } this.updateFrame = function(frame) { var xoff = frame.x - this.x; var yoff = frame.y - this.y; for(var y = 0;y<frame.height;y++) { for(var x = 0;x<frame.width;x++) { updateChar(xoff + x,yoff + y/*,data*/); } } } this.updateChar = function(frame,x,y) { var xoff = frame.x - this.x; var yoff = frame.y - this.y; updateChar(xoff + x,yoff + y); } this.screenShot = function(file,append) { var f = new File(file); if(append) f.open('ab',true,4096); else f.open('wb',true,4096) ; if(!f.is_open) return false; for(var y = 0;y<this.height;y++) { for(var x = 0;x<this.width;x++) { var c = getTopCanvas(x,y); var d = getData(c,x,y); if(d.ch) f.write(d.ch); else f.write(" "); if(d.attr) f.writeBin(d.attr,1); else f.writeBin(0,1); } } f.close(); return true; } this.invalidate = function() { properties.buffer = {}; } /* private functions */ function updateChar(x,y/*,data*/) { //ToDo: maybe store char update so I dont have to retrieve it later? if(!properties.update[y]) properties.update[y] = {}; properties.update[y][x] = 1; /*data; */ } function getUpdateList() { var list = []; for(var y in properties.update) { for(var x in properties.update[y]) { var c = getTopCanvas(x,y); var d = getData(c,x,y); if(d.px < 1 || d.py < 1 || d.px > screen_columns || d.py > screen_rows) continue; if(!properties.buffer[x]) { properties.buffer[x] = {}; properties.buffer[x][y] = d; list.push(d); } else if(properties.buffer[x][y] == undefined || properties.buffer[x][y].ch != d.ch || properties.buffer[x][y].attr != d.attr) { properties.buffer[x][y] = d; list.push(d); } } } properties.update={}; // if(list.length > 0) // console.logStamp("end getUpdateList"); return list.sort(updateSort); } function getData(c,x,y) { var cd = { x:Number(x), y:Number(y), px:Number(x) + properties.x, py:Number(y) + properties.y }; if(c) { var d = c.frame.getData(x-c.xoff,y-c.yoff,true); cd.id = c.frame.id; cd.ch = d.ch; if(d.attr) cd.attr = d.attr; else cd.attr = c.frame.attr; } return cd; } function updateSort(a,b) { if(a.y == b.y) return a.x-b.x; return a.y-b.y; } function drawChar(ch,attr,xpos,ypos) { if(attr) ansi.attributes = attr; if(xpos == screen_columns && ypos == screen_rows) ansi.cleartoeol(); else if(ch == undefined) { ansi.write(" "); } else { ansi.write(ch); } } function getTopCanvas(x,y) { var top = undefined; for (var prop in properties.canvas) { var c = properties.canvas[prop]; if(c.hasData(x,y)) top = c; } return top; } /* initialize display properties */ this.x = x; this.y = y; this.width = width; this.height = height; } /* character/attribute pair representing a screen position and its contents */ function Char(ch,attr) { this.ch = ch; this.attr = attr; } /* self-validating cursor position object */ function Cursor(x,y,frame) { var properties = { x:undefined, y:undefined, frame:undefined } this.__defineGetter__("x", function() { return properties.x; }); this.__defineSetter__("x", function(x) { if(isNaN(x)) throw("invalid x coordinate: " + x); properties.x = x; }); this.__defineGetter__("y", function() { return properties.y; }); this.__defineSetter__("y", function(y) { if(isNaN(y)) throw("invalid y coordinate: " + y); properties.y = y; }); if(frame instanceof Frame) properties.frame = frame; else throw("the frame is not a frame"); this.x = x; this.y = y; } /* self-validating scroll offset object */ function Offset(x,y,frame) { var properties = { x:undefined, y:undefined, frame:undefined } this.__defineGetter__("x", function() { return properties.x; }); this.__defineSetter__("x", function(x) { if(x == undefined) throw("invalid x offset: " + x); else if(x < 0) x = 0; else if(x > properties.frame.data_width - properties.frame.width) x = properties.frame.data_width - properties.frame.width; properties.x = x; }); this.__defineGetter__("y", function() { return properties.y; }); this.__defineSetter__("y", function(y) { if(y == undefined) throw("invalid y offset: " + y); else if(y < 0) y = 0; else if(y > properties.frame.data_height - properties.frame.height) y = properties.frame.data_height - properties.frame.height; properties.y = y; }); if(frame instanceof Frame) properties.frame = frame; else throw("the frame is not a frame"); this.x = x; this.y = y; } /* exports */ module.exports = Frame;