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 000000000..ed883aede Binary files /dev/null and b/apps/terminal/app.png differ 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 000000000..5b38b2ab7 Binary files /dev/null and b/apps/terminal/screenshot1.png differ diff --git a/apps/terminal/screenshot2.png b/apps/terminal/screenshot2.png new file mode 100644 index 000000000..e6d94d8d2 Binary files /dev/null and b/apps/terminal/screenshot2.png differ