From 0fe4544107a0b46ca6b1db394d2858c198f6f77f Mon Sep 17 00:00:00 2001 From: Lisa Milne Date: Tue, 28 Nov 2023 22:33:20 +1000 Subject: [PATCH] libcurses (ncurses) starter, and switch less to using it --- clite/commands.js | 39 +++++--- clite/core.css | 2 +- clite/core.js | 221 +++++++++++++++++++++++++++------------------ clite/libclite.js | 12 +++ clite/libcurses.js | 108 ++++++++++++++++++++++ clite/libterm.js | 8 -- clite/shell.js | 4 + 7 files changed, 282 insertions(+), 112 deletions(-) create mode 100644 clite/libcurses.js diff --git a/clite/commands.js b/clite/commands.js index 9434b2a..1041acc 100644 --- a/clite/commands.js +++ b/clite/commands.js @@ -322,6 +322,7 @@ clite.commands.load('less',function(args,env,io) { var stdlib = io.include('stdlib'); var stdio = io.include('stdio'); var term = io.include('term'); + var curses = io.include('curses'); var clite = io.include('clite'); var file = null; @@ -354,18 +355,20 @@ Options: } } + var cols; + var rows; var lines = []; var topLine = 0; var bottomLine = 0; function showAt(line) { var start = line; - var end = line+tty.lines; + var end = line+rows; if (end > lines.length) end = lines.length; - tty.clear(); + curses.clear(); for (var i=start; i tty.chars) { - var sects = line.match(new RegExp('(.{1,'+tty.chars+'})','g')); + if (line.length > cols) { + var sects = line.match(new RegExp('(.{1,'+cols+'})','g')); lines.push.apply(lines,sects); return; } lines.push(line); }); - bottomLine = (lines.length-tty.lines); + bottomLine = (lines.length-rows); showAt(0); } @@ -415,15 +418,19 @@ Options: } } - var tty = term.opentty(); - tty.setRaw(false); + curses.initscr(); + cols = curses.getmaxx(); + rows = curses.getmaxy(); + curses.cbreak(); + curses.printw('Loading...'); - tty.handleKeys(function(e) { - switch (e.key) { + function input(key) { + switch (key) { case 'Escape': case 'q': - term.closetty(tty); + curses.endwin(); io.exit(0); + return; break; case 'Up': case 'ArrowUp': @@ -441,7 +448,7 @@ Options: break; case 'PageUp': if (topLine > 0) { - topLine -= tty.lines-1; + topLine -= rows-1; if (topLine < 0) topLine = 0; showAt(topLine); @@ -449,7 +456,7 @@ Options: break; case 'PageDown': if (topLine < bottomLine) { - topLine += tty.lines-1; + topLine += rows-1; if (topLine >= bottomLine) topLine = bottomLine; showAt(topLine); @@ -464,8 +471,10 @@ Options: showAt(topLine); break; } - }); - tty.draw('Loading...'); + curses.getch(input); + } + + curses.getch(input); return null; }); diff --git a/clite/core.css b/clite/core.css index 3c0a253..111e8cf 100644 --- a/clite/core.css +++ b/clite/core.css @@ -7,7 +7,7 @@ header h1 {line-height:40px; font-size:30px;} section {} section.content {position:relative; overflow:hidden;} section.content div#terminal {width:100%; min-width: 500px; max-width:1000px; max-height:100%; font-size:14px; line-height:20px; margin:0 auto; overflow:hidden; overflow-y:auto; scrollbar-width:none;} -section.content div#tty {width:100%; min-width: 500px; max-width:1000px; margin:0 auto; overflow:hidden; overflow-y:auto; scrollbar-width:none; z-index:99; background:#000000;} +section.content div#termalt {width:100%; min-width: 500px; max-width:1000px; margin:0 auto; overflow:hidden; overflow-y:auto; scrollbar-width:none; z-index:99; background:#000000;} section.content article {unicode-bidi: embed; white-space: pre-wrap;} section.content form {display:flex;} section.content form label, section.content form input, section.content form input:focus {display:block; border:none; margin:0; padding:0; font-family:monospace; font-size:14px; line-height:20px; background-color:#000000; color:#FFFFFF; outline:none;} diff --git a/clite/core.js b/clite/core.js index 24cb3f1..8c1ae12 100644 --- a/clite/core.js +++ b/clite/core.js @@ -433,6 +433,11 @@ readme.txt:/usr/clite/readme:0:0:-rw-r--r--`; } function init8(data) { + clite.core.execSafe(clite.libs.data); + clite.core.load.script('clite/libcurses.js',init9); + } + + function init9(data) { clite.core.execSafe(clite.libs.data); clite.libs.data = null; clite.libs.load = null; @@ -466,10 +471,10 @@ readme.txt:/usr/clite/readme:0:0:-rw-r--r--`; exit:null, // filled in by fork() include:null, // filled in by fork() }; - var pid = clite.lib.fork(env,io,init9); + var pid = clite.lib.fork(env,io,init10); } } - function init9(env,io) { + function init10(env,io) { clite.proc.setLogin(io.pid,1); clite.lib.exec('/bin/sh',['sh'],env,io); } @@ -1789,12 +1794,19 @@ clite.log = { clite.term = { data:{ type:'text', - show:true, + show:false, + isalt:false, form:null, field:null, handler:null, prompt:'', - buff:'' + buff:'', + alt:{ + israwOut:false, + israwIn:false, + lines:0, + cols:0 + } }, events:{ keydown:function(e) { @@ -1805,8 +1817,13 @@ clite.term = { return true; }, keyup:function(e) { - if (clite.term.tty.data != null) - return clite.term.tty.data.data.onkeypress(e); + if (clite.term.data.isalt && clite.term.data.alt.israwIn) { + if (clite.term.data.handler != null) + clite.term.data.handler(e.key); + e.target.value = ''; + clite.term.data.buff = ''; + return false; + } if (e.key.length > 1 && e.key != 'Spacebar' && clite.term.data.handler != null) { if (e.key == 'Enter') { clite.term.data.buff = ''; @@ -1829,23 +1846,76 @@ clite.term = { }); } }, - tty:{ - data:null - }, clear:function() { + if (clite.term.data.isalt) { + document.getElementById('termalt').innerHTML = ''; + return; + } document.getElementById('terminal').innerHTML = ''; }, writeLine:function(txt) { + if (clite.term.data.isalt) { + var t = document.getElementById('termalt'); + if (!t) + t = clite.term.genAlt(); + if (clite.term.data.alt.israwOut) { + t.innerHTML += txt; + if (clite.term.data.show) + clite.term.genForm(); + return; + } + var a = document.createElement('article'); + a.innerHTML = clite.lib.htmlEncode(txt); + t.appendChild(a); + if (clite.term.data.show) + clite.term.genForm(); + return; + } var a = document.createElement('article'); a.innerHTML = clite.lib.htmlEncode(txt); var t = document.getElementById('terminal'); if (!t) return; t.appendChild(a); - if (document.getElementById('form')) + if (clite.term.data.show) clite.term.genForm(); t.scrollTop = t.scrollHeight; }, + genAlt:function() { + var t = document.getElementById('termalt'); + if (!t) { + t = document.createElement('div'); + t.id = 'termalt'; + } + + var term = document.getElementById('terminal'); + var p = term.parentNode; + + t.style.width = term.offsetWidth+'px'; + if (p.offsetHeight > term.offsetHeight) { + t.style.minHeight = p.offsetHeight+'px'; + }else{ + t.style.minHeight = term.offsetHeight+'px'; + } + t.style.position = 'absolute'; + t.style.top = term.offsetTop+'px'; + t.style.left = term.offsetLeft+'px'; + p.appendChild(t); + + // some calculations to help with putting content in + var s = document.createElement('span'); + s.innerText = 'W'; + t.appendChild(s); + // 20 should not be hardcoded + clite.term.data.alt.lines = parseInt((t.offsetHeight/20)-1); + // TODO: shouldn't need the 0.8 thing, but urgh + clite.term.data.alt.cols = parseInt((t.offsetWidth/s.offsetWidth)*0.8); + t.removeChild(s); + + clite.term.data.buff = ''; + + return t; + }, genForm:function() { var f = document.getElementById('form'); if (!clite.term.data.show) { @@ -1857,19 +1927,28 @@ clite.term = { f = document.createElement('form'); f.id = 'form'; f.innerHTML = ''; - var l = document.createElement('label'); - l.innerHTML = clite.lib.htmlEncode(clite.term.data.prompt); - f.appendChild(l); + if (!clite.term.isalt) { + var l = document.createElement('label'); + l.innerHTML = clite.lib.htmlEncode(clite.term.data.prompt); + f.appendChild(l); + } var i = document.createElement('input'); i.type = clite.term.getType(); i.value = clite.term.data.buff; f.appendChild(i); clite.term.data.form = f; clite.term.data.field = i; - var t = document.getElementById('terminal'); - if (!t) - return; - t.appendChild(f); + if (clite.term.data.isalt) { + var t = document.getElementById('termalt'); + if (!t) + t = clite.term.genAlt(); + t.appendChild(f); + }else{ + var t = document.getElementById('terminal'); + if (!t) + return; + t.appendChild(f); + } i.onkeydown = clite.term.events.keydown; i.onkeyup = clite.term.events.keyup; i.onfocusout = clite.term.events.refocus; @@ -1893,6 +1972,7 @@ clite.term = { } } f.onsubmit = function() {return false;}; + clite.term.events.refocus(); }, setPass:function(is) { clite.term.data.type = (!!is) ? 'password' : 'text'; @@ -1944,78 +2024,45 @@ clite.term = { clite.term.data.show = true; clite.term.genForm(); break; + case 'alt': // switch to the alternate frame buffer + if (typeof v === 'boolean') { + clite.term.data.isalt = v; + } + if (!clite.term.data.isalt) { + var t = document.getElementById('termalt'); + if (t) + t.parentNode.removeChild(t); + }else{ + clite.term.genAlt(); + } + clite.term.genForm(); + return clite.term.data.isalt; + break; + case 'rawout': + if (!clite.term.data.isalt) + return false; + if (typeof v === 'boolean') { + clite.term.data.alt.israwOut = v; + } + return clite.term.data.alt.israwOut; + break; + case 'rawin': + if (!clite.term.data.isalt) + return false; + if (typeof v === 'boolean') { + clite.term.data.alt.israwIn = v; + } + return clite.term.data.alt.israwIn; + break; + case 'atton': + break; + case 'attoff': + break; + case 'echo': + break; default: return false; } - }, - api:{ - // gets an empty element with key events (allows view/less to display content in a clean element) - opentty:function() { - var tty = { - handleKeys:function(cb) { - this.data.cb = cb; - }, - draw:function(data) { - if (typeof data == 'string') { - this.data.el.innerHTML += clite.lib.htmlEncode(data); - return true; - }else if (data instanceof HTMLImageElement) { - this.data.el.appendChild(data); - return true; - } - return false; - }, - clear:function() { - this.data.el.innerHTML = ''; - }, - setRaw:function(r) { - this.data.el.style.whiteSpace = ((!!r) ? 'normal' : 'pre-wrap'); - }, - data:{ - cb:null, - el:null, - onkeypress:function(e) { - try{ - return this.cb(e); - }catch(err) {} - } - } - }; - clite.term.tty.data = tty; - tty.data.el = document.createElement('div'); - tty.data.el.id = 'tty'; - tty.data.el.onkeypress = tty.data.onkeypress; - var term = document.getElementById('terminal'); - var p = term.parentNode; - tty.data.el.style.width = term.offsetWidth+'px'; - if (p.offsetHeight > term.offsetHeight) { - tty.data.el.style.minHeight = p.offsetHeight+'px'; - }else{ - tty.data.el.style.minHeight = term.offsetHeight+'px'; - } - tty.data.el.style.position = 'absolute'; - tty.data.el.style.top = term.offsetTop+'px'; - tty.data.el.style.left = term.offsetLeft+'px'; - p.appendChild(tty.data.el); - // some calculations to help with putting content in - var s = document.createElement('span'); - s.innerText = 'W'; - tty.data.el.appendChild(s); - // 20 should not be hardcoded - tty.lines = parseInt((tty.data.el.offsetHeight/20)-1); - // TODO: shouldn't need the 0.8 thing, but urgh - tty.chars = parseInt((tty.data.el.offsetWidth/s.offsetWidth)*0.8); - tty.data.el.removeChild(s); - clite.term.events.refocus(); - return tty; - }, - closetty:function(tty) { - try{ - tty.data.el.parentNode.removeChild(tty.data.el); - clite.term.tty.data = null; - delete tty; - }catch(err){} - } } }; @@ -2144,7 +2191,5 @@ clite.lib = { exit:function(pid,v) { if (pid > -1) clite.proc.exit(pid); - if (clite.term.tty.data != null) - clite.term.api.closetty(clite.term.tty.data); } } diff --git a/clite/libclite.js b/clite/libclite.js index 35bb317..1d96c4a 100644 --- a/clite/libclite.js +++ b/clite/libclite.js @@ -97,6 +97,18 @@ return Object.create({ parts.push(s); // TODO: resolve all paths passed as arguments? - catches ~ and * etc return parts; + }, + + ptrnew:function() { + return Object.create({value:null}); + }, + ptrset:function(ptr,val) { + ptr.value = val; + }, + ptrget:function(ptr) { + if (typeof ptr.value === 'undefined') + return null; + return ptr.value; } }); diff --git a/clite/libcurses.js b/clite/libcurses.js new file mode 100644 index 0000000..01125ba --- /dev/null +++ b/clite/libcurses.js @@ -0,0 +1,108 @@ +clite.libs.data = function() { + +clite.libs.load('libcurses','curses',function(io,env) { + +return Object.create({ + + isinit:false, + types:{ + A_NULL:0, + A_BOLD:1, + }, + attmap:function(t) { + switch (t) { + case this.types.A_BOLD: + return 'bold'; + break; + } + return null; + }, + + initscr:function() { + // curses only works on a tty + if (!clite.io.isatty(io.pid,io.stdin)) + return false; + this.isinit = clite.term.ttyctrl('alt',true); + }, + + endwin:function() { + if (!this.isinit) + return; + clite.term.ttyctrl('rawin',false); + clite.term.ttyctrl('rawout',false); + this.isinit = clite.term.ttyctrl('alt',false); + }, + + cbreak:function() { + if (!this.isinit) + return; + clite.term.ttyctrl('rawin',true); + }, + + clear:function() { + if (!this.isinit) + return; + clite.term.clear(); + }, + + noecho:function() { + if (!this.isinit) + return; + clite.term.ttyctrl('echo',false); + }, + + printw:function(fmt) { + if (!this.isinit) + return; + var args = Array.from(arguments); + args.shift(); + clite.io.vfprintf(io.pid,io.stdout,fmt,args); + }, + + getch:function(cb) { + if (!this.isinit) + return false; + return clite.io.read(io.pid,io.stdin,cb); + }, + + refresh:function() { + // no op, drawing isn't queued + }, + + attron:function(a) { + if (!this.isinit) + return; + a = this.attmap(a); + clite.term.ttyctrl('atton',a); + }, + attroff:function(a) { + if (!this.isinit) + return; + a = this.attmap(a); + clite.term.ttyctrl('attoff',a); + }, + + getmaxx:function() { + if (!this.isinit) + return 0; + return clite.term.data.alt.cols; + }, + getmaxy:function() { + if (!this.isinit) + return 0; + return clite.term.data.alt.lines; + }, + getmaxyx:function(ptry,ptrx) { + if (!this.isinit) + return false; + ptrx.value = clite.term.data.alt.cols; + ptry.value = clite.term.data.alt.lines; + return true; + } + + +}); + +}); + +}; diff --git a/clite/libterm.js b/clite/libterm.js index 1560a7a..2cd8ea9 100644 --- a/clite/libterm.js +++ b/clite/libterm.js @@ -11,14 +11,6 @@ return Object.create({ ttyctrl:function(fn,v) { return clite.term.ttyctrl(fn,v); - }, - - // gets an empty element with key events (allows view/less to display content in a clean element) - opentty:function() { - return clite.term.api.opentty(); - }, - closetty:function(tty) { - return clite.term.api.closetty(tty); } }); diff --git a/clite/shell.js b/clite/shell.js index 49fa899..d8322e3 100644 --- a/clite/shell.js +++ b/clite/shell.js @@ -367,6 +367,10 @@ clite.commands.load('sh',function(args,env,io) { } // set the prompt term.ttyctrl('prompt',prompt); + // ensure input is enabled + term.ttyctrl('show'); + // and that we're on the main framebuffer + term.ttyctrl('alt',false); // then read from stdin stdio.read(io.stdin,input); }