From 812a4cfaf469951d5ccedac7f2e8bdeb3a90e7cf Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Mon, 22 Jan 2024 14:39:04 +0000 Subject: [PATCH] Add VT100 termional app --- apps/terminal/ChangeLog | 1 + apps/terminal/app-icon.js | 1 + apps/terminal/app.js | 263 ++++++++++++++++++++++++++++++++++ apps/terminal/app.png | Bin 0 -> 393 bytes apps/terminal/metadata.json | 14 ++ apps/terminal/screenshot1.png | Bin 0 -> 2337 bytes apps/terminal/screenshot2.png | Bin 0 -> 3204 bytes 7 files changed, 279 insertions(+) create mode 100644 apps/terminal/ChangeLog create mode 100644 apps/terminal/app-icon.js create mode 100644 apps/terminal/app.js create mode 100644 apps/terminal/app.png create mode 100644 apps/terminal/metadata.json create mode 100644 apps/terminal/screenshot1.png create mode 100644 apps/terminal/screenshot2.png diff --git a/apps/terminal/ChangeLog b/apps/terminal/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/terminal/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/terminal/app-icon.js b/apps/terminal/app-icon.js new file mode 100644 index 000000000..1c35a67d2 --- /dev/null +++ b/apps/terminal/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4cA///w8h7fOm2V/8fjkf/Gt58jKf4AtiVJkmSARFICK3PlnyCJ1t03aEcBZjCBVJCP4R/CK9N8gRPpmUEaGSuRHP+1KCJ8m5AjgI4PyNZ+cAQIQCwgjKCgQRNdP4R/CL4PJAQIRWAH4ApA==")) \ No newline at end of file diff --git a/apps/terminal/app.js b/apps/terminal/app.js new file mode 100644 index 000000000..79a50c73f --- /dev/null +++ b/apps/terminal/app.js @@ -0,0 +1,263 @@ +/* Espruino VT100 JS REPL + + +TODO: Add option to connect to a remote BLE device + +*/ + +var settings = Object.assign({ + // default values + textSize: 1, + loopAround: 1, + oneToOne: 0, + speedScaling: 24 +}, /*require('Storage').readJSON("repl.settings.json", true) || */{}); + +// Key Maps for Keyboard +var KEYMAPLOWER = [ + "`1234567890-=\b\b", + "\tqwertyuiop[]\n\n", + "\2asdfghjkl;'#\x82\n", + "\2\\zxcvbnm,./\x80\x83\x81", +]; +var KEYMAPUPPER = [ + "¬!\"£$%^&*()_+\b\b", + "\tQWERTYUIOP{}\n\n", + "\2ASDFGHJKL:@~\x82\n", + "\2|ZXCVBNM<>?\x80\x83\x81", +]; +var KEYIMGL = Graphics.createImage(` + + # # + ## # +###### + ## # + # # + + + # + ### + ##### + # + # + # + # + # + # + # + # + # + # +`); +KEYIMGL.transparent = 0; +var KEYIMGR = Graphics.createImage(` + + + # + ## + ######### + ## + # + + + + ####### + # + # + # + # + # + ##### + ### + # + + # + ### + ##### + + + # # + ## ## +### ##### ### + ## ### ## + # # # + +`); +KEYIMGR.transparent = 0; +/* If a char in the keymap is >=128, +subtract 128 and look in this array for +multi-character key codes*/ +var KEYEXTRA = [ + String.fromCharCode(27, 91, 68), // 0x80 left + String.fromCharCode(27, 91, 67), // 0x81 right + String.fromCharCode(27, 91, 65), // 0x82 up + String.fromCharCode(27, 91, 66), // 0x83 down + String.fromCharCode(27, 91, 53, 126), // 0x84 page up + String.fromCharCode(27, 91, 54, 126), // 0x85 page down +]; + +// state +const R = Bangle.appRect; +var kbx = 0, + kby = 0, + kbdx = 0, + kbdy = 0, + kbShift = false, + flashToggle = false; +const PX = 12, + PY = 16, + DRAGSCALE = settings.speedScaling; +var xoff = 0, + yoff = g.getHeight() - PY * 4; + +function draw() { + "ram"; + var map = kbShift ? KEYMAPUPPER : KEYMAPLOWER; + //g.drawImage(KEYIMG,0,yoff); + g.reset().setFont("6x8:2"); + g.clearRect(R.x, yoff, R.x2, R.y2); + if (kbx >= 0) + g.setColor(g.theme.bgH).fillRect(xoff + kbx * PX, yoff + kby * PY, xoff + (kbx + 1) * PX - 1, yoff + (kby + 1) * PY - 1).setColor(g.theme.fg); + g.drawImage(KEYIMGL, xoff - 1, yoff + PY, { scale: 2 }); + g.drawImage(KEYIMGR, xoff + PX * 12, yoff, { scale: 2 }); + var replace = /[\x80\x81\x82\x83\x84\x85]/g; + g.drawString(map[0].replace(replace," "), xoff, yoff); + g.drawString(map[1].replace(replace," "), xoff, yoff + PY); + g.drawString(map[2].replace(replace," "), xoff, yoff + PY * 2); + g.drawString(map[3].replace(replace," "), xoff, yoff + PY * 3); + g.flip(); +} + +function startTerminal(dataOutCallback) { + g.reset().clearRect(R); + // Set up the terminal + term = require("VT100").connect(g, { + charWidth: 6, + charHeight: 8 + }); + term.oy = R.y; + term.h = yoff; // we added this - it's not usually part of it + term.consoleHeight = 0 | ((term.h - term.oy) / term.charH); + term.scrollDown = function() { + g.setClipRect(R.x, term.y, R.x2, term.oy + term.h); + g.scroll(0, -this.charH); + g.setClipRect(R.x, R.y, R.x2, R.y2); + this.y--; + }; + term.fgCol = g.theme.fg; + term.bgCol = g.theme.bg; + draw(); + var flashInterval = setInterval(() => { + flashToggle = !flashToggle; + draw(); + }, 1000); + + function keyPress(kbx, kby) { + var map = kbShift ? KEYMAPUPPER : KEYMAPLOWER; + var ch = map[kby][kbx]; + if (ch == "\2") + kbShift = !kbShift; + else { + if (ch.charCodeAt(0) > 127) + ch = KEYEXTRA[ch.charCodeAt(0) - 128]; + dataOutCallback(ch); + } + Bangle.buzz(20); + draw(); + } + + Bangle.setUI({ + mode: "custom", + drag: e => { + if (settings.oneToOne) { + kbx = Math.max(Math.min(Math.floor((e.x - 16) / (6 * 2)), 13), 0); + kby = Math.max(Math.min(Math.floor((e.y - 120) / (8 * 2)), 3), 0); + //print(e.y, kby, e.x, kbx); + } + + if (!settings.oneToOne) { + kbdx += e.dx; + kbdy += e.dy; + var dx = Math.round(kbdx / DRAGSCALE), + dy = Math.round(kbdy / DRAGSCALE); + kbdx -= dx * DRAGSCALE; + kbdy -= dy * DRAGSCALE; + if (dx || dy) { + if (settings.loopAround) { + kbx = (kbx + dx + 15) % 15; + kby = (kby + dy + 4) % 4; + } else { + kbx = Math.max(Math.min((kbx + dx), 13), 0); + kby = Math.max(Math.min((kby + dy), 3), 0); + } + } + } + draw(); + + if (!e.b && e.y > Bangle.appRect.y && settings.oneToOne /*&& settings.releaseToSelect*/ ) + keyPress(kbx, kby); + }, + touch: () => { + if (!settings.oneToOne /*|| !settings.releaseToSelect*/ ) + keyPress(kbx, kby); + } + }); + let catchSwipe = () => { + E.stopEventPropagation && E.stopEventPropagation(); + }; + Bangle.prependListener && Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares. + return { + dataReceived : function(d) { + g.reset().setFont("6x8"); + // USB.write(e); // optionally mirror back to the PC + // Send characters to the terminal + for (var i in d) term.char(d[i]); + // update the screen + g.flip(); + } + }; +} + +function mainMenu() { + E.showMenu({ + "":{title:"Terminal"}, + /*LANG*/"JS REPL" : function() { + var t = startTerminal(function(d) { + LoopbackB.write(d); + }); + LoopbackB.on('data', function(d) { + t.dataReceived(d); + }); + // Now move the console to Loopback + LoopbackA.setConsole(); + }, + /*LANG*/"Bluetooth" : function() { + Bangle.setUI(); + E.showMessage(/*LANG*/"Scanning...", /*LANG*/"Bluetooth"); + NRF.findDevices(function(devices) { + if (!devices.length) + return E.showAlert("No devices found").then(() => mainMenu()); + var menu = { "" : { title: /*LANG*/"Bluetooth", back : () => mainMenu() } }; + devices.forEach(dev => { + var name = dev.name || dev.id.substr(0,17); + menu[name] = function() { + Bangle.setUI(); + E.showMessage(/*LANG*/"Connecting...", /*LANG*/"Bluetooth"); + require("ble_uart").connect(dev).then(function(uart) { + var t = startTerminal(function(d) { + uart.write(d); + }); + t.dataReceived("Connected to:\n "+name+"\n") + uart.on('data', function(d) { t.dataReceived(d); }); + }).catch(err => { + E.showAlert(err.toString()).then(() => mainMenu()); + }); + }; + }); + E.showMenu(menu); + }, { filters: [{ services: ['6e400001-b5a3-f393-e0a9-e50e24dcca9e'] }], timeout: 2000, active:true }); + } + }); +} + +mainMenu(); \ No newline at end of file diff --git a/apps/terminal/app.png b/apps/terminal/app.png new file mode 100644 index 0000000000000000000000000000000000000000..ed883aede3a246764de84dcc9f4036f404988640 GIT binary patch literal 393 zcmV;40e1e0P)@#0VE;Gug& z_7g(svU&(ld2$2s;D8z~Mu6m;hq48Isi-*x6j@{%<0p5=i*>*vl7w6jWM3Y#@>X4l4j&p@2>7ataBK n(KU(sFH~;*=Pn_H7@Oc7(T{9Dl6PQU00000NkvXXu0mjfRKcLP literal 0 HcmV?d00001 diff --git a/apps/terminal/metadata.json b/apps/terminal/metadata.json new file mode 100644 index 000000000..d32d00927 --- /dev/null +++ b/apps/terminal/metadata.json @@ -0,0 +1,14 @@ +{ "id": "terminal", + "name": "VT100 Terminal", + "shortName":"Terminal", + "version":"0.01", + "description": "Terminal and Keyboard that can be used as a REPL. You can type JS commands into Bangle.js's own REPL and execute them, or you can connect to other Bluetooth LE UART devices (like other Espruinos) and issue commands.", + "icon": "app.png", + "tags": "terminal,tool,bluetooth", + "screenshots" : [ { "url":"screenshot1.png" }, { "url":"screenshot2.png" } ], + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"terminal.app.js","url":"app.js"}, + {"name":"terminal.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/terminal/screenshot1.png b/apps/terminal/screenshot1.png new file mode 100644 index 0000000000000000000000000000000000000000..5b38b2ab775e7e7baa3c7cf97f4abfe018c98cdd GIT binary patch literal 2337 zcmeHJ{Xf%-9-nQsO_8CLZIOjePr4)12i+PIe9w!uYoLiQghkK}~ z(`FS>qOWHTCd$*udfnIO_4&MC@7L$^`Q`n2|M1DUR9Au>h{HqAfj^l?`o2sebA!L!TMrW}(c|8h^KCWhI^Dyb$w%rvJ(?QdMR^wi>@N3|tH znX1U3&!!w9J(e2Wxv?~wlz66ax9`De;(&i;^%u?YXX>w;_8_lKw$~bN{_{jOUo)hr zx!kyJ_X@8_;0d^GWYGs$vF?Cd?Nx&p_7#}&SU=~{9Pn%0>$$bnz*14YEBN(W@zX@v z-y9G)OLw+r^LX@hcO$q?)!}P#KOgyK5DN{j%+|#{IRRZ?T6*!BbLgq@j}+R*7cn#)Y1~jjGC|%M5Jh0@*u*kuT2QE-^X9++JoL z>=_fmzNLWmk}ku4_pjG#0F$`cC*>_r4M~>Cg4WyH^dicA@(2N%G;vpv$~iD*>^+5& zAyow5#%+EJgMUL@mABlo2Q2EUoo76Wz@lF5d)E9HGOqK{Ptljg0cY-0lx&GbkMc&- zj9S^VQK*05Bw3$l5k8HKr>m@_{(44{pYyI6ta>J70@$^8=4O;D7>lVLp$ESJH9@S6ipR1%{XRxm!^+#hfQp3c4vRZ;swa{n zYC9ES!_+h$b&P-c$@PByD|ao&Bh*8>?A9s0w%+K$Y2bUI7}ae+u&%-2TrCpTfr;>b z-5Uz)!niFpNM2*R7!VNT4?CNVsVl(kMuH# z7ccPE=rdwsoxFn*HqvLL5zom<(b9=-T7GESYNdJk-42)jc^r41T^TJcoi0dU(5-&1 zCo3r-Hr~TVejjr!>uSg2>`@QyHEAS#A|nj5GTBY!dF|%X_A@8GtwvvEr@WMYzDeUV zislWz))oOn@dDFVH}_YE*>4xvHOO^n$vv;2>LX->d^}PG{lJPI9(!O#=?CXYgOiVo zK>zW{#zdOg0SfakGTovPboO8W)kzE(11{jXa^muvC16OS%@EiFdGoH_$cRpFwneZm z&~+au14rx3Q3G!wPVCBrm`nld1`x&D-74t4uoec{{LyTuE0p4=WTeox-a#)Uz=#?P z!c!uCE{Jfil7@>sZb>qI+>fl~XNd6@Z<+bV!nxM6a>o&dm0RgfHu$@nO)l|1qFs0U z3GR^vnLUao5A4kRot*P&FZjsz5QM2_21*3UCZW2b&C4N0 z8JC%&Hd1>aX1|B|N9uFgzWT(GK3WEH>HcwR&UU6{eCwV0Tu7Rf;x@e)m)qto7cTSH z9%+#wChInoY-*fO{Ni>(*k*$CB&YdMfl)Lp9i4b?6L~y7#_9eJlP`d)@#;d?3u;@B zoILdocI4|~sb4KGh0nl7rbFHvep_)a|J_%dIkeQx(K#A3rl&~8%%{t*o21&Rg^SBP zQ1ut5=i7XTI}TjAKpRn6m;*kFwPbsK9f-iH2VlIYS!OF-v%aw*&V&2TE-f4>oZa@w3xM ztOLoy-xN*fgpB(FZ?Q;()#EO}6;x}%2&Q@EYw4-sOTwzT1QE4BN9f>+C&%b^-5hf< z*NSH)k!7qxr~bP2!}tM8n_J#i@>bROh9picQ){#lLGV#(^LdD~Kpy96!#nLkuvlhX zp_-6?KpR%r?ht&z;!hipUOs7M)U;X~^OOTE%Jb(r9E^zH^Or-4%!RY!AEAR-2#oXx zBc=8zvCawqai;i~1{a>1;zr&!NreY!+5&kM?S{s5`qU4I2aX_>pk5Rx$Gt@u1b)%I(56JPHlRd}I H@9+Nxifcsj literal 0 HcmV?d00001 diff --git a/apps/terminal/screenshot2.png b/apps/terminal/screenshot2.png new file mode 100644 index 0000000000000000000000000000000000000000..e6d94d8d263d35016d6cfc68f6041ffaf9aeb90b GIT binary patch literal 3204 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 Ee@}Vz>% literal 0 HcmV?d00001