diff --git a/.babelrc b/.babelrc index d8d6229..e087903 100644 --- a/.babelrc +++ b/.babelrc @@ -6,13 +6,9 @@ "last 2 versions", "> 4%", "ie 11", - "safari 8", - "android 4.4" + "safari 8" ] } - }], - ["minify", { - "mergeVars": false }] ] } diff --git a/.eslintignore b/.eslintignore index 76fd6af..94e5049 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,7 +2,8 @@ out/**/* # libraries -js/lib/* +js/lib/chibi.js +js/lib/polyfills.js # php generated file js/lang.js diff --git a/.eslintrc b/.eslintrc index b6aeb27..3d11065 100644 --- a/.eslintrc +++ b/.eslintrc @@ -148,7 +148,7 @@ "object-property-newline": ["error", { "allowMultiplePropertiesPerLine": true }], "one-var": ["error", { "initialized": "never" }], "operator-linebreak": ["error", "after", { "overrides": { "?": "before", ":": "before" } }], - "padded-blocks": ["error", { "blocks": "never", "switches": "never", "classes": "never" }], + "padded-blocks": ["off", { "blocks": "never", "switches": "never", "classes": "never" }], "prefer-promise-reject-errors": "error", "quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": true }], "rest-spread-spacing": ["error", "never"], diff --git a/_build_common.sh b/_build_common.sh index 833611b..0838336 100755 --- a/_build_common.sh +++ b/_build_common.sh @@ -1,3 +1,7 @@ #!/bin/bash export FRONT_END_HASH=$(git rev-parse --short HEAD) + +if [ -z "$ESP_LANG" ]; then + export ESP_LANG=en +fi diff --git a/_build_css.sh b/_build_css.sh index 8679569..3bd074e 100755 --- a/_build_css.sh +++ b/_build_css.sh @@ -10,4 +10,4 @@ else fi mkdir -p out/css -npm run sass -- --output-style ${stylearg} sass/app.scss "out/css/app.$FRONT_END_HASH.css" +npm run sass -- --output-style ${stylearg} sass/app.scss "out/css/app.$FRONT_END_HASH-$ESP_LANG.css" diff --git a/_build_html.sh b/_build_html.sh index a4f869d..cb125ea 100755 --- a/_build_html.sh +++ b/_build_html.sh @@ -3,4 +3,4 @@ source "_build_common.sh" echo 'Building HTML...' -php ./compile_html.php +php ./compile_html.php $@ diff --git a/_build_js.sh b/_build_js.sh index 7321e48..32ff64a 100755 --- a/_build_js.sh +++ b/_build_js.sh @@ -2,8 +2,6 @@ source "_build_common.sh" mkdir -p out/js -echo 'Generating lang.js...' -php ./dump_js_lang.php echo 'Processing JS...' npm run webpack diff --git a/_debug_replacements.php b/_debug_replacements.php index 5363312..3e99899 100644 --- a/_debug_replacements.php +++ b/_debug_replacements.php @@ -93,4 +93,6 @@ return [ 'theme' => 0, 'pwlock' => 0, 'access_name' => 'espterm', + + 'allow_decopt_12' => 0, ]; diff --git a/_pages.php b/_pages.php index c2d9ad7..b45d23f 100644 --- a/_pages.php +++ b/_pages.php @@ -41,7 +41,7 @@ pg('help', 'cfg page-help', 'help', '/help'); pg('about', 'cfg page-about', 'about', '/about'); pg('term', 'term', '', '/', 'title.term'); -pg('reset_screen', 'api', '', '/system/cls', 'title.term'); +pg('reset_screen', 'api', '', '/api/v1/clear', 'title.term'); pg('index', 'api', '', '/', ''); diff --git a/base.php b/base.php index e31c847..57ce7fb 100644 --- a/base.php +++ b/base.php @@ -35,6 +35,7 @@ if (!file_exists(__DIR__ . '/_env.php')) { define('JS_WEB_ROOT', $root); +define('ESP_PROD', (bool)getenv('ESP_PROD')); define('ESP_DEMO', (bool)getenv('ESP_DEMO')); if (ESP_DEMO) { define('DEMO_APS', << */ diff --git a/build.sh b/build.sh index 75ac53f..4763975 100755 --- a/build.sh +++ b/build.sh @@ -7,8 +7,8 @@ source "_build_common.sh" rm -fr out/* ./_build_css.sh -./_build_js.sh -./_build_html.sh +./_build_js.sh $@ +./_build_html.sh $@ ./_build_assets.sh echo 'ESPTerm front-end ready' diff --git a/compile_html.php b/compile_html.php index c8698fc..099011a 100755 --- a/compile_html.php +++ b/compile_html.php @@ -55,9 +55,24 @@ foreach($_pages as $_k => $p) { // making it not a very big improvement at the expense of ugly html. // $s = process_html($s); ob_clean(); - } // clean up - $of = $dest . $_k . ((in_array($_k, $no_tpl_files)||ESP_DEMO) ? '.html' : '.tpl'); - file_put_contents($of, $s); // write to a file + } + + $outputPath = $dest . $_k . ((in_array($_k, $no_tpl_files)||ESP_DEMO) ? '.html' : '.tpl'); + + if (file_exists($outputPath)) unlink($outputPath); + if (ESP_PROD) { + $tmpfile = tempnam('/tmp', 'espterm').'.html'; + file_put_contents($tmpfile, $s); + // using https://github.com/tdewolff/minify + system('minify --html-keep-default-attrvals '. + '-o '.escapeshellarg($outputPath).' '. + ''.escapeshellarg($tmpfile), $rv); + + // fallback if minify is not installed + if (!file_exists($outputPath)) file_put_contents($outputPath, $s); + } else { + file_put_contents($outputPath, $s); + } } ob_flush(); diff --git a/dump_js_lang.php b/dump_js_lang.php deleted file mode 100755 index d577f16..0000000 --- a/dump_js_lang.php +++ /dev/null @@ -1,22 +0,0 @@ - 0) { + if (e.deltaY > 0) { val += step } else { val -= step } - if (!Number.isFinite(min)) val = Math.max(val, +min) - if (!Number.isFinite(max)) val = Math.min(val, +max) + if (Number.isFinite(min)) val = Math.max(val, +min) + if (Number.isFinite(max)) val = Math.min(val, +max) $this.val(val) if ('createEvent' in document) { diff --git a/js/index.js b/js/index.js index eff520a..dfb22b0 100644 --- a/js/index.js +++ b/js/index.js @@ -14,3 +14,5 @@ window.$ = $ window.qs = qs window.themes = require('./term/themes') + +window.TermConf = require('./term_conf') diff --git a/js/lang.js b/js/lang.js index 31117a3..01bac93 100644 --- a/js/lang.js +++ b/js/lang.js @@ -1,8 +1,5 @@ -// Generated from PHP locale file -let _tr = { - "wifi.connected_ip_is": "Connected, IP is ", - "wifi.not_conn": "Not connected.", - "wifi.enter_passwd": "Enter password for \":ssid:\"" -}; +let data = require('locale-data') -module.exports = function tr (key) { return _tr[key] || '?' + key + '?' } +module.exports = function localize (key) { + return data[key] || `?${key}?` +} diff --git a/js/lib/chibi.js b/js/lib/chibi.js old mode 100755 new mode 100644 diff --git a/js/lib/color_utils.js b/js/lib/color_utils.js new file mode 100644 index 0000000..c3f490a --- /dev/null +++ b/js/lib/color_utils.js @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2010 Tim Baumann + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +// NOTE: +// Extracted from ColorTriangle and +// Converted to ES6 by MightyPork (2017) + +/******************* + * Color conversion * + *******************/ + +const M = Math +const TAU = 2 * M.PI + +exports.hue2rgb = function (v1, v2, h) { + if (h < 0) h += 1 + if (h > 1) h -= 1 + + if ((6 * h) < 1) return v1 + (v2 - v1) * 6 * h + if ((2 * h) < 1) return v2 + if ((3 * h) < 2) return v1 + (v2 - v1) * ((2 / 3) - h) * 6 + return v1 +} + +exports.hsl2rgb = function (h, s, l) { + h /= TAU + let r, g, b + + if (s === 0) { + r = g = b = l + } else { + let var_1, var_2 + + if (l < 0.5) var_2 = l * (1 + s) + else var_2 = (l + s) - (s * l) + + var_1 = 2 * l - var_2 + + r = exports.hue2rgb(var_1, var_2, h + (1 / 3)) + g = exports.hue2rgb(var_1, var_2, h) + b = exports.hue2rgb(var_1, var_2, h - (1 / 3)) + } + return [r, g, b] +} + +exports.rgb2hsl = function (r, g, b) { + const min = M.min(r, g, b) + const max = M.max(r, g, b) + const d = max - min // delta + + let h, s, l + + l = (max + min) / 2 + + if (d === 0) { + // gray + h = s = 0 // HSL results from 0 to 1 + } else { + // chroma + if (l < 0.5) s = d / (max + min) + else s = d / (2 - max - min) + + const d_r = (((max - r) / 6) + (d / 2)) / d + const d_g = (((max - g) / 6) + (d / 2)) / d + const d_b = (((max - b) / 6) + (d / 2)) / d // deltas + + if (r === max) h = d_b - d_g + else if (g === max) h = (1 / 3) + d_r - d_b + else if (b === max) h = (2 / 3) + d_g - d_r + + if (h < 0) h += 1 + else if (h > 1) h -= 1 + } + h *= TAU + return [h, s, l] +} + +exports.hex2rgb = function (hex) { + const groups = hex.match(/^#([\da-f]{3,6})$/i) + if (groups) { + hex = groups[1] + const bytes = hex.length / 3 + const max = (16 ** bytes) - 1 + return [0, 1, 2].map(x => parseInt(hex.slice(x * bytes, (x + 1) * bytes), 16) / max) + } + return [0, 0, 0] +} + +function pad (n) { + return `00${n}`.substr(-2) +} + +exports.rgb255ToHex = function (r, g, b) { + return '#' + [r, g, b].map(x => pad(x.toString(16))).join('') +} + +exports.rgb2hex = function (r, g, b) { + return '#' + [r, g, b].map(x => pad(Math.round(x * 255).toString(16))).join('') +} diff --git a/js/lib/colortriangle.js b/js/lib/colortriangle.js new file mode 100644 index 0000000..699f351 --- /dev/null +++ b/js/lib/colortriangle.js @@ -0,0 +1,572 @@ +/* + * Copyright (c) 2010 Tim Baumann + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +// NOTE: Converted to ES6 by MightyPork (2017) +// Modified for ESPTerm + +const EventEmitter = require('events') +const { + rgb2hex, + hex2rgb, + hsl2rgb, + rgb2hsl +} = require('./color_utils') + +const win = window +const doc = document +const M = Math +const TAU = 2 * M.PI + +function times (i, fn) { + for (let j = 0; j < i; j++) { + fn(j) + } +} + +function each (obj, fn) { + if (obj.length) { + times(obj.length, function (i) { + fn(obj[i], i) + }) + } else { + for (let key in obj) { + if (obj.hasOwnProperty(key)) { + fn(obj[key], key) + } + } + } +} + +module.exports = class ColorTriangle extends EventEmitter { + /**************** + * ColorTriangle * + ****************/ + + // Constructor function: + constructor (color, options) { + super() + + this.options = { + size: 150, + padding: 8, + triangleSize: 0.8, + wheelPointerColor1: '#444', + wheelPointerColor2: '#eee', + trianglePointerSize: 16, + // wheelPointerSize: 16, + trianglePointerColor1: '#eee', + trianglePointerColor2: '#444', + background: 'transparent' + } + + this.pixelRatio = window.devicePixelRatio + + this.setOptions(options) + this.calculateProperties() + + this.createContainer() + this.createTriangle() + this.createWheel() + this.createWheelPointer() + this.createTrianglePointer() + this.attachEvents() + + color = color || '#f00' + if (typeof color == 'string') { + this.setHEX(color) + } + } + + calculateProperties () { + let opts = this.options + + this.padding = opts.padding + this.innerSize = opts.size - opts.padding * 2 + this.triangleSize = opts.triangleSize * this.innerSize + this.wheelThickness = (this.innerSize - this.triangleSize) / 2 + this.wheelPointerSize = opts.wheelPointerSize || this.wheelThickness + + this.wheelRadius = (this.innerSize) / 2 + this.triangleRadius = (this.triangleSize) / 2 + this.triangleSideLength = M.sqrt(3) * this.triangleRadius + } + + calculatePositions () { + const r = this.triangleRadius + const hue = this.hue + const third = TAU / 3 + const s = this.saturation + const l = this.lightness + + // Colored point + const hx = this.hx = M.cos(hue) * r + const hy = this.hy = -M.sin(hue) * r + // Black point + const sx = this.sx = M.cos(hue - third) * r + const sy = this.sy = -M.sin(hue - third) * r + // White point + const vx = this.vx = M.cos(hue + third) * r + const vy = this.vy = -M.sin(hue + third) * r + // Current point + const mx = (sx + vx) / 2 + const my = (sy + vy) / 2 + const a = (1 - 2 * M.abs(l - 0.5)) * s + this.x = sx + (vx - sx) * l + (hx - mx) * a + this.y = sy + (vy - sy) * l + (hy - my) * a + } + + createContainer () { + let c = this.container = doc.createElement('div') + c.className = 'color-triangle' + + c.style.display = 'block' + c.style.padding = `${this.padding}px` + c.style.position = 'relative' + c.style.boxShadow = '0 1px 10px black' + c.style.borderRadius = '5px' + c.style.width = c.style.height = `${this.innerSize + 2 * this.padding}px` + c.style.background = this.options.background + } + + createWheel () { + let c = this.wheel = doc.createElement('canvas') + c.width = c.height = this.innerSize * this.pixelRatio + c.style.width = c.style.height = `${this.innerSize}px` + c.style.position = 'absolute' + c.style.margin = c.style.padding = '0' + c.style.left = c.style.top = `${this.padding}px` + + this.drawWheel(c.getContext('2d')) + this.container.appendChild(c) + } + + drawWheel (ctx) { + let s, i + + ctx.save() + ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0) + ctx.translate(this.wheelRadius, this.wheelRadius) + s = this.wheelRadius - this.triangleRadius + // Draw a circle for every color + for (i = 0; i < 360; i++) { + ctx.rotate(TAU / -360) // rotate one degree + ctx.beginPath() + ctx.fillStyle = 'hsl(' + i + ', 100%, 50%)' + ctx.arc(this.wheelRadius - (s / 2), 0, s / 2, 0, TAU, true) + ctx.fill() + } + ctx.restore() + } + + createTriangle () { + let c = this.triangle = doc.createElement('canvas') + + c.width = c.height = this.innerSize * this.pixelRatio + c.style.width = c.style.height = `${this.innerSize}px` + c.style.position = 'absolute' + c.style.margin = c.style.padding = '0' + c.style.left = c.style.top = this.padding + 'px' + + this.triangleCtx = c.getContext('2d') + + this.container.appendChild(c) + } + + drawTriangle () { + const hx = this.hx + const hy = this.hy + const sx = this.sx + const sy = this.sy + const vx = this.vx + const vy = this.vy + const size = this.innerSize + + let ctx = this.triangleCtx + + // clear + ctx.clearRect(0, 0, size * this.pixelRatio, size * this.pixelRatio) + + ctx.save() + ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0) + ctx.translate(this.wheelRadius, this.wheelRadius) + + // make a triangle + ctx.beginPath() + ctx.moveTo(hx, hy) + ctx.lineTo(sx, sy) + ctx.lineTo(vx, vy) + ctx.closePath() + ctx.clip() + + ctx.fillStyle = '#000' + ctx.fillRect(-this.wheelRadius, -this.wheelRadius, size, size) + // => black triangle + + // create gradient from hsl(hue, 1, 1) to transparent + let grad0 = ctx.createLinearGradient(hx, hy, (sx + vx) / 2, (sy + vy) / 2) + const hsla = 'hsla(' + M.round(this.hue * (360 / TAU)) + ', 100%, 50%, ' + grad0.addColorStop(0, hsla + '1)') + grad0.addColorStop(1, hsla + '0)') + ctx.fillStyle = grad0 + ctx.fillRect(-this.wheelRadius, -this.wheelRadius, size, size) + // => gradient: one side of the triangle is black, the opponent angle is $color + + // create color gradient from white to transparent + let grad1 = ctx.createLinearGradient(vx, vy, (hx + sx) / 2, (hy + sy) / 2) + grad1.addColorStop(0, '#fff') + grad1.addColorStop(1, 'rgba(255, 255, 255, 0)') + ctx.globalCompositeOperation = 'lighter' + ctx.fillStyle = grad1 + ctx.fillRect(-this.wheelRadius, -this.wheelRadius, size, size) + // => white angle + + ctx.restore() + } + + // The two pointers + createWheelPointer () { + let c = this.wheelPointer = doc.createElement('canvas') + const size = this.wheelPointerSize + c.width = c.height = size * this.pixelRatio + c.style.width = c.style.height = `${size}px` + c.style.position = 'absolute' + c.style.margin = c.style.padding = '0' + this.drawPointer(c.getContext('2d'), size / 2, this.options.wheelPointerColor1, this.options.wheelPointerColor2) + this.container.appendChild(c) + } + + moveWheelPointer () { + const r = this.wheelPointerSize / 2 + const s = this.wheelPointer.style + s.top = this.padding + this.wheelRadius - M.sin(this.hue) * (this.triangleRadius + this.wheelThickness / 2) - r + 'px' + s.left = this.padding + this.wheelRadius + M.cos(this.hue) * (this.triangleRadius + this.wheelThickness / 2) - r + 'px' + } + + createTrianglePointer () { // create pointer in the triangle + let c = this.trianglePointer = doc.createElement('canvas') + const size = this.options.trianglePointerSize + + c.width = c.height = size * this.pixelRatio + c.style.width = c.style.height = `${size}px` + c.style.position = 'absolute' + c.style.margin = c.style.padding = '0' + this.drawPointer(c.getContext('2d'), size / 2, this.options.trianglePointerColor1, this.options.trianglePointerColor2) + this.container.appendChild(c) + } + + moveTrianglePointer (x, y) { + const s = this.trianglePointer.style + const r = this.options.trianglePointerSize / 2 + s.top = (this.y + this.wheelRadius + this.padding - r) + 'px' + s.left = (this.x + this.wheelRadius + this.padding - r) + 'px' + } + + drawPointer (ctx, r, color1, color2) { + ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0) + ctx.fillStyle = color2 + ctx.beginPath() + ctx.arc(r, r, r, 0, TAU, true) + ctx.fill() // => black circle + ctx.fillStyle = color1 + ctx.beginPath() + ctx.arc(r, r, r - 2, 0, TAU, true) + ctx.fill() // => white circle with 1px black border + ctx.fillStyle = color2 + ctx.beginPath() + ctx.arc(r, r, r / 4 + 2, 0, TAU, true) + ctx.fill() // => black circle with big white border and a small black border + ctx.globalCompositeOperation = 'destination-out' + ctx.beginPath() + ctx.arc(r, r, r / 4, 0, TAU, true) + ctx.fill() // => transparent center + } + + // The Element and the DOM + inject (parent) { + parent.appendChild(this.container) + } + + getRelativeCoordinates (evt) { + let elem = this.triangle + let rect = elem.getBoundingClientRect() + + return { + x: evt.clientX - rect.x, + y: evt.clientY - rect.y + } + } + + dispose () { + let parent = this.container.parentNode + if (parent) { + parent.removeChild(this.container) + } + } + + getElement () { + return this.container + } + + // Color accessors + getCSS () { + const h = Math.round(this.hue * (360 / TAU)) + const s = Math.round(this.saturation * 100) + const l = Math.round(this.lightness * 100) + + return `hsl(${h}, ${s}%, ${l}%)` + } + + getHEX () { + return rgb2hex(...this.getRGB()) + } + + setHEX (hex) { + this.setRGB(...hex2rgb(hex)) + } + + getRGB () { + return hsl2rgb(...this.getHSL()) + } + + setRGB (r, g, b) { + this.setHSL(...rgb2hsl(r, g, b)) + } + + getHSL () { + return [this.hue, this.saturation, this.lightness] + } + + setHSL (h, s, l) { + this.hue = h + this.saturation = s + this.lightness = l + + this.initColor() + } + + initColor () { + this.calculatePositions() + this.moveWheelPointer() + this.drawTriangle() + this.moveTrianglePointer() + } + + // Mouse event handling + attachEvents () { + this.down = null + + let mousedown = (evt) => { + evt.stopPropagation() + evt.preventDefault() + + doc.body.addEventListener('mousemove', mousemove, false) + doc.body.addEventListener('mouseup', mouseup, false) + + let xy = this.getRelativeCoordinates(evt) + this.map(xy.x, xy.y) + } + + let mousemove = (evt) => { + let xy = this.getRelativeCoordinates(evt) + this.move(xy.x, xy.y) + } + + let mouseup = (evt) => { + if (this.down) { + this.down = null + this.emit('dragend') + } + doc.body.removeEventListener('mousemove', mousemove, false) + doc.body.removeEventListener('mouseup', mouseup, false) + } + + this.container.addEventListener('mousedown', mousedown, false) + this.container.addEventListener('mousemove', mousemove, false) + } + + map (x, y) { + let x0 = x + let y0 = y + x -= this.wheelRadius + y -= this.wheelRadius + + const r = M.sqrt(x * x + y * y) // Pythagoras + if (r > this.triangleRadius && r < this.wheelRadius) { + // Wheel + this.down = 'wheel' + this.emit('dragstart') + this.move(x0, y0) + } else if (r < this.triangleRadius) { + // Inner circle + this.down = 'triangle' + this.emit('dragstart') + this.move(x0, y0) + } + } + + move (x, y) { + if (!this.down) { + return + } + + x -= this.wheelRadius + y -= this.wheelRadius + + let rad = M.atan2(-y, x) + if (rad < 0) { + rad += TAU + } + + if (this.down === 'wheel') { + this.hue = rad + this.initColor() + this.emit('drag') + } else if (this.down === 'triangle') { + // get radius and max radius + let rad0 = (rad + TAU - this.hue) % TAU + let rad1 = rad0 % (TAU / 3) - (TAU / 6) + let a = 0.5 * this.triangleRadius + let b = M.tan(rad1) * a + let r = M.sqrt(x * x + y * y) // Pythagoras + let maxR = M.sqrt(a * a + b * b) // Pythagoras + + if (r > maxR) { + const dx = M.tan(rad1) * r + let rad2 = M.atan(dx / maxR) + if (rad2 > TAU / 6) { + rad2 = TAU / 6 + } else if (rad2 < -TAU / 6) { + rad2 = -TAU / 6 + } + rad += rad2 - rad1 + + rad0 = (rad + TAU - this.hue) % TAU + rad1 = rad0 % (TAU / 3) - (TAU / 6) + b = M.tan(rad1) * a + r = maxR = M.sqrt(a * a + b * b) // Pythagoras + } + + x = M.round(M.cos(rad) * r) + y = M.round(-M.sin(rad) * r) + + const l = this.lightness = ((M.sin(rad0) * r) / this.triangleSideLength) + 0.5 + + const widthShare = 1 - (M.abs(l - 0.5) * 2) + let s = this.saturation = (((M.cos(rad0) * r) + (this.triangleRadius / 2)) / (1.5 * this.triangleRadius)) / widthShare + s = M.max(0, s) // cannot be lower than 0 + s = M.min(1, s) // cannot be greater than 1 + + this.lightness = l + this.saturation = s + + this.x = x + this.y = y + this.moveTrianglePointer() + + this.emit('drag') + } + } + + /*************** + * Init helpers * + ***************/ + + static initInput (input, options) { + options = options || {} + + let ct + let openColorTriangle = function () { + let hex = input.value + if (options.parseColor) hex = options.parseColor(hex) + if (!ct) { + options.size = options.size || input.offsetWidth + options.background = win.getComputedStyle(input, null).backgroundColor + options.margin = options.margin || 10 + options.event = options.event || 'dragend' + + ct = new ColorTriangle(hex, options) + ct.on(options.event, () => { + const hex = ct.getHEX() + input.value = options.uppercase ? hex.toUpperCase() : hex + fireChangeEvent() + }) + } else { + ct.setHEX(hex) + } + + let top = input.offsetTop + if (win.innerHeight - input.getBoundingClientRect().top > input.offsetHeight + options.margin + options.size) { + top += input.offsetHeight + options.margin // below + } else { + top -= options.margin + options.size // above + } + + let el = ct.getElement() + el.style.position = 'absolute' + el.style.left = input.offsetLeft + 'px' + el.style.top = top + 'px' + el.style.zIndex = '1338' // above everything + + ct.inject(input.parentNode) + } + + let closeColorTriangle = () => { + if (ct) { + ct.dispose() + } + } + + let fireChangeEvent = () => { + let evt = doc.createEvent('HTMLEvents') + evt.initEvent('input', true, false) // bubbles = true, cancable = false + input.dispatchEvent(evt) // fire event + } + + input.addEventListener('focus', openColorTriangle, false) + input.addEventListener('blur', closeColorTriangle, false) + input.addEventListener('keyup', () => { + const val = input.value + if (val.match(/^#((?:[0-9A-Fa-f]{3})|(?:[0-9A-Fa-f]{6}))$/)) { + openColorTriangle() + fireChangeEvent() + } else { + closeColorTriangle() + } + }, false) + } + + /******************* + * Helper functions * + *******************/ + + setOptions (opts) { + opts = opts || {} + let dflt = this.options + let options = this.options = {} + + each(dflt, function (val, key) { + options[key] = (opts.hasOwnProperty(key)) + ? opts[key] + : val + }) + } +} diff --git a/js/term/buttons.js b/js/term/buttons.js new file mode 100644 index 0000000..34347f5 --- /dev/null +++ b/js/term/buttons.js @@ -0,0 +1,57 @@ +const { qs } = require('../utils') + +module.exports = function initButtons (input) { + let container = qs('#action-buttons') + + // button labels + let labels = [] + + // button elements + let buttons = [] + + // add a button element + let pushButton = function pushButton () { + let button = document.createElement('button') + button.classList.add('action-button') + button.setAttribute('data-n', buttons.length) + buttons.push(button) + container.appendChild(button) + + button.addEventListener('click', e => { + // might as well use the attribute ¯\_(ツ)_/¯ + let index = +button.getAttribute('data-n') + input.sendButton(index) + }) + + return button + } + + // remove a button element + let popButton = function popButton () { + let button = buttons.pop() + button.parentNode.removeChild(button) + } + + // sync with DOM + let update = function updateButtons () { + if (labels.length > buttons.length) { + for (let i = buttons.length; i < labels.length; i++) { + pushButton() + } + } else if (buttons.length > labels.length) { + for (let i = labels.length; i <= buttons.length; i++) { + popButton() + } + } + + for (let i = 0; i < labels.length; i++) { + let label = labels[i].trim() + let button = buttons[i] + button.textContent = label || '\u00a0' // label or nbsp + if (!label) button.classList.add('inactive') + else button.classList.remove('inactive') + } + } + + return { update, labels } +} diff --git a/js/term/connection.js b/js/term/connection.js index 8e8f7be..d53966c 100644 --- a/js/term/connection.js +++ b/js/term/connection.js @@ -3,6 +3,9 @@ const $ = require('../lib/chibi') let demo try { demo = require('./demo') } catch (err) {} +const RECONN_DELAY = 2000 +const HEARTBEAT_TIME = 3000 + /** Handle connections */ module.exports = class TermConnection extends EventEmitter { constructor (screen) { @@ -17,6 +20,18 @@ module.exports = class TermConnection extends EventEmitter { this.reconnTimeout = null this.forceClosing = false + try { + this.blobReader = new FileReader() + this.blobReader.onload = (evt) => { + this.onDecodedWSMessage(this.blobReader.result) + } + this.blobReader.onerror = (evt) => { + console.error(evt) + } + } catch (e) { + this.blobReader = null + } + this.pageShown = false this.disconnectTimeout = null @@ -59,43 +74,58 @@ module.exports = class TermConnection extends EventEmitter { } clearTimeout(this.reconnTimeout) - this.reconnTimeout = setTimeout(() => this.init(), 2000) + this.reconnTimeout = setTimeout(() => this.init(), RECONN_DELAY) this.emit('disconnect', evt.code) } - onWSMessage (evt) { - try { - switch (evt.data.charAt(0)) { - case '.': - // heartbeat, no-op message - break - - case '-': - // console.log('xoff'); - this.xoff = true - this.autoXoffTimeout = setTimeout(() => { - this.xoff = false - }, 250) - break - - case '+': - // console.log('xon'); + onDecodedWSMessage (str) { + switch (str.charAt(0)) { + case '.': + console.log(str) + // heartbeat, no-op message + break + + case '-': + // console.log('xoff'); + this.xoff = true + this.autoXoffTimeout = setTimeout(() => { this.xoff = false - clearTimeout(this.autoXoffTimeout) - break - - default: - this.screen.load(evt.data) - if (!this.pageShown) { - window.showPage() - this.pageShown = true - } - break + }, 250) + break + + case '+': + // console.log('xon'); + this.xoff = false + clearTimeout(this.autoXoffTimeout) + break + + default: + this.screen.load(str) + if (!this.pageShown) { + window.showPage() + this.pageShown = true + } + break + } + this.heartbeat() + } + + onWSMessage (evt) { + if (typeof evt.data === 'string') this.onDecodedWSMessage(evt.data) + else { + if (!this.blobReader) { + console.error('No FileReader!') + return + } + + if (this.blobReader.readyState !== 1) { + this.blobReader.readAsText(evt.data) + } else { + setTimeout(() => { + this.onWSMessage(evt) + }, 1) } - this.heartbeat() - } catch (e) { - console.error(e) } } @@ -166,7 +196,24 @@ module.exports = class TermConnection extends EventEmitter { heartbeat () { clearTimeout(this.heartbeatTimeout) - this.heartbeatTimeout = setTimeout(() => this.onHeartbeatFail(), 2500) + this.heartbeatTimeout = setTimeout(() => this.onHeartbeatFail(), HEARTBEAT_TIME) + } + + sendPing () { + console.log('> ping') + this.emit('ping') + $.get('http://' + window._root + '/api/v1/ping', (resp, status) => { + if (status === 200) { + clearInterval(this.pingInterval) + console.info('Server ready, opening socket…') + this.emit('ping-success') + this.init() + // location.reload() + } else this.emit('ping-fail', status) + }, { + timeout: 100, + loader: false // we have loader on-screen + }) } onHeartbeatFail () { @@ -174,22 +221,9 @@ module.exports = class TermConnection extends EventEmitter { this.emit('silence') console.error('Heartbeat lost, probing server...') clearInterval(this.pingInterval) + this.pingInterval = setInterval(() => { this.sendPing() }, 1000) - this.pingInterval = setInterval(() => { - console.log('> ping') - this.emit('ping') - $.get('http://' + window._root + '/system/ping', (resp, status) => { - if (status === 200) { - clearInterval(this.pingInterval) - console.info('Server ready, opening socket…') - this.emit('ping-success') - this.init() - // location.reload() - } else this.emit('ping-fail', status) - }, { - timeout: 100, - loader: false // we have loader on-screen - }) - }, 1000) + // first ping, if this gets through, it'll will reduce delay + setTimeout(() => { this.sendPing() }, 200) } } diff --git a/js/term/debug_screen.js b/js/term/debug_screen.js index d49d747..680690c 100644 --- a/js/term/debug_screen.js +++ b/js/term/debug_screen.js @@ -4,17 +4,32 @@ module.exports = function attachDebugScreen (screen) { const debugCanvas = mk('canvas') const ctx = debugCanvas.getContext('2d') - debugCanvas.style.position = 'absolute' - // hackity hack should probably set this in CSS - debugCanvas.style.top = '6px' - debugCanvas.style.left = '6px' - debugCanvas.style.pointerEvents = 'none' + debugCanvas.classList.add('debug-canvas') + + let mouseHoverCell = null + let updateToolbar + + let onMouseMove = e => { + mouseHoverCell = screen.screenToGrid(e.offsetX, e.offsetY) + startDrawing() + updateToolbar() + } + let onMouseOut = () => (mouseHoverCell = null) let addCanvas = function () { - if (!debugCanvas.parentNode) screen.canvas.parentNode.appendChild(debugCanvas) + if (!debugCanvas.parentNode) { + screen.canvas.parentNode.appendChild(debugCanvas) + screen.canvas.addEventListener('mousemove', onMouseMove) + screen.canvas.addEventListener('mouseout', onMouseOut) + } } let removeCanvas = function () { - if (debugCanvas.parentNode) debugCanvas.parentNode.removeChild(debugCanvas) + if (debugCanvas.parentNode) { + debugCanvas.parentNode.removeChild(debugCanvas) + screen.canvas.removeEventListener('mousemove', onMouseMove) + screen.canvas.removeEventListener('mouseout', onMouseOut) + onMouseOut() + } } let updateCanvasSize = function () { let { width, height, devicePixelRatio } = screen.window @@ -25,9 +40,13 @@ module.exports = function attachDebugScreen (screen) { debugCanvas.style.height = `${height * cellSize.height}px` } + let drawInfo = mk('div') + drawInfo.classList.add('draw-info') + let startTime, endTime, lastReason let cells = new Map() let clippedRects = [] + let updateFrames = [] let startDrawing @@ -39,7 +58,7 @@ module.exports = function attachDebugScreen (screen) { }, drawEnd () { endTime = Date.now() - console.log(`Draw: ${lastReason} (${(endTime - startTime)} ms) with fancy graphics: ${screen.window.graphics}`) + console.log(drawInfo.textContent = `Draw: ${lastReason} (${(endTime - startTime)} ms) with graphics=${screen.window.graphics}`) startDrawing() }, setCell (cell, flags) { @@ -47,6 +66,11 @@ module.exports = function attachDebugScreen (screen) { }, clipRect (...args) { clippedRects.push(args) + }, + pushFrame (frame) { + frame.push(Date.now()) + updateFrames.push(frame) + startDrawing() } } @@ -73,10 +97,16 @@ module.exports = function attachDebugScreen (screen) { } let isDrawing = false + let lastDrawTime = 0 + let t = 0 let drawLoop = function () { if (isDrawing) window.requestAnimationFrame(drawLoop) + let dt = (Date.now() - lastDrawTime) / 1000 + lastDrawTime = Date.now() + t += dt + let { devicePixelRatio, width, height } = screen.window let { width: cellWidth, height: cellHeight } = screen.getCellSize() let screenLength = width * height @@ -131,7 +161,42 @@ module.exports = function attachDebugScreen (screen) { ctx.fill() } - if (activeCells === 0) { + let didDrawUpdateFrames = false + if (updateFrames.length) { + let framesToDelete = [] + for (let frame of updateFrames) { + let time = frame[4] + let elapsed = Date.now() - time + if (elapsed > 1000) framesToDelete.push(frame) + else { + didDrawUpdateFrames = true + ctx.globalAlpha = 1 - elapsed / 1000 + ctx.strokeStyle = '#ff0' + ctx.lineWidth = 2 + ctx.strokeRect(frame[0] * cellWidth, frame[1] * cellHeight, frame[2] * cellWidth, frame[3] * cellHeight) + } + } + for (let frame of framesToDelete) { + updateFrames.splice(updateFrames.indexOf(frame), 1) + } + } + + if (mouseHoverCell) { + ctx.save() + ctx.globalAlpha = 1 + ctx.lineWidth = 1 + 0.5 * Math.sin(t * 10) + ctx.strokeStyle = '#fff' + ctx.lineJoin = 'round' + ctx.setLineDash([2, 2]) + ctx.lineDashOffset = t * 10 + ctx.strokeRect(mouseHoverCell[0] * cellWidth, mouseHoverCell[1] * cellHeight, cellWidth, cellHeight) + ctx.lineDashOffset += 2 + ctx.strokeStyle = '#000' + ctx.strokeRect(mouseHoverCell[0] * cellWidth, mouseHoverCell[1] * cellHeight, cellWidth, cellHeight) + ctx.restore() + } + + if (activeCells === 0 && !mouseHoverCell && !didDrawUpdateFrames) { isDrawing = false removeCanvas() } @@ -142,6 +207,7 @@ module.exports = function attachDebugScreen (screen) { addCanvas() updateCanvasSize() isDrawing = true + lastDrawTime = Date.now() drawLoop() } @@ -149,6 +215,33 @@ module.exports = function attachDebugScreen (screen) { const toolbar = mk('div') toolbar.classList.add('debug-toolbar') let toolbarAttached = false + const dataDisplay = mk('div') + dataDisplay.classList.add('data-display') + toolbar.appendChild(dataDisplay) + const internalDisplay = mk('div') + internalDisplay.classList.add('internal-display') + toolbar.appendChild(internalDisplay) + toolbar.appendChild(drawInfo) + const buttons = mk('div') + buttons.classList.add('toolbar-buttons') + toolbar.appendChild(buttons) + + { + const redraw = mk('button') + redraw.textContent = 'Redraw' + redraw.addEventListener('click', e => { + screen.renderer.resetDrawn() + screen.renderer.draw('debug-redraw') + }) + buttons.appendChild(redraw) + + const fancyGraphics = mk('button') + fancyGraphics.textContent = 'Toggle Graphics' + fancyGraphics.addEventListener('click', e => { + screen.window.graphics = +!screen.window.graphics + }) + buttons.appendChild(fancyGraphics) + } const attachToolbar = function () { screen.canvas.parentNode.appendChild(toolbar) @@ -161,20 +254,128 @@ module.exports = function attachDebugScreen (screen) { if (debug !== toolbarAttached) { toolbarAttached = debug if (debug) attachToolbar() - else detachToolbar() + else { + detachToolbar() + removeCanvas() + } } }) - screen.on('draw', () => { - if (!toolbarAttached) return - let cursorCell = screen.cursor.y * screen.window.width + screen.cursor.x - let cellFG = screen.screenFG[cursorCell] - let cellBG = screen.screenBG[cursorCell] - let cellCode = (screen.screen[cursorCell] || '').codePointAt(0) - let cellAttrs = screen.screenAttrs[cursorCell] + const displayCellAttrs = attrs => { + let result = attrs.toString(16) + if (attrs & 1 || attrs & 2) { + result += ':has(' + if (attrs & 1) result += 'fg' + if (attrs & 2) result += (attrs & 1 ? ',' : '') + 'bg' + result += ')' + } + let attributes = [] + if (attrs & (1 << 2)) attributes.push('\\[bold]bold\\()') + if (attrs & (1 << 3)) attributes.push('\\[underline]underln\\()') + if (attrs & (1 << 4)) attributes.push('\\[invert]invert\\()') + if (attrs & (1 << 5)) attributes.push('blink') + if (attrs & (1 << 6)) attributes.push('\\[italic]italic\\()') + if (attrs & (1 << 7)) attributes.push('\\[strike]strike\\()') + if (attrs & (1 << 8)) attributes.push('\\[overline]overln\\()') + if (attrs & (1 << 9)) attributes.push('\\[faint]faint\\()') + if (attrs & (1 << 10)) attributes.push('fraktur') + if (attributes.length) result += ':' + attributes.join() + return result.trim() + } + + const formatColor = color => color < 256 ? color : `#${`000000${(color - 256).toString(16)}`.substr(-6)}` + const getCellData = cell => { + if (cell < 0 || cell > screen.screen.length) return '(-)' + let cellAttrs = screen.renderer.drawnScreenAttrs[cell] | 0 + let cellFG = screen.renderer.drawnScreenFG[cell] | 0 + let cellBG = screen.renderer.drawnScreenBG[cell] | 0 + let fgText = formatColor(cellFG) + let bgText = formatColor(cellBG) + fgText += `\\[color=${screen.renderer.getColor(cellFG).replace(/ /g, '')}]●\\[]` + bgText += `\\[color=${screen.renderer.getColor(cellBG).replace(/ /g, '')}]●\\[]` + let cellCode = (screen.renderer.drawnScreen[cell] || '').codePointAt(0) | 0 let hexcode = cellCode.toString(16).toUpperCase() if (hexcode.length < 4) hexcode = `0000${hexcode}`.substr(-4) hexcode = `U+${hexcode}` - toolbar.textContent = `Cursor cell (${cursorCell}): ${hexcode} FG: ${cellFG} BG: ${cellBG} Attrs: ${cellAttrs.toString(2)}` + let x = cell % screen.window.width + let y = Math.floor(cell / screen.window.width) + return `((${y},${x})=${cell}:\\[bold]${hexcode}\\[]:F${fgText}:B${bgText}:A(${displayCellAttrs(cellAttrs)}))` + } + + const setFormattedText = (node, text) => { + node.innerHTML = '' + + let match + let attrs = {} + + let pushSpan = content => { + let span = mk('span') + node.appendChild(span) + span.textContent = content + for (let key in attrs) span[key] = attrs[key] + } + + while ((match = text.match(/\\\[(.*?)\]/))) { + if (match.index > 0) pushSpan(text.substr(0, match.index)) + + attrs = { style: '' } + let data = match[1].split(' ') + for (let attr of data) { + if (!attr) continue + let key, value + if (attr.indexOf('=') > -1) { + key = attr.substr(0, attr.indexOf('=')) + value = attr.substr(attr.indexOf('=') + 1) + } else { + key = attr + value = true + } + + if (key === 'color') console.log(value) + + if (key === 'bold') attrs.style += 'font-weight:bold;' + if (key === 'italic') attrs.style += 'font-style:italic;' + if (key === 'underline') attrs.style += 'text-decoration:underline;' + if (key === 'invert') attrs.style += 'background:#000;filter:invert(1);' + if (key === 'strike') attrs.style += 'text-decoration:line-through;' + if (key === 'overline') attrs.style += 'text-decoration:overline;' + if (key === 'faint') attrs.style += 'opacity:0.5;' + else if (key === 'color') attrs.style += `color:${value};` + else attrs[key] = value + } + + text = text.substr(match.index + match[0].length) + } + + if (text) pushSpan(text) + } + + let internalInfo = {} + + updateToolbar = () => { + if (!toolbarAttached) return + let text = `C((${screen.cursor.y},${screen.cursor.x}),hang:${screen.cursor.hanging},vis:${screen.cursor.visible})` + if (mouseHoverCell) { + text += ' m' + getCellData(mouseHoverCell[1] * screen.window.width + mouseHoverCell[0]) + } + setFormattedText(dataDisplay, text) + + if ('flags' in internalInfo) { + // we got ourselves some internal data + let text = ' ' + text += ` flags:${internalInfo.flags.toString(2)}` + text += ` curAttrs:${internalInfo.cursorAttrs.toString(2)}` + text += ` Region:${internalInfo.regionStart}->${internalInfo.regionEnd}` + text += ` Charset:${internalInfo.charsetGx} (0:${internalInfo.charsetG0},1:${internalInfo.charsetG1})` + text += ` Heap:${internalInfo.freeHeap}` + text += ` Clients:${internalInfo.clientCount}` + setFormattedText(internalDisplay, text) + } + } + + screen.on('draw', updateToolbar) + screen.on('internal', data => { + internalInfo = data + updateToolbar() }) } diff --git a/js/term/demo.js b/js/term/demo.js index 0ea9e33..c3efb04 100644 --- a/js/term/demo.js +++ b/js/term/demo.js @@ -2,6 +2,8 @@ const EventEmitter = require('events') const { parse2B } = require('../utils') const { themes } = require('./themes') +const encodeAsCodePoint = i => String.fromCodePoint(i + (i >= 0xD800 ? 0x801 : 1)) + class ANSIParser { constructor (handler) { this.reset() @@ -36,30 +38,41 @@ class ANSIParser { this.handler('insert-blanks', numOr1) } else if (type === 'q') this.handler('set-cursor-style', numOr1) else if (type === 'm') { - if (!numbers.length || numbers[0] === 0) { + if (!numbers.length) { this.handler('reset-style') return } - let type = numbers[0] - if (type === 1) this.handler('add-attrs', 1) // bold - else if (type === 2) this.handler('add-attrs', 1 << 1) // faint - else if (type === 3) this.handler('add-attrs', 1 << 2) // italic - else if (type === 4) this.handler('add-attrs', 1 << 3) // underline - else if (type === 5 || type === 6) this.handler('add-attrs', 1 << 4) // blink - else if (type === 7) this.handler('add-attrs', -1) // invert - else if (type === 9) this.handler('add-attrs', 1 << 6) // strike - else if (type === 20) this.handler('add-attrs', 1 << 5) // fraktur - else if (type >= 30 && type <= 37) this.handler('set-color-fg', type % 10) - else if (type >= 40 && type <= 47) this.handler('set-color-bg', type % 10) - else if (type === 39) this.handler('reset-color-fg') - else if (type === 49) this.handler('reset-color-bg') - else if (type >= 90 && type <= 98) this.handler('set-color-fg', (type % 10) + 8) - else if (type >= 100 && type <= 108) this.handler('set-color-bg', (type % 10) + 8) - else if (type === 38 || type === 48) { - if (numbers[1] === 5) { - let color = (numbers[2] | 0) & 0xFF - if (type === 38) this.handler('set-color-fg', color) - if (type === 48) this.handler('set-color-bg', color) + let type + while ((type = numbers.shift())) { + if (type === 0) this.handler('reset-style') + else if (type === 1) this.handler('add-attrs', 1 << 2) // bold + else if (type === 2) this.handler('add-attrs', 1 << 9) // faint + else if (type === 3) this.handler('add-attrs', 1 << 6) // italic + else if (type === 4) this.handler('add-attrs', 1 << 3) // underline + else if (type === 5 || type === 6) this.handler('add-attrs', 1 << 5) // blink + else if (type === 7) this.handler('add-attrs', 1 << 4) // invert + else if (type === 9) this.handler('add-attrs', 1 << 7) // strike + else if (type === 20) this.handler('add-attrs', 1 << 10) // fraktur + else if (type >= 30 && type <= 37) this.handler('set-color-fg', type % 10) + else if (type >= 40 && type <= 47) this.handler('set-color-bg', type % 10) + else if (type === 39) this.handler('reset-color-fg') + else if (type === 49) this.handler('reset-color-bg') + else if (type >= 90 && type <= 98) this.handler('set-color-fg', (type % 10) + 8) + else if (type >= 100 && type <= 108) this.handler('set-color-bg', (type % 10) + 8) + else if (type === 38 || type === 48) { + let mode = numbers.shift() + if (mode === 2) { + let r = numbers.shift() + let g = numbers.shift() + let b = numbers.shift() + let color = (r << 16 | g << 8 | b) + 256 + if (type === 38) this.handler('set-color-fg', color) + if (type === 48) this.handler('set-color-bg', color) + } else if (mode === 5) { + let color = (numbers.shift() | 0) & 0xFF + if (type === 38) this.handler('set-color-fg', color) + if (type === 48) this.handler('set-color-bg', color) + } } } } else if (type === 'h' || type === 'l') { @@ -101,8 +114,7 @@ class ANSIParser { if (!this.joinChunks) this.reset() } } -const TERM_DEFAULT_STYLE = 0 -const TERM_MIN_DRAW_DELAY = 10 +const TERM_DEFAULT_STYLE = [0, 0, 0] let getRainbowColor = t => { let r = Math.floor(Math.sin(t) * 2.5 + 2.5) @@ -117,33 +129,34 @@ class ScrollingTerminal { this.height = 25 this.termScreen = screen this.parser = new ANSIParser((...args) => this.handleParsed(...args)) + this.buttonLabels = [] this.reset() this._lastLoad = Date.now() - this.termScreen.load(this.serialize()) + this.loadTimer() window.showPage() } reset () { - this.style = TERM_DEFAULT_STYLE + this.style = TERM_DEFAULT_STYLE.slice() this.cursor = { x: 0, y: 0, style: 1, visible: true } this.trackMouse = false - this.theme = -1 - this.rainbow = false + this.theme = 0 + this.rainbow = this.superRainbow = false this.parser.reset() this.clear() } clear () { this.screen = [] for (let i = 0; i < this.width * this.height; i++) { - this.screen.push([' ', this.style]) + this.screen.push([' ', this.style.slice()]) } } scroll () { this.screen.splice(0, this.width) for (let i = 0; i < this.width; i++) { - this.screen.push([' ', TERM_DEFAULT_STYLE]) + this.screen.push([' ', TERM_DEFAULT_STYLE.slice()]) } this.cursor.y-- } @@ -152,7 +165,7 @@ class ScrollingTerminal { if (this.cursor.y >= this.height) this.scroll() } writeChar (character) { - this.screen[this.cursor.y * this.width + this.cursor.x] = [character, this.style] + this.screen[this.cursor.y * this.width + this.cursor.x] = [character, this.style.slice()] this.cursor.x++ if (this.cursor.x >= this.width) { this.cursor.x = 0 @@ -181,12 +194,12 @@ class ScrollingTerminal { } deleteChar () { // FIXME unused? this.moveBack() - this.screen.splice((this.cursor.y + 1) * this.width, 0, [' ', TERM_DEFAULT_STYLE]) + this.screen.splice((this.cursor.y + 1) * this.width, 0, [' ', TERM_DEFAULT_STYLE.slice()]) this.screen.splice(this.cursor.y * this.width + this.cursor.x, 1) } deleteForward (n) { n = Math.min(this.width, n) - for (let i = 0; i < n; i++) this.screen.splice((this.cursor.y + 1) * this.width, 0, [' ', TERM_DEFAULT_STYLE]) + for (let i = 0; i < n; i++) this.screen.splice((this.cursor.y + 1) * this.width, 0, [' ', TERM_DEFAULT_STYLE.slice()]) this.screen.splice(this.cursor.y * this.width + this.cursor.x, n) } clampCursor () { @@ -205,11 +218,12 @@ class ScrollingTerminal { } else if (action === 'clear') { this.clear() } else if (action === 'bell') { - this.termScreen.load('B') + this.termScreen.load('U\x01B') } else if (action === 'back') { this.moveBack() } else if (action === 'new-line') { this.newLine() + this.cursor.x = 0 } else if (action === 'return') { this.cursor.x = 0 } else if (action === 'set-cursor') { @@ -231,17 +245,21 @@ class ScrollingTerminal { } else if (action === 'set-cursor-style') { this.cursor.style = Math.max(0, Math.min(6, args[0])) } else if (action === 'reset-style') { - this.style = TERM_DEFAULT_STYLE + this.style = TERM_DEFAULT_STYLE.slice() } else if (action === 'add-attrs') { - this.style |= (args[0] << 16) + this.style[2] |= args[0] } else if (action === 'set-color-fg') { - this.style = (this.style & 0xFFFFFF00) | (1 << 8 << 16) | args[0] + this.style[0] = args[0] + this.style[2] |= 1 } else if (action === 'set-color-bg') { - this.style = (this.style & 0xFFFF00FF) | (1 << 9 << 16) | (args[0] << 8) + this.style[1] = args[0] + this.style[2] |= 1 << 1 } else if (action === 'reset-color-fg') { - this.style = this.style & 0xFFFEFF00 + this.style[0] = 0 + if (this.style[2] & 1) this.style[2] ^= 1 } else if (action === 'reset-color-bg') { - this.style = this.style & 0xFFFD00FF + this.style[1] = 0 + if (this.style[2] & (1 << 1)) this.style[2] ^= (1 << 1) } else if (action === 'hide-cursor') { this.cursor.visible = false } else if (action === 'show-cursor') { @@ -250,65 +268,132 @@ class ScrollingTerminal { } write (text) { this.parser.write(text) - this.scheduleLoad() } - serialize () { - let serialized = 'S' - serialized += String.fromCodePoint(this.height + 1) + String.fromCodePoint(this.width + 1) - serialized += String.fromCodePoint(this.cursor.y + 1) + String.fromCodePoint(this.cursor.x + 1) - + getScreenOpts () { + let data = 'O' + data += encodeAsCodePoint(25) + data += encodeAsCodePoint(80) + data += encodeAsCodePoint(this.theme) + data += encodeAsCodePoint(7) + data += encodeAsCodePoint(0) + data += encodeAsCodePoint(0) + data += encodeAsCodePoint(0) let attributes = +this.cursor.visible attributes |= (3 << 5) * +this.trackMouse // track mouse controls both attributes |= 3 << 7 // buttons/links always visible attributes |= (this.cursor.style << 9) - serialized += String.fromCodePoint(attributes + 1) + data += encodeAsCodePoint(attributes) + return data + } + getButtons () { + let data = 'B' + data += encodeAsCodePoint(this.buttonLabels.length) + data += this.buttonLabels.map(x => x + '\x01').join('') + return data + } + getTitle () { + return 'TESPTerm Web UI Demo\x01' + } + getCursor () { + let data = 'C' + data += encodeAsCodePoint(this.cursor.y) + data += encodeAsCodePoint(this.cursor.x) + data += encodeAsCodePoint(0) + return data + } + encodeColor (color) { + if (color < 256) { + return encodeAsCodePoint(color) + } else { + color -= 256 + return encodeAsCodePoint((color & 0xFFF) | 0x10000) + encodeAsCodePoint(color >> 12) + } + } + serializeScreen () { + let serialized = 'S' + serialized += encodeAsCodePoint(0) + encodeAsCodePoint(0) + serialized += encodeAsCodePoint(this.height) + encodeAsCodePoint(this.width) - let lastStyle = null + let lastStyle = [null, null, null] let index = 0 for (let cell of this.screen) { - let style = cell[1] + let style = cell[1].slice() if (this.rainbow) { let x = index % this.width let y = Math.floor(index / this.width) - // C instead of F in mask and 1 << 8 in attrs to change attr bits 8 and 9 - style = (style & 0xFFFC0000) | (1 << 8 << 16) | getRainbowColor((x + y) / 10 + Date.now() / 1000) + // C instead of F in mask and 1 << 8 in attrs to change attr bits 1 and 2 + let t = (x + y) / 10 + Date.now() / 1000 + if (this.superRainbow) { + t = (x * y + Date.now()) / 100 + Date.now() / 1000 + } + style[0] = getRainbowColor(t) + style[1] = 0 + if (this.superRainbow) style[1] = getRainbowColor(t / 10) + style[2] = style[2] | 1 + if (!this.superRainbow && style[2] & (1 << 1)) style[2] ^= (1 << 1) index++ } - if (style !== lastStyle) { - let foreground = style & 0xFF - let background = (style >> 8) & 0xFF - let attributes = (style >> 16) & 0xFFFF - let setForeground = foreground !== (lastStyle & 0xFF) - let setBackground = background !== ((lastStyle >> 8) & 0xFF) - let setAttributes = attributes !== ((lastStyle >> 16) & 0xFFFF) - if (setForeground && setBackground) serialized += '\x03' + String.fromCodePoint((style & 0xFFFF) + 1) - else if (setForeground) serialized += '\x05' + String.fromCodePoint(foreground + 1) - else if (setBackground) serialized += '\x06' + String.fromCodePoint(background + 1) - if (setAttributes) serialized += '\x04' + String.fromCodePoint(attributes + 1) - lastStyle = style - } + let foreground = style[0] + let background = style[1] + let attributes = style[2] + let setForeground = foreground !== lastStyle[0] + let setBackground = background !== lastStyle[1] + let setAttributes = attributes !== lastStyle[2] + + if (setForeground && setBackground) { + if (foreground < 256 && background < 256) { + serialized += '\x03' + encodeAsCodePoint((background << 8) | foreground) + } else { + serialized += '\x05' + this.encodeColor(foreground) + serialized += '\x06' + this.encodeColor(background) + } + } else if (setForeground) serialized += '\x05' + this.encodeColor(foreground) + else if (setBackground) serialized += '\x06' + this.encodeColor(background) + if (setAttributes) serialized += '\x04' + encodeAsCodePoint(attributes) + lastStyle = style + serialized += cell[0] } return serialized } - scheduleLoad () { - clearTimeout(this._scheduledLoad) - if (this._lastLoad < Date.now() - TERM_MIN_DRAW_DELAY) { - this.termScreen.load(this.serialize(), { theme: this.theme }) - this.theme = -1 // prevent useless theme setting next time - } else { - this._scheduledLoad = setTimeout(() => { - this.termScreen.load(this.serialize()) - }, TERM_MIN_DRAW_DELAY - this._lastLoad) + getUpdate () { + let topics = 0 + let topicData = [] + let screenOpts = this.getScreenOpts() + let title = this.getTitle() + let buttons = this.getButtons() + let cursor = this.getCursor() + let screen = this.serializeScreen() + if (this._screenOpts !== screenOpts) { + this._screenOpts = screenOpts + topicData.push(screenOpts) + } + if (this._title !== title) { + this._title = title + topicData.push(title) + } + if (this._buttons !== buttons) { + this._buttons = buttons + topicData.push(buttons) + } + if (this._cursor !== cursor) { + this._cursor = cursor + topicData.push(cursor) + } + if (this._screen !== screen) { + this._screen = screen + topicData.push(screen) } + if (!topicData.length) return '' + return 'U' + encodeAsCodePoint(topics) + topicData.join('') } - rainbowTimer () { - if (!this.rainbow) return - clearInterval(this._rainbowTimer) - this._rainbowTimer = setInterval(() => { - if (this.rainbow) this.scheduleLoad() - }, 50) + loadTimer () { + clearInterval(this._loadTimer) + this._loadTimer = setInterval(() => { + let update = this.getUpdate() + if (update) this.termScreen.load(update) + }, 30) } } @@ -326,22 +411,7 @@ class Process extends EventEmitter { } let demoData = { - buttons: { - 1: '', - 2: '', - 3: '', - 4: '', - 5: function (terminal, shell) { - if (shell.child) shell.child.destroy() - let chars = 'info\r' - let loop = function () { - shell.write(chars[0]) - chars = chars.substr(1) - if (chars) setTimeout(loop, 100) - } - setTimeout(loop, 200) - } - }, + buttons: [], mouseReceiver: null } @@ -456,7 +526,7 @@ let demoshIndex = { } return new Promise((resolve, reject) => { const self = this - let x = 14 + let x = 13 let cycles = 0 let loop = function () { for (let y = 0; y < splash.length; y++) { @@ -464,9 +534,9 @@ let demoshIndex = { if (dx > 0) drawCell(dx, y) } - if (++x < 69) { + if (++x < 70) { if (++cycles >= 3) { - setTimeout(loop, 20) + setTimeout(loop, 50) cycles = 0 } else loop() } else { @@ -557,7 +627,7 @@ let demoshIndex = { let theme = +args[0] | 0 const maxnum = themes.length if (!args.length || !Number.isFinite(theme) || theme < 0 || theme >= maxnum) { - this.emit('write', `\x1b[31mUsage: theme [0–${maxnum - 1}]\r\n`) + this.emit('write', `\x1b[31mUsage: theme [0–${maxnum - 1}]\n`) this.destroy() return } @@ -568,6 +638,40 @@ let demoshIndex = { this.destroy() } }, + themes: class ShowThemes extends Process { + color (hex) { + hex = parseInt(hex.substr(1), 16) + let r = hex >> 16 + let g = (hex >> 8) & 0xFF + let b = hex & 0xFF + this.emit('write', `\x1b[48;2;${r};${g};${b}m`) + if (((r + g + b) / 3) > 127) { + this.emit('write', '\x1b[38;5;16m') + } else { + this.emit('write', '\x1b[38;5;255m') + } + } + run (...args) { + for (let i in themes) { + let theme = themes[i] + + let name = ` ${i}`.substr(-2) + + this.emit('write', `Theme ${name}: `) + + for (let col = 0; col < 16; col++) { + let text = ` ${col}`.substr(-2) + this.color(theme[col]) + this.emit('write', text) + this.emit('write', '\x1b[m ') + } + + this.emit('write', '\n') + } + + this.destroy() + } + }, cursor: class SetCursor extends Process { run (...args) { let steady = args.includes('--steady') @@ -584,16 +688,40 @@ let demoshIndex = { } }, rainbow: class ToggleRainbow extends Process { - constructor (shell) { + constructor (shell, options = {}) { super() this.shell = shell + this.su = options.su + this.abort = false } - run () { - this.shell.terminal.rainbow = !this.shell.terminal.rainbow - this.shell.terminal.rainbowTimer() - this.emit('write', '') + write (data, newLine = true) { + if (data === 'y') { + this.shell.terminal.rainbow = !this.shell.terminal.rainbow + this.shell.terminal.superRainbow = true + demoData._didWarnRainbow = true + } else { + this.emit('write', data) + } + if (newLine) this.emit('write', '\x1b[0;32m\x1b[G\x1b[79PRainbow!\n') this.destroy() } + run () { + if (this.su && !this.shell.terminal.rainbow) { + if (!demoData._didWarnRainbow) { + this.emit('write', '\x1b[31;1mWarning: flashy colors. Continue? [y/N] ') + } else { + this.write('y', false) + } + } else { + this.shell.terminal.rainbow = !this.shell.terminal.rainbow + this.shell.terminal.superRainbow = false + this.destroy() + } + } + destroy () { + this.abort = true + super.destroy() + } }, mouse: class ShowMouse extends Process { constructor (shell) { @@ -601,6 +729,15 @@ let demoshIndex = { this.shell = shell } run () { + this.emit('buttons', [ + { + label: 'Exit', + action (shell) { + shell.write('\x03') + } + } + ]) + this.shell.terminal.trackMouse = true demoData.mouseReceiver = this this.randomData = [] @@ -618,7 +755,7 @@ let demoshIndex = { } render () { this.emit('write', '\x1b[m\x1b[2J\x1b[1;1H') - this.emit('write', '\x1b[97m\x1b[1mMouse Demo\r\n\x1b[mMouse movement, clicking and scrolling!') + this.emit('write', '\x1b[97m\x1b[1mMouse Demo\r\n\x1b[mMouse movement, clicking, and scrolling!') // render random data for scrolling for (let y = 0; y < 23; y++) { @@ -662,47 +799,26 @@ let demoshIndex = { constructor (shell) { super() this.shell = shell + this.didDestroy = false } run (...args) { if (args.length === 0) { - this.emit('write', '\x1b[31mUsage: sudo \x1b[m\r\n') - this.destroy() - } else if (args.length === 4 && args.join(' ').toLowerCase() === 'make me a sandwich') { - const b = '\x1b[33m' - const r = '\x1b[0m' - const l = '\x1b[32m' - const c = '\x1b[38;5;229m' - const h = '\x1b[38;5;225m' - this.emit('write', - ` ${b}_.---._\r\n` + - ` _.-~ ~-._\r\n` + - ` _.-~ ~-._\r\n` + - ` _.-~ ~---._\r\n` + - ` _.-~ ~\\\r\n` + - ` .-~ _.;\r\n` + - ` :-._ _.-~ ./\r\n` + - ` \`-._~-._ _..__.-~ _.-~\r\n` + - ` ${c}/ ${b}~-._~-._ / .__..--${c}~-${l}---._\r\n` + - `${c} \\_____(_${b};-._\\. _.-~_/${c} ~)${l}.. . \\\r\n` + - `${l} /(_____ ${b}\\\`--...--~_.-~${c}______..-+${l}_______)\r\n` + - `${l} .(_________/${b}\`--...--~/${l} _/ ${h} ${b}/\\\r\n` + - `${b} /-._${h} \\_ ${l}(___./_..-~${h}__.....${b}__..-~./\r\n` + - `${b} \`-._~-._${h} ~\\--------~ .-~${b}_..__.-~ _.-~\r\n` + - `${b} ~-._~-._ ${h}~---------\` ${b}/ .__..--~\r\n` + - `${b} ~-._\\. _.-~_/\r\n` + - `${b} \\\`--...--~_.-~\r\n` + - `${b} \`--...--~${r}\r\n`) + this.emit('write', '\x1b[31mUsage: sudo [command]\x1b[m\r\n') this.destroy() } else { let name = args.shift() if (this.shell.index[name]) { let Process = this.shell.index[name] if (Process instanceof Function) { - let child = new Process(this) + let child = this.child = new Process(this.shell, { su: true }) + this.on('in', data => child.write(data)) let write = data => this.emit('write', data) + let setButtons = buttons => this.emit('buttons', buttons) child.on('write', write) + child.on('buttons', setButtons) child.on('exit', code => { child.removeListener('write', write) + child.removeListener('buttons', setButtons) this.destroy() }) child.run(...args) @@ -716,12 +832,49 @@ let demoshIndex = { } } } + destroy () { + if (this.didDestroy) return + this.didDestroy = true + if (this.child) this.child.destroy() + super.destroy() + } }, make: class Make extends Process { + constructor (shell, options = {}) { + super() + this.su = options.su + } run (...args) { if (args.length === 0) this.emit('write', '\x1b[31mmake: *** No targets specified. Stop.\x1b[0m\r\n') else if (args.length === 3 && args.join(' ').toLowerCase() === 'me a sandwich') { - this.emit('write', '\x1b[31mmake: me a sandwich : Permission denied\x1b[0m\r\n') + if (this.su) { + const b = '\x1b[33m' + const r = '\x1b[0m' + const l = '\x1b[32m' + const c = '\x1b[38;5;229m' + const h = '\x1b[38;5;225m' + this.emit('write', + ` ${b}_.---._\r\n` + + ` _.-~ ~-._\r\n` + + ` _.-~ ~-._\r\n` + + ` _.-~ ~---._\r\n` + + ` _.-~ ~\\\r\n` + + ` .-~ _.;\r\n` + + ` :-._ _.-~ ./\r\n` + + ` \`-._~-._ _..__.-~ _.-~\r\n` + + ` ${c}/ ${b}~-._~-._ / .__..--${c}~-${l}---._\r\n` + + `${c} \\_____(_${b};-._\\. _.-~_/${c} ~)${l}.. . \\\r\n` + + `${l} /(_____ ${b}\\\`--...--~_.-~${c}______..-+${l}_______)\r\n` + + `${l} .(_________/${b}\`--...--~/${l} _/ ${h} ${b}/\\\r\n` + + `${b} /-._${h} \\_ ${l}(___./_..-~${h}__.....${b}__..-~./\r\n` + + `${b} \`-._~-._${h} ~\\--------~ .-~${b}_..__.-~ _.-~\r\n` + + `${b} ~-._~-._ ${h}~---------\` ${b}/ .__..--~\r\n` + + `${b} ~-._\\. _.-~_/\r\n` + + `${b} \\\`--...--~_.-~\r\n` + + `${b} \`--...--~${r}\r\n`) + } else { + this.emit('write', '\x1b[31mmake: me a sandwich : Permission denied\x1b[0m\r\n') + } } else { this.emit('write', `\x1b[31mmake: *** No rule to make target '${args.join(' ').toLowerCase()}'. Stop.\x1b[0m\r\n`) } @@ -747,6 +900,12 @@ let demoshIndex = { } } } +let autocompleteIndex = { + 'local-echo': 'local-echo [--suppress-note]', + theme: 'theme [n]', + cursor: 'cursor [block|line|bar] [--steady]', + sudo: 'sudo [command]' +} class DemoShell { constructor (terminal, printInfo) { @@ -756,6 +915,7 @@ class DemoShell { this.history = [] this.historyIndex = 0 this.cursorPos = 0 + this.lastInputLength = 0 this.child = null this.index = demoshIndex @@ -775,6 +935,8 @@ class DemoShell { this.terminal.write('$ \x1b[m') this.history.unshift('') this.cursorPos = 0 + + this.setButtons() } copyFromHistoryIndex () { if (!this.historyIndex) return @@ -782,8 +944,29 @@ class DemoShell { this.history[0] = current this.historyIndex = 0 } + getCompleted (visual = false) { + if (this.history[0]) { + let input = this.history[0] + let prefix = '' + if (input.startsWith('sudo ')) { + let newInput = input.replace(/^(sudo\s+)+/, '') + prefix = input.substr(0, input.length - newInput.length) + input = newInput + } + for (let name in this.index) { + if (name.startsWith(input) && name !== input) { + if (visual && name in autocompleteIndex) return prefix + autocompleteIndex[name] + return prefix + name + } + } + return null + } + return null + } handleParsed (action, ...args) { + this.terminal.write(`\x1b[${this.lastInputLength - this.cursorPos}P`) this.terminal.write('\b\x1b[P'.repeat(this.cursorPos)) + if (action === 'write') { this.copyFromHistoryIndex() this.history[0] = this.history[0].substr(0, this.cursorPos) + args[0] + this.history[0].substr(this.cursorPos) @@ -794,7 +977,11 @@ class DemoShell { this.cursorPos-- if (this.cursorPos < 0) this.cursorPos = 0 } else if (action === 'tab') { - console.warn('TAB not implemented') // TODO completion + let completed = this.getCompleted() + if (completed) { + this.history[0] = completed + this.cursorPos = this.history[0].length + } } else if (action === 'move-cursor-x') { this.cursorPos = Math.max(0, Math.min(this.history[this.historyIndex].length, this.cursorPos + args[0])) } else if (action === 'delete-line') { @@ -817,17 +1004,27 @@ class DemoShell { this.terminal.write(this.history[this.historyIndex]) this.terminal.write('\b'.repeat(this.history[this.historyIndex].length)) this.terminal.moveForward(this.cursorPos) - this.terminal.write('') // dummy. Apply the moveFoward + + this.lastInputLength = this.cursorPos + + let completed = this.getCompleted(true) + if (this.historyIndex === 0 && completed && action !== 'return') { + // show closest match faintly + let rest = completed.substr(this.history[0].length) + this.terminal.write(`\x1b[2m${rest}\x1b[m${'\b'.repeat(rest.length)}`) + this.lastInputLength += rest.length + } if (action === 'return') { - this.terminal.write('\r\n') + this.terminal.write('\n') this.parse(this.history[this.historyIndex]) } } parse (input) { if (input === 'help') input = 'info' // TODO: basic chaining (i.e. semicolon) - this.run(input) + if (input) this.run(input) + else this.prompt() } run (command) { let parts = [''] @@ -857,11 +1054,17 @@ class DemoShell { spawn (name, args = []) { let Process = this.index[name] if (Process instanceof Function) { + this.setButtons([]) this.child = new Process(this) let write = data => this.terminal.write(data) + let setButtons = buttons => this.setButtons(buttons) this.child.on('write', write) + this.child.on('buttons', setButtons) this.child.on('exit', code => { - if (this.child) this.child.removeListener('write', write) + if (this.child) { + this.child.removeListener('write', write) + this.child.removeListener('buttons', setButtons) + } this.child = null this.prompt(!code) }) @@ -871,6 +1074,46 @@ class DemoShell { this.prompt() } } + + setButtons (buttons) { + if (!buttons) { + const shell = this + let writeChars = chars => { + let loop = () => { + shell.write(chars[0]) + chars = chars.substr(1) + if (chars) setTimeout(loop, 100) + } + setTimeout(loop, 200) + } + + buttons = [ + { + label: 'Open GitHub', + action (shell) { + if (shell.child) shell.child.destroy() + writeChars('github\r') + } + }, + { + label: 'Help', + action (shell) { + if (shell.child) shell.child.destroy() + writeChars('info\r') + } + } + ] + } + if (!buttons.length) buttons.push({ label: '', action () {} }) + this.buttons = buttons + this.terminal.buttonLabels = buttons.map(x => x.label) + } + + onButtonPress (index) { + let button = this.buttons[index] + if (!button) return + button.action(this, this.terminal) + } } window.demoInterface = module.exports = { @@ -882,11 +1125,7 @@ window.demoInterface = module.exports = { this.shell.write(content) } else if (type === 'b') { let button = content.charCodeAt(0) - let action = demoData.buttons[button] - if (action) { - if (typeof action === 'string') this.shell.write(action) - else if (action instanceof Function) action(this.terminal, this.shell) - } + this.shell.onButtonPress(button - 1) } else if (type === 'm' || type === 'p' || type === 'r') { let row = parse2B(content, 0) let column = parse2B(content, 2) diff --git a/js/term/index.js b/js/term/index.js index 41593ac..a382d1a 100644 --- a/js/term/index.js +++ b/js/term/index.js @@ -1,4 +1,5 @@ const { qs, mk } = require('../utils') +const localize = require('../lang') const Notify = require('../notif') const TermScreen = require('./screen') const TermConnection = require('./connection') @@ -6,6 +7,7 @@ const TermInput = require('./input') const TermUpload = require('./upload') const initSoftKeyboard = require('./soft_keyboard') const attachDebugScreen = require('./debug_screen') +const initButtons = require('./buttons') /** Init the terminal sub-module - called from HTML */ module.exports = function (opts) { @@ -17,6 +19,13 @@ module.exports = function (opts) { screen.conn = conn input.termUpload = termUpload + const buttons = initButtons(input) + screen.on('button-labels', labels => { + // TODO: don't use pointers for this + buttons.labels.splice(0, buttons.labels.length, ...labels) + buttons.update() + }) + let showSplashTimeout = null let showSplash = (obj, delay = 250) => { clearTimeout(showSplashTimeout) @@ -27,11 +36,11 @@ module.exports = function (opts) { conn.on('open', () => { // console.log('*open') - showSplash({ title: 'Connecting', loading: true }) + showSplash({ title: localize('term_conn.connecting'), loading: true }) }) conn.on('connect', () => { // console.log('*connect') - showSplash({ title: 'Waiting for content', loading: true }) + showSplash({ title: localize('term_conn.waiting_content'), loading: true }) }) conn.on('load', () => { // console.log('*load') @@ -40,7 +49,7 @@ module.exports = function (opts) { }) conn.on('disconnect', () => { // console.log('*disconnect') - showSplash({ title: 'Disconnected' }) + showSplash({ title: localize('term_conn.disconnected') }, 500) screen.screen = [] screen.screenFG = [] screen.screenBG = [] @@ -48,12 +57,12 @@ module.exports = function (opts) { }) conn.on('silence', () => { // console.log('*silence') - showSplash({ title: 'Waiting for server', loading: true }, 0) + showSplash({ title: localize('term_conn.waiting_server'), loading: true }, 0) }) // conn.on('ping-fail', () => { screen.window.statusScreen = { title: 'Disconnected' } }) conn.on('ping-success', () => { // console.log('*ping-success') - showSplash({ title: 'Re-connecting', loading: true }, 0) + showSplash({ title: localize('term_conn.reconnecting'), loading: true }, 0) }) conn.init() @@ -67,20 +76,36 @@ module.exports = function (opts) { } qs('#screen').appendChild(screen.canvas) - screen.load(opts.labels, opts) // load labels and theme initSoftKeyboard(screen, input) if (attachDebugScreen) attachDebugScreen(screen) + let fullscreenIcon = {} // dummy let isFullscreen = false + let properFullscreen = false let fitScreen = false + let screenPadding = screen.window.padding let fitScreenIfNeeded = function fitScreenIfNeeded () { if (isFullscreen) { - screen.window.fitIntoWidth = window.screen.width - screen.window.fitIntoHeight = window.screen.height + fullscreenIcon.className = 'icn-resize-small' + if (properFullscreen) { + screen.window.fitIntoWidth = window.screen.width + screen.window.fitIntoHeight = window.screen.height + screen.window.padding = 0 + } else { + screen.window.fitIntoWidth = window.innerWidth + if (qs('#term-nav').classList.contains('hidden')) { + screen.window.fitIntoHeight = window.innerHeight + } else { + screen.window.fitIntoHeight = window.innerHeight - 24 + } + screen.window.padding = 0 + } } else { - screen.window.fitIntoWidth = fitScreen ? window.innerWidth - 20 : 0 + fullscreenIcon.className = 'icn-resize-full' + screen.window.fitIntoWidth = fitScreen ? window.innerWidth - 4 : 0 screen.window.fitIntoHeight = fitScreen ? window.innerHeight : 0 + screen.window.padding = screenPadding } } fitScreenIfNeeded() @@ -106,6 +131,8 @@ module.exports = function (opts) { // add fullscreen mode & button if (window.Element.prototype.requestFullscreen || window.Element.prototype.webkitRequestFullscreen) { + properFullscreen = true + let checkForFullscreen = function () { // document.fullscreenElement is not really supported yet, so here's a hack if (isFullscreen && (window.innerWidth !== window.screen.width || window.innerHeight !== window.screen.height)) { @@ -114,28 +141,40 @@ module.exports = function (opts) { } } setInterval(checkForFullscreen, 500) + } - // (why are the buttons anchors?) - let button = mk('a') - button.href = '#' - button.addEventListener('click', e => { - e.preventDefault() + // (why are the buttons anchors?) + let button = mk('a') + button.id = 'fullscreen-button' + button.href = '#' + button.addEventListener('click', e => { + e.preventDefault() - isFullscreen = true + if (document.body.classList.contains('pseudo-fullscreen')) { + document.body.classList.remove('pseudo-fullscreen') + isFullscreen = false fitScreenIfNeeded() - screen.updateSize() + return + } + isFullscreen = true + fitScreenIfNeeded() + screen.updateSize() + + if (properFullscreen) { if (screen.canvas.requestFullscreen) screen.canvas.requestFullscreen() else screen.canvas.webkitRequestFullscreen() - }) - let icon = mk('i') - icon.classList.add('icn-resize-full') // TODO: less confusing icons - button.appendChild(icon) - let span = mk('span') - span.textContent = 'Fullscreen' - button.appendChild(span) - qs('#term-nav').insertBefore(button, qs('#term-nav').firstChild) - } + } else { + document.body.classList.add('pseudo-fullscreen') + } + }) + fullscreenIcon = mk('i') + fullscreenIcon.className = 'icn-resize-full' + button.appendChild(fullscreenIcon) + let span = mk('span') + span.textContent = localize('term_nav.fullscreen') + button.appendChild(span) + qs('#term-nav').insertBefore(button, qs('#term-nav').firstChild) // for debugging window.termScreen = screen diff --git a/js/term/input.js b/js/term/input.js index c0649d0..1f319c7 100644 --- a/js/term/input.js +++ b/js/term/input.js @@ -242,8 +242,8 @@ module.exports = function (conn, screen) { } /** Send a button event */ - function sendButton (n) { - conn.send('b' + String.fromCharCode(n)) + function sendButton (index) { + conn.send('b' + String.fromCharCode(index + 1)) } const shouldAcceptEvent = function () { @@ -354,13 +354,6 @@ module.exports = function (conn, screen) { function init (opts) { initKeys(opts) - // Button presses - $('#action-buttons button').forEach(s => { - s.addEventListener('click', function (evt) { - sendButton(+this.dataset['n']) - }) - }) - // global mouse state tracking - for motion reporting window.addEventListener('mousedown', evt => { if (evt.button === 0) mb1 = 1 @@ -404,6 +397,7 @@ module.exports = function (conn, screen) { /** Send a literal string message */ sendString, + sendButton, /** Enable alternate key modes (cursors, numpad, fn) */ setAlts: function (cu, np, fn, crlf) { diff --git a/js/term/screen.js b/js/term/screen.js index b24ce98..4a28355 100644 --- a/js/term/screen.js +++ b/js/term/screen.js @@ -55,6 +55,7 @@ module.exports = class TermScreen extends EventEmitter { devicePixelRatio: 1, fontFamily: '"DejaVu Sans Mono", "Liberation Mono", "Inconsolata", "Menlo", monospace', fontSize: 20, + padding: 6, gridScaleX: 1.0, gridScaleY: 1.2, fitIntoWidth: 0, @@ -67,11 +68,15 @@ module.exports = class TermScreen extends EventEmitter { // scaling caused by fitIntoWidth/fitIntoHeight this._windowScale = 1 + // actual padding, as it may be disabled by fullscreen mode etc. + this._padding = 0 + // properties of this.window that require updating size and redrawing this.windowState = { width: 0, height: 0, devicePixelRatio: 0, + padding: 0, gridScaleX: 0, gridScaleY: 0, fontFamily: '', @@ -98,10 +103,12 @@ module.exports = class TermScreen extends EventEmitter { const self = this this.window = new Proxy(this._window, { set (target, key, value, receiver) { - target[key] = value - self.scheduleSizeUpdate() - self.renderer.scheduleDraw(`window:${key}=${value}`) - self.emit(`update-window:${key}`, value) + if (target[key] !== value) { + target[key] = value + self.scheduleSizeUpdate() + self.renderer.scheduleDraw(`window:${key}=${value}`) + self.emit(`update-window:${key}`, value) + } return true } }) @@ -215,7 +222,7 @@ module.exports = class TermScreen extends EventEmitter { selectionPos[1]}px)` } - if (!touchDidMove) { + if (!touchDidMove && !this.mouseMode.clicks) { this.emit('tap', Object.assign(e, { x: touchPosition[0], y: touchPosition[1] @@ -261,10 +268,20 @@ module.exports = class TermScreen extends EventEmitter { } }) + let aggregateWheelDelta = 0 this.canvas.addEventListener('wheel', e => { if (this.mouseMode.clicks) { - this.input.onMouseWheel(...this.screenToGrid(e.offsetX, e.offsetY), - e.deltaY > 0 ? 1 : -1) + if (Math.abs(e.wheelDeltaY) === 120) { + // mouse wheel scrolling + this.input.onMouseWheel(...this.screenToGrid(e.offsetX, e.offsetY), e.deltaY > 0 ? 1 : -1) + } else { + // smooth scrolling + aggregateWheelDelta -= e.wheelDeltaY + if (Math.abs(aggregateWheelDelta) >= 40) { + this.input.onMouseWheel(...this.screenToGrid(e.offsetX, e.offsetY), aggregateWheelDelta > 0 ? 1 : -1) + aggregateWheelDelta = 0 + } + } // prevent page scrolling e.preventDefault() @@ -312,10 +329,14 @@ module.exports = class TermScreen extends EventEmitter { screenToGrid (x, y, rounded = false) { let cellSize = this.getCellSize() - return [ - Math.floor((x + (rounded ? cellSize.width / 2 : 0)) / cellSize.width), - Math.floor(y / cellSize.height) - ] + x = x / this._windowScale - this._padding + y = y / this._windowScale - this._padding + x = Math.floor((x + (rounded ? cellSize.width / 2 : 0)) / cellSize.width) + y = Math.floor(y / cellSize.height) + x = Math.max(0, Math.min(this.window.width - 1, x)) + y = Math.max(0, Math.min(this.window.height - 1, y)) + + return [x, y] } /** @@ -328,7 +349,7 @@ module.exports = class TermScreen extends EventEmitter { gridToScreen (x, y, withScale = false) { let cellSize = this.getCellSize() - return [x * cellSize.width, y * cellSize.height].map(v => withScale ? v * this._windowScale : v) + return [x * cellSize.width, y * cellSize.height].map(v => this._padding + (withScale ? v * this._windowScale : v)) } /** @@ -363,7 +384,7 @@ module.exports = class TermScreen extends EventEmitter { */ updateSize () { // see below (this is just updating it) - this._window.devicePixelRatio = Math.round(this._windowScale * (window.devicePixelRatio || 1) * 2) / 2 + this._window.devicePixelRatio = Math.ceil(this._windowScale * (window.devicePixelRatio || 1)) let didChange = false for (let key in this.windowState) { @@ -378,13 +399,15 @@ module.exports = class TermScreen extends EventEmitter { width, height, fitIntoWidth, - fitIntoHeight + fitIntoHeight, + padding } = this.window const cellSize = this.getCellSize() // real height of the canvas element in pixels let realWidth = width * cellSize.width let realHeight = height * cellSize.height + let originalWidth = realWidth if (fitIntoWidth && fitIntoHeight) { let terminalAspect = realWidth / realHeight @@ -392,30 +415,30 @@ module.exports = class TermScreen extends EventEmitter { if (terminalAspect < fitAspect) { // align heights - realHeight = fitIntoHeight + realHeight = fitIntoHeight - 2 * padding realWidth = realHeight * terminalAspect } else { // align widths - realWidth = fitIntoWidth + realWidth = fitIntoWidth - 2 * padding realHeight = realWidth / terminalAspect } - } else if (fitIntoWidth) { - realHeight = fitIntoWidth / (realWidth / realHeight) - realWidth = fitIntoWidth - } else if (fitIntoHeight) { - realWidth = fitIntoHeight * (realWidth / realHeight) - realHeight = fitIntoHeight } // store new window scale - this._windowScale = realWidth / (width * cellSize.width) + this._windowScale = realWidth / originalWidth + + realWidth += 2 * padding + realHeight += 2 * padding + + // store padding + this._padding = padding * (originalWidth / realWidth) // the DPR must be rounded to a very nice value to prevent gaps between cells - let devicePixelRatio = this._window.devicePixelRatio = Math.round(this._windowScale * (window.devicePixelRatio || 1) * 2) / 2 + let devicePixelRatio = this._window.devicePixelRatio = Math.ceil(this._windowScale * (window.devicePixelRatio || 1)) - this.canvas.width = width * devicePixelRatio * cellSize.width + this.canvas.width = (width * cellSize.width + 2 * Math.round(this._padding)) * devicePixelRatio this.canvas.style.width = `${realWidth}px` - this.canvas.height = height * devicePixelRatio * cellSize.height + this.canvas.height = (height * cellSize.height + 2 * Math.round(this._padding)) * devicePixelRatio this.canvas.style.height = `${realHeight}px` // the screen has been cleared (by changing canvas width) diff --git a/js/term/screen_parser.js b/js/term/screen_parser.js index c210e00..1e9f197 100644 --- a/js/term/screen_parser.js +++ b/js/term/screen_parser.js @@ -1,13 +1,56 @@ const $ = require('../lib/chibi') const { qs } = require('../utils') -const { themes } = require('./themes') // constants for decoding the update blob +const SEQ_SKIP = 1 const SEQ_REPEAT = 2 const SEQ_SET_COLORS = 3 const SEQ_SET_ATTRS = 4 const SEQ_SET_FG = 5 const SEQ_SET_BG = 6 +const SEQ_SET_ATTR_0 = 7 + +function du (str) { + let num = str.codePointAt(0) + if (num > 0xDFFF) num -= 0x800 + return num - 1 +} + +/* eslint-disable no-multi-spaces */ +const TOPIC_SCREEN_OPTS = 'O' +const TOPIC_CONTENT = 'S' +const TOPIC_TITLE = 'T' +const TOPIC_BUTTONS = 'B' +const TOPIC_CURSOR = 'C' +const TOPIC_INTERNAL = 'D' +const TOPIC_BELL = '!' + +const OPT_CURSOR_VISIBLE = (1 << 0) +const OPT_DEBUGBAR = (1 << 1) +const OPT_CURSORS_ALT_MODE = (1 << 2) +const OPT_NUMPAD_ALT_MODE = (1 << 3) +const OPT_FN_ALT_MODE = (1 << 4) +const OPT_CLICK_TRACKING = (1 << 5) +const OPT_MOVE_TRACKING = (1 << 6) +const OPT_SHOW_BUTTONS = (1 << 7) +const OPT_SHOW_CONFIG_LINKS = (1 << 8) +// const OPT_CURSOR_SHAPE = (7 << 9) +const OPT_CRLF_MODE = (1 << 12) +const OPT_BRACKETED_PASTE = (1 << 13) +const OPT_REVERSE_VIDEO = (1 << 14) + +const ATTR_FG = (1 << 0) // 1 if not using default background color (ignore cell bg) - color extension bit +const ATTR_BG = (1 << 1) // 1 if not using default foreground color (ignore cell fg) - color extension bit +const ATTR_BOLD = (1 << 2) // Bold font +const ATTR_UNDERLINE = (1 << 3) // Underline decoration +const ATTR_INVERSE = (1 << 4) // Invert colors - this is useful so we can clear then with SGR manipulation commands +const ATTR_BLINK = (1 << 5) // Blinking +const ATTR_ITALIC = (1 << 6) // Italic font +const ATTR_STRIKE = (1 << 7) // Strike-through decoration +const ATTR_OVERLINE = (1 << 8) // Over-line decoration +const ATTR_FAINT = (1 << 9) // Faint foreground color (reduced alpha) +const ATTR_FRAKTUR = (1 << 10) // Fraktur font (unicode substitution) +/* eslint-enable no-multi-spaces */ module.exports = class ScreenParser { constructor (screen) { @@ -16,246 +59,341 @@ module.exports = class ScreenParser { // true if TermScreen#load was called at least once this.contentLoaded = false } + /** - * Parses the content of an `S` message and schedules a draw - * @param {string} str - the message content + * Hide the warning message about failed data load */ - loadContent (str) { - // current index - let i = 0 - let strArray = Array.from ? Array.from(str) : str.split('') - - // Uncomment to capture screen content for the demo page - // console.log(JSON.stringify(`S${str}`)) - + hideLoadFailedMsg () { if (!this.contentLoaded) { let errmsg = qs('#load-failed') if (errmsg) errmsg.parentNode.removeChild(errmsg) this.contentLoaded = true } + } - // window size - const newHeight = strArray[i++].codePointAt(0) - 1 - const newWidth = strArray[i++].codePointAt(0) - 1 - const resized = (this.screen.window.height !== newHeight) || (this.screen.window.width !== newWidth) - this.screen.window.height = newHeight - this.screen.window.width = newWidth - - // cursor position - let [cursorY, cursorX] = [ - strArray[i++].codePointAt(0) - 1, - strArray[i++].codePointAt(0) - 1 - ] - let cursorMoved = (cursorX !== this.screen.cursor.x || cursorY !== this.screen.cursor.y) - this.screen.cursor.x = cursorX - this.screen.cursor.y = cursorY - - if (cursorMoved) { - this.screen.renderer.resetCursorBlink() - this.screen.emit('cursor-moved') - } + loadUpdate (str) { + // console.log(`update ${str}`) + // current index + let ci = 0 + let strArray = Array.from ? Array.from(str) : str.split('') - // attributes - let attributes = strArray[i++].codePointAt(0) - 1 + let text + let resized = false + const topics = du(strArray[ci++]) + // this.screen.cursor.hanging = !!(attributes & (1 << 1)) + + while (ci < strArray.length) { + const topic = strArray[ci++] + + if (topic === TOPIC_SCREEN_OPTS) { + const newHeight = du(strArray[ci++]) + const newWidth = du(strArray[ci++]) + const theme = du(strArray[ci++]) + const defFg = (du(strArray[ci++]) & 0xFFFF) | ((du(strArray[ci++]) & 0xFFFF) << 16) + const defBg = (du(strArray[ci++]) & 0xFFFF) | ((du(strArray[ci++]) & 0xFFFF) << 16) + const attributes = du(strArray[ci++]) + + // theming + this.screen.renderer.loadTheme(theme) + this.screen.renderer.setDefaultColors(defFg, defBg) + + // apply size + resized = (this.screen.window.height !== newHeight) || (this.screen.window.width !== newWidth) + this.screen.window.height = newHeight + this.screen.window.width = newWidth + + // process attributes + this.screen.cursor.visible = !!(attributes & OPT_CURSOR_VISIBLE) + + this.screen.input.setAlts( + !!(attributes & OPT_CURSORS_ALT_MODE), + !!(attributes & OPT_NUMPAD_ALT_MODE), + !!(attributes & OPT_FN_ALT_MODE), + !!(attributes & OPT_CRLF_MODE) + ) - this.screen.cursor.visible = !!(attributes & 1) - this.screen.cursor.hanging = !!(attributes & (1 << 1)) + const trackMouseClicks = !!(attributes & OPT_CLICK_TRACKING) + const trackMouseMovement = !!(attributes & OPT_MOVE_TRACKING) + + // 0 - Block blink 2 - Block steady (1 is unused) + // 3 - Underline blink 4 - Underline steady + // 5 - I-bar blink 6 - I-bar steady + let cursorShape = (attributes >> 9) & 0x07 + // if it's not zero, decrement such that the two most significant bits + // are the type and the least significant bit is the blink state + if (cursorShape > 0) cursorShape-- + const cursorStyle = cursorShape >> 1 + const cursorBlinking = !(cursorShape & 1) + if (cursorStyle === 0) this.screen.cursor.style = 'block' + else if (cursorStyle === 1) this.screen.cursor.style = 'line' + else if (cursorStyle === 2) this.screen.cursor.style = 'bar' + if (this.screen.cursor.blinking !== cursorBlinking) { + this.screen.cursor.blinking = cursorBlinking + this.screen.renderer.resetCursorBlink() + } - this.screen.input.setAlts( - !!(attributes & (1 << 2)), // cursors alt - !!(attributes & (1 << 3)), // numpad alt - !!(attributes & (1 << 4)), // fn keys alt - !!(attributes & (1 << 12)) // crlf mode - ) + this.screen.input.setMouseMode(trackMouseClicks, trackMouseMovement) + this.screen.selection.selectable = !trackMouseClicks && !trackMouseMovement + $(this.screen.canvas).toggleClass('selectable', this.screen.selection.selectable) + this.screen.mouseMode = { + clicks: trackMouseClicks, + movement: trackMouseMovement + } - let trackMouseClicks = !!(attributes & (1 << 5)) - let trackMouseMovement = !!(attributes & (1 << 6)) + const showButtons = !!(attributes & OPT_SHOW_BUTTONS) + const showConfigLinks = !!(attributes & OPT_SHOW_CONFIG_LINKS) - // 0 - Block blink 2 - Block steady (1 is unused) - // 3 - Underline blink 4 - Underline steady - // 5 - I-bar blink 6 - I-bar steady - let cursorShape = (attributes >> 9) & 0x07 + $('.x-term-conf-btn').toggleClass('hidden', !showConfigLinks) + $('#action-buttons').toggleClass('hidden', !showButtons) - // if it's not zero, decrement such that the two most significant bits - // are the type and the least significant bit is the blink state - if (cursorShape > 0) cursorShape-- + this.screen.bracketedPaste = !!(attributes & OPT_BRACKETED_PASTE) + this.screen.reverseVideo = !!(attributes & OPT_REVERSE_VIDEO) - let cursorStyle = cursorShape >> 1 - let cursorBlinking = !(cursorShape & 1) + const debugbar = !!(attributes & OPT_DEBUGBAR) + // TODO do something with debugbar - if (cursorStyle === 0) this.screen.cursor.style = 'block' - else if (cursorStyle === 1) this.screen.cursor.style = 'line' - else if (cursorStyle === 2) this.screen.cursor.style = 'bar' + } else if (topic === TOPIC_CURSOR) { - if (this.screen.cursor.blinking !== cursorBlinking) { - this.screen.cursor.blinking = cursorBlinking - this.screen.renderer.resetCursorBlink() - } + // cursor position + const cursorY = du(strArray[ci++]) + const cursorX = du(strArray[ci++]) + const hanging = du(strArray[ci++]) - this.screen.input.setMouseMode(trackMouseClicks, trackMouseMovement) - this.screen.selection.selectable = !trackMouseClicks && !trackMouseMovement - $(this.screen.canvas).toggleClass('selectable', this.screen.selection.selectable) - this.screen.mouseMode = { - clicks: trackMouseClicks, - movement: trackMouseMovement - } + const cursorMoved = ( + hanging !== this.screen.cursor.hanging || + cursorX !== this.screen.cursor.x || + cursorY !== this.screen.cursor.y) - let showButtons = !!(attributes & (1 << 7)) - let showConfigLinks = !!(attributes & (1 << 8)) - - $('.x-term-conf-btn').toggleClass('hidden', !showConfigLinks) - $('#action-buttons').toggleClass('hidden', !showButtons) - - this.screen.bracketedPaste = !!(attributes & (1 << 13)) - this.screen.reverseVideo = !!(attributes & (1 << 14)) - - // content - let fg = 7 - let bg = 0 - let attrs = 0 - let cell = 0 // cell index - let lastChar = ' ' - let screenLength = this.screen.window.width * this.screen.window.height - - if (resized) { - this.screen.updateSize() - this.screen.blinkingCellCount = 0 - this.screen.screen = new Array(screenLength).fill(' ') - this.screen.screenFG = new Array(screenLength).fill(' ') - this.screen.screenBG = new Array(screenLength).fill(' ') - this.screen.screenAttrs = new Array(screenLength).fill(0) - } + this.screen.cursor.x = cursorX + this.screen.cursor.y = cursorY - const MASK_LINE_ATTR = 0xC8 - const MASK_BLINK = 1 << 4 - - let setCellContent = () => { - // Remove blink attribute if it wouldn't have any effect - let myAttrs = attrs - let hasFG = attrs & (1 << 8) - let hasBG = attrs & (1 << 9) - if ((myAttrs & MASK_BLINK) !== 0 && - ((lastChar === ' ' && ((myAttrs & MASK_LINE_ATTR) === 0)) || // no line styles - (fg === bg && hasFG && hasBG) // invisible text - ) - ) { - myAttrs ^= MASK_BLINK - } - // update blinking cells counter if blink state changed - if ((this.screen.screenAttrs[cell] & MASK_BLINK) !== (myAttrs & MASK_BLINK)) { - if (myAttrs & MASK_BLINK) this.screen.blinkingCellCount++ - else this.screen.blinkingCellCount-- - } + this.screen.cursor.hanging = !!hanging - this.screen.screen[cell] = lastChar - this.screen.screenFG[cell] = fg - this.screen.screenBG[cell] = bg - this.screen.screenAttrs[cell] = myAttrs - } + if (cursorMoved) { + this.screen.renderer.resetCursorBlink() + this.screen.emit('cursor-moved') + } - while (i < strArray.length && cell < screenLength) { - let character = strArray[i++] - let charCode = character.codePointAt(0) - - let data - switch (charCode) { - case SEQ_REPEAT: - let count = strArray[i++].codePointAt(0) - 1 - for (let j = 0; j < count; j++) { - setCellContent() - if (++cell > screenLength) break + this.screen.renderer.scheduleDraw('cursor-moved') + } else if (topic === TOPIC_TITLE) { + + // TODO optimize this + text = '' + while (ci < strArray.length) { + let c = strArray[ci++] + if (c !== '\x01') { + text += c + } else { + break } - break - - case SEQ_SET_COLORS: - data = strArray[i++].codePointAt(0) - 1 - fg = data & 0xFF - bg = (data >> 8) & 0xFF - break - - case SEQ_SET_ATTRS: - data = strArray[i++].codePointAt(0) - 1 - attrs = data & 0xFFFF - break - - case SEQ_SET_FG: - data = strArray[i++].codePointAt(0) - 1 - fg = data & 0xFF - break - - case SEQ_SET_BG: - data = strArray[i++].codePointAt(0) - 1 - bg = data & 0xFF - break - - default: - if (charCode < 32) character = '\ufffd' - lastChar = character - setCellContent() - cell++ - } - } + } - if (this.screen.window.debug) console.log(`Blinky cells: ${this.screen.blinkingCellCount}`) + qs('#screen-title').textContent = text + if (text.length === 0) text = 'Terminal' + qs('title').textContent = `${text} :: ESPTerm` - this.screen.renderer.scheduleDraw('load', 16) - this.screen.conn.emit('load') - } + } else if (topic === TOPIC_BUTTONS) { + const count = du(strArray[ci++]) - /** - * Parses the content of a `T` message and updates the screen title and button - * labels. - * @param {string} str - the message content - */ - loadLabels (str) { - let pieces = str.split('\x01') - let screenTitle = pieces[0] - qs('#screen-title').textContent = screenTitle - if (screenTitle.length === 0) screenTitle = 'Terminal' - qs('title').textContent = `${screenTitle} :: ESPTerm` - $('#action-buttons button').forEach((button, i) => { - let label = pieces[i + 1].trim() - // if empty string, use the "dim" effect and put nbsp instead to - // stretch the button vertically - button.innerHTML = label ? $.htmlEscape(label) : ' ' - button.style.opacity = label ? 1 : 0.2 - }) + let labels = [] + for (let j = 0; j < count; j++) { + text = '' + while (ci < strArray.length) { + let c = strArray[ci++] + if (c === '\x01') break + text += c + } + labels.push(text) + } + + this.screen.emit('button-labels', labels) + } else if (topic === TOPIC_BELL) { + + this.screen.beep() + + } else if (topic === TOPIC_INTERNAL) { + // debug info + + const flags = du(strArray[ci++]) + const cursorAttrs = du(strArray[ci++]) + const regionStart = du(strArray[ci++]) + const regionEnd = du(strArray[ci++]) + const charsetGx = du(strArray[ci++]) + const charsetG0 = strArray[ci++] + const charsetG1 = strArray[ci++] + const freeHeap = du(strArray[ci++]) + const clientCount = du(strArray[ci++]) + + this.screen.emit('internal', { + flags, + cursorAttrs, + regionStart, + regionEnd, + charsetGx, + charsetG0, + charsetG1, + freeHeap, + clientCount + }) + } else if (topic === TOPIC_CONTENT) { + // set screen content + + const frameY = du(strArray[ci++]) + const frameX = du(strArray[ci++]) + const frameHeight = du(strArray[ci++]) + const frameWidth = du(strArray[ci++]) + + if (this.screen._debug && this.screen.window.debug) { + this.screen._debug.pushFrame([frameX, frameY, frameWidth, frameHeight]) + } + + // content + let fg = 7 + let bg = 0 + let attrs = 0 + let cell = 0 // cell index + let lastChar = ' ' + let frameLength = frameWidth * frameHeight + let screenLength = this.screen.window.width * this.screen.window.height + + if (resized) { + this.screen.updateSize() + this.screen.blinkingCellCount = 0 + this.screen.screen = new Array(screenLength).fill(' ') + this.screen.screenFG = new Array(screenLength).fill(' ') + this.screen.screenBG = new Array(screenLength).fill(' ') + this.screen.screenAttrs = new Array(screenLength).fill(0) + } + + const MASK_LINE_ATTR = ATTR_UNDERLINE | ATTR_OVERLINE | ATTR_STRIKE + const MASK_BLINK = ATTR_BLINK + + let pushCell = () => { + // Remove blink attribute if it wouldn't have any effect + let myAttrs = attrs + let hasFG = attrs & ATTR_FG + let hasBG = attrs & ATTR_BG + let cellFG = fg + let cellBG = bg + + // use 0,0 if no fg/bg. this is to match back-end implementation + // and allow leaving out fg/bg setting for cells with none + if (!hasFG) cellFG = 0 + if (!hasBG) cellBG = 0 + + if ((myAttrs & MASK_BLINK) !== 0 && + ((lastChar === ' ' && ((myAttrs & MASK_LINE_ATTR) === 0)) || // no line styles + (fg === bg && hasFG && hasBG) // invisible text + ) + ) { + myAttrs ^= MASK_BLINK + } + // update blinking cells counter if blink state changed + if ((this.screen.screenAttrs[cell] & MASK_BLINK) !== (myAttrs & MASK_BLINK)) { + if (myAttrs & MASK_BLINK) this.screen.blinkingCellCount++ + else this.screen.blinkingCellCount-- + } + + let cellXInFrame = cell % frameWidth + let cellYInFrame = Math.floor(cell / frameWidth) + let index = (frameY + cellYInFrame) * this.screen.window.width + frameX + cellXInFrame + + // 8 dark system colors turn bright when bold + if ((myAttrs & ATTR_BOLD) && !(myAttrs & ATTR_FAINT) && hasFG && cellFG < 8) { + cellFG += 8 + } + + this.screen.screen[index] = lastChar + this.screen.screenFG[index] = cellFG + this.screen.screenBG[index] = cellBG + this.screen.screenAttrs[index] = myAttrs + } + + while (ci < strArray.length && cell < frameLength) { + let character = strArray[ci++] + let charCode = character.codePointAt(0) + + let data, count + switch (charCode) { + case SEQ_REPEAT: + count = du(strArray[ci++]) + for (let j = 0; j < count; j++) { + pushCell() + if (++cell > frameLength) break + } + break + + case SEQ_SKIP: + cell += du(strArray[ci++]) + break + + case SEQ_SET_COLORS: + data = du(strArray[ci++]) + fg = data & 0xFF + bg = (data >> 8) & 0xFF + break + + case SEQ_SET_ATTRS: + data = du(strArray[ci++]) + attrs = data & 0xFFFF + break + + case SEQ_SET_ATTR_0: + attrs = 0 + break + + case SEQ_SET_FG: + data = du(strArray[ci++]) + if (data & 0x10000) { + data &= 0xFFF + data |= (du(strArray[ci++]) & 0xFFF) << 12 + data += 256 + } + fg = data + break + + case SEQ_SET_BG: + data = du(strArray[ci++]) + if (data & 0x10000) { + data &= 0xFFF + data |= (du(strArray[ci++]) & 0xFFF) << 12 + data += 256 + } + bg = data + break + + default: + if (charCode < 32) character = '\ufffd' + lastChar = character + pushCell() + cell++ + } + } + + if (this.screen.window.debug) console.log(`Blinky cells: ${this.screen.blinkingCellCount}`) + + this.screen.renderer.scheduleDraw('load', 16) + this.screen.conn.emit('load') + + } + + if ((topics & 0x3B) !== 0) this.hideLoadFailedMsg() + } } /** * Loads a message from the server, and optionally a theme. * @param {string} str - the message - * @param {object} [opts] - options - * @param {number} [opts.theme] - theme - * @param {number} [opts.defaultFg] - default foreground - * @param {number} [opts.defaultBg] - default background */ - load (str, opts = null) { + load (str) { const content = str.substr(1) - if (opts) { - if (typeof opts.defaultFg !== 'undefined' && typeof opts.defaultBg !== 'undefined') { - this.screen.renderer.setDefaultColors(opts.defaultFg, opts.defaultBg) - } - - if (typeof opts.theme !== 'undefined') { - if (opts.theme >= 0 && opts.theme < themes.length) { - this.screen.renderer.palette = themes[opts.theme] - } - } - } + // This is a good place for debugging the message + // console.log(str) switch (str[0]) { - case 'S': - this.loadContent(content) - break - - case 'T': - this.loadLabels(content) - break - - case 'B': - this.screen.beep() + case 'U': + this.loadUpdate(content) break case 'G': diff --git a/js/term/screen_renderer.js b/js/term/screen_renderer.js index a557351..270e484 100644 --- a/js/term/screen_renderer.js +++ b/js/term/screen_renderer.js @@ -9,6 +9,21 @@ const frakturExceptions = { 'Z': '\u2128' } +// TODO do not repeat - this is also defined in screen_parser ... +/* eslint-disable no-multi-spaces */ +const ATTR_FG = (1 << 0) // 1 if not using default background color (ignore cell bg) - color extension bit +const ATTR_BG = (1 << 1) // 1 if not using default foreground color (ignore cell fg) - color extension bit +const ATTR_BOLD = (1 << 2) // Bold font +const ATTR_UNDERLINE = (1 << 3) // Underline decoration +const ATTR_INVERSE = (1 << 4) // Invert colors - this is useful so we can clear then with SGR manipulation commands +const ATTR_BLINK = (1 << 5) // Blinking +const ATTR_ITALIC = (1 << 6) // Italic font +const ATTR_STRIKE = (1 << 7) // Strike-through decoration +const ATTR_OVERLINE = (1 << 8) // Over-line decoration +const ATTR_FAINT = (1 << 9) // Faint foreground color (reduced alpha) +const ATTR_FRAKTUR = (1 << 10) // Fraktur font (unicode substitution) +/* eslint-enable no-multi-spaces */ + module.exports = class ScreenRenderer { constructor (screen) { this.screen = screen @@ -37,6 +52,9 @@ module.exports = class ScreenRenderer { resetDrawn () { // used to determine if a cell should be redrawn; storing the current state // as it is on screen + if (this.screen.window && this.screen.window.debug) { + console.log('Resetting drawn screen') + } this.drawnScreen = [] this.drawnScreenFG = [] this.drawnScreenBG = [] @@ -61,11 +79,17 @@ module.exports = class ScreenRenderer { } } + loadTheme (i) { + if (i in themes) this.palette = themes[i] + } + setDefaultColors (fg, bg) { - this.defaultFgNum = fg - this.defaultBgNum = bg - this.resetDrawn() - this.scheduleDraw('defaultColors') + if (fg !== this.defaultFgNum || bg !== this.defaultBgNum) { + this.resetDrawn() + this.defaultFgNum = fg + this.defaultBgNum = bg + this.scheduleDraw('default-colors') + } } /** @@ -159,9 +183,27 @@ module.exports = class ScreenRenderer { */ drawBackground ({ x, y, cellWidth, cellHeight, bg }) { const ctx = this.ctx + const { width, height } = this.screen.window + const padding = Math.round(this.screen._padding) ctx.fillStyle = this.getColor(bg) - ctx.clearRect(x * cellWidth, y * cellHeight, cellWidth, cellHeight) - ctx.fillRect(x * cellWidth, y * cellHeight, cellWidth, cellHeight) + let screenX = x * cellWidth + padding + let screenY = y * cellHeight + padding + let isBorderCell = x === 0 || y === 0 || x === width - 1 || y === height - 1 + if (isBorderCell) { + let left = screenX + let top = screenY + let right = screenX + cellWidth + let bottom = screenY + cellHeight + if (x === 0) left -= padding + else if (x === width - 1) right += padding + if (y === 0) top -= padding + else if (y === height - 1) bottom += padding + ctx.clearRect(left, top, right - left, bottom - top) + ctx.fillRect(left, top, right - left, bottom - top) + } else { + ctx.clearRect(screenX, screenY, cellWidth, cellHeight) + ctx.fillRect(screenX, screenY, cellWidth, cellHeight) + } } /** @@ -182,24 +224,28 @@ module.exports = class ScreenRenderer { if (!text) return const ctx = this.ctx + const padding = Math.round(this.screen._padding) let underline = false let strike = false let overline = false - if (attrs & (1 << 1)) ctx.globalAlpha = 0.5 - if (attrs & (1 << 3)) underline = true - if (attrs & (1 << 5)) text = ScreenRenderer.alphaToFraktur(text) - if (attrs & (1 << 6)) strike = true - if (attrs & (1 << 7)) overline = true + if (attrs & ATTR_FAINT) ctx.globalAlpha = 0.5 + if (attrs & ATTR_UNDERLINE) underline = true + if (attrs & ATTR_FRAKTUR) text = ScreenRenderer.alphaToFraktur(text) + if (attrs & ATTR_STRIKE) strike = true + if (attrs & ATTR_OVERLINE) overline = true ctx.fillStyle = this.getColor(fg) + let screenX = x * cellWidth + padding + let screenY = y * cellHeight + padding + let codePoint = text.codePointAt(0) if (codePoint >= 0x2580 && codePoint <= 0x259F) { // block elements ctx.beginPath() - const left = x * cellWidth - const top = y * cellHeight + const left = screenX + const top = screenY const cw = cellWidth const ch = cellHeight const c2w = cellWidth / 2 @@ -251,16 +297,16 @@ module.exports = class ScreenRenderer { for (let dx = 0; dx < cw; dx += dotSpacingX) { // prevent overflow let dotSizeY = Math.min(dotSize, ch - dy) - ctx.rect(x * cw + (alignRight ? cw - dx - dotSize : dx), y * ch + dy, dotSize, dotSizeY) + ctx.rect(left + (alignRight ? cw - dx - dotSize : dx), top + dy, dotSize, dotSizeY) } alignRight = !alignRight } } else if (codePoint === 0x2594) { // upper one eighth block >▔< - ctx.rect(x * cw, y * ch, cw, ch / 8) + ctx.rect(left, top, cw, ch / 8) } else if (codePoint === 0x2595) { // right one eighth block >▕< - ctx.rect((x + 7 / 8) * cw, y * ch, cw / 8, ch) + ctx.rect(left + (7 / 8) * cw, top, cw / 8, ch) } else if (codePoint === 0x2596) { // left bottom quadrant >▖< ctx.rect(left, top + c2h, c2w, c2h) @@ -300,9 +346,33 @@ module.exports = class ScreenRenderer { } ctx.fill() + } else if (codePoint >= 0xE0B0 && codePoint <= 0xE0B3) { + // powerline symbols, except branch, line, and lock. Basically, just the triangles + ctx.beginPath() + + if (codePoint === 0xE0B0 || codePoint === 0xE0B1) { + // right-pointing triangle + ctx.moveTo(screenX, screenY) + ctx.lineTo(screenX + cellWidth, screenY + cellHeight / 2) + ctx.lineTo(screenX, screenY + cellHeight) + } else if (codePoint === 0xE0B2 || codePoint === 0xE0B3) { + // left-pointing triangle + ctx.moveTo(screenX + cellWidth, screenY) + ctx.lineTo(screenX, screenY + cellHeight / 2) + ctx.lineTo(screenX + cellWidth, screenY + cellHeight) + } + + if (codePoint % 2 === 0) { + // triangle + ctx.fill() + } else { + // chevron + ctx.strokeStyle = ctx.fillStyle + ctx.stroke() + } } else { // Draw other characters using the text renderer - ctx.fillText(text, (x + 0.5) * cellWidth, (y + 0.5) * cellHeight) + ctx.fillText(text, screenX + 0.5 * cellWidth, screenY + 0.5 * cellHeight) } // -- line drawing - a reference for a possible future rect/line implementation --- @@ -324,21 +394,21 @@ module.exports = class ScreenRenderer { ctx.beginPath() if (underline) { - let lineY = Math.round(y * cellHeight + charSize.height) + 0.5 - ctx.moveTo(x * cellWidth, lineY) - ctx.lineTo((x + 1) * cellWidth, lineY) + let lineY = Math.round(screenY + charSize.height) + 0.5 + ctx.moveTo(screenX, lineY) + ctx.lineTo(screenX + cellWidth, lineY) } if (strike) { - let lineY = Math.round((y + 0.5) * cellHeight) + 0.5 - ctx.moveTo(x * cellWidth, lineY) - ctx.lineTo((x + 1) * cellWidth, lineY) + let lineY = Math.round(screenY + 0.5 * cellHeight) + 0.5 + ctx.moveTo(screenX, lineY) + ctx.lineTo(screenX + cellWidth, lineY) } if (overline) { - let lineY = Math.round(y * cellHeight) + 0.5 - ctx.moveTo(x * cellWidth, lineY) - ctx.lineTo((x + 1) * cellWidth, lineY) + let lineY = Math.round(screenY) + 0.5 + ctx.moveTo(screenX, lineY) + ctx.lineTo(screenX + cellWidth, lineY) } ctx.stroke() @@ -402,7 +472,7 @@ module.exports = class ScreenRenderer { ctx.textBaseline = 'middle' // bits in the attr value that affect the font - const FONT_MASK = 0b101 + const FONT_MASK = ATTR_BOLD | ATTR_ITALIC // Map of (attrs & FONT_MASK) -> Array of cell indices let fontGroups = new Map() @@ -413,11 +483,10 @@ module.exports = class ScreenRenderer { for (let cell = 0; cell < screenLength; cell++) { let x = cell % width let y = Math.floor(cell / width) - let isCursor = !this.screen.cursor.hanging && + let isCursor = this.cursorBlinkOn && this.screen.cursor.x === x && this.screen.cursor.y === y && - this.screen.cursor.visible && - this.cursorBlinkOn + this.screen.cursor.visible let wasCursor = x === this.drawnCursor[0] && y === this.drawnCursor[1] @@ -428,13 +497,13 @@ module.exports = class ScreenRenderer { let bg = this.screen.screenBG[cell] | 0 let attrs = this.screen.screenAttrs[cell] | 0 - if (!(attrs & (1 << 8))) fg = this.defaultFgNum - if (!(attrs & (1 << 9))) bg = this.defaultBgNum + if (!(attrs & ATTR_FG)) fg = this.defaultFgNum + if (!(attrs & ATTR_BG)) bg = this.defaultBgNum - if (attrs & (1 << 10)) [fg, bg] = [bg, fg] // swap - reversed character colors + if (attrs & ATTR_INVERSE) [fg, bg] = [bg, fg] // swap - reversed character colors if (this.screen.reverseVideo) [fg, bg] = [bg, fg] // swap - reversed all screen - if (attrs & (1 << 4) && !this.blinkStyleOn) { + if (attrs & ATTR_BLINK && !this.blinkStyleOn) { // blinking is enabled and blink style is off // set text to nothing so drawCharacter doesn't draw anything text = '' @@ -450,7 +519,8 @@ module.exports = class ScreenRenderer { bg !== this.drawnScreenBG[cell] || // background updated attrs !== this.drawnScreenAttrs[cell] || // attributes updated isCursor !== wasCursor || // cursor blink/position updated - (isCursor && this.screen.cursor.style !== this.drawnCursor[2]) // cursor style updated + (isCursor && this.screen.cursor.style !== this.drawnCursor[2]) || // cursor style updated + (isCursor && this.screen.cursor.hanging !== this.drawnCursor[3]) // cursor hanging updated let font = attrs & FONT_MASK if (!fontGroups.has(font)) fontGroups.set(font, []) @@ -499,6 +569,7 @@ module.exports = class ScreenRenderer { // mask to redrawing regions only if (this.screen.window.graphics >= 1) { let debug = this.screen.window.debug && this.screen._debug + let padding = Math.round(this.screen._padding) ctx.save() ctx.beginPath() for (let y = 0; y < height; y++) { @@ -508,13 +579,13 @@ module.exports = class ScreenRenderer { let redrawing = redrawMap.get(cell) if (redrawing && regionStart === null) regionStart = x if (!redrawing && regionStart !== null) { - ctx.rect(regionStart * cellWidth, y * cellHeight, (x - regionStart) * cellWidth, cellHeight) + ctx.rect(padding + regionStart * cellWidth, padding + y * cellHeight, (x - regionStart) * cellWidth, cellHeight) if (debug) this.screen._debug.clipRect(regionStart * cellWidth, y * cellHeight, (x - regionStart) * cellWidth, cellHeight) regionStart = null } } if (regionStart !== null) { - ctx.rect(regionStart * cellWidth, y * cellHeight, (width - regionStart) * cellWidth, cellHeight) + ctx.rect(padding + regionStart * cellWidth, padding + y * cellHeight, (width - regionStart) * cellWidth, cellHeight) if (debug) this.screen._debug.clipRect(regionStart * cellWidth, y * cellHeight, (width - regionStart) * cellWidth, cellHeight) } } @@ -548,8 +619,8 @@ module.exports = class ScreenRenderer { // set font once because in Firefox, this is a really slow action for some // reason let modifiers = {} - if (font & 1) modifiers.weight = 'bold' - if (font & 1 << 2) modifiers.style = 'italic' + if (font & ATTR_BOLD) modifiers.weight = 'bold' + if (font & ATTR_ITALIC) modifiers.style = 'italic' ctx.font = this.screen.getFont(modifiers) for (let data of fontGroups.get(font)) { @@ -565,22 +636,34 @@ module.exports = class ScreenRenderer { this.drawnScreenBG[cell] = bg this.drawnScreenAttrs[cell] = attrs - if (isCursor) this.drawnCursor = [x, y, this.screen.cursor.style] + if (isCursor) this.drawnCursor = [x, y, this.screen.cursor.style, this.screen.cursor.hanging] + // draw cursor if (isCursor && !inSelection) { ctx.save() ctx.beginPath() + + let cursorX = x + let cursorY = y + + if (this.screen.cursor.hanging) { + // draw hanging cursor in the margin + cursorX += 1 + } + + let screenX = cursorX * cellWidth + this.screen._padding + let screenY = cursorY * cellHeight + this.screen._padding if (this.screen.cursor.style === 'block') { // block - ctx.rect(x * cellWidth, y * cellHeight, cellWidth, cellHeight) + ctx.rect(screenX, screenY, cellWidth, cellHeight) } else if (this.screen.cursor.style === 'bar') { // vertical bar let barWidth = 2 - ctx.rect(x * cellWidth, y * cellHeight, barWidth, cellHeight) + ctx.rect(screenX, screenY, barWidth, cellHeight) } else if (this.screen.cursor.style === 'line') { // underline let lineHeight = 2 - ctx.rect(x * cellWidth, y * cellHeight + charSize.height, cellWidth, lineHeight) + ctx.rect(screenX, screenY + charSize.height, cellWidth, lineHeight) } ctx.clip() @@ -590,9 +673,9 @@ module.exports = class ScreenRenderer { // HACK: ensure cursor is visible if (fg === bg) bg = fg === 0 ? 7 : 0 - this.drawBackground({ x, y, cellWidth, cellHeight, bg }) + this.drawBackground({ x: cursorX, y: cursorY, cellWidth, cellHeight, bg }) this.drawCharacter({ - x, y, charSize, cellWidth, cellHeight, text, fg, attrs + x: cursorX, y: cursorY, charSize, cellWidth, cellHeight, text, fg, attrs }) ctx.restore() } @@ -604,7 +687,7 @@ module.exports = class ScreenRenderer { if (this.screen.window.debug && this.screen._debug) this.screen._debug.drawEnd() - this.screen.emit('draw') + this.screen.emit('draw', why) } drawStatus (statusScreen) { @@ -620,14 +703,15 @@ module.exports = class ScreenRenderer { this.drawnScreen = [] const cellSize = this.screen.getCellSize() - const screenWidth = width * cellSize.width - const screenHeight = height * cellSize.height + const screenWidth = width * cellSize.width + 2 * this.screen._padding + const screenHeight = height * cellSize.height + 2 * this.screen._padding ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0) - ctx.clearRect(0, 0, screenWidth, screenHeight) + ctx.fillStyle = this.getColor(this.defaultBgNum) + ctx.fillRect(0, 0, screenWidth, screenHeight) ctx.font = `24px ${fontFamily}` - ctx.fillStyle = '#fff' + ctx.fillStyle = this.getColor(this.defaultFgNum) ctx.textAlign = 'center' ctx.textBaseline = 'middle' ctx.fillText(statusScreen.title || '', screenWidth / 2, screenHeight / 2 - 50) @@ -637,7 +721,7 @@ module.exports = class ScreenRenderer { ctx.save() ctx.translate(screenWidth / 2, screenHeight / 2 + 20) - ctx.strokeStyle = '#fff' + ctx.strokeStyle = this.getColor(this.defaultFgNum) ctx.lineWidth = 5 ctx.lineCap = 'round' diff --git a/js/term/soft_keyboard.js b/js/term/soft_keyboard.js index 55bfa82..9d89960 100644 --- a/js/term/soft_keyboard.js +++ b/js/term/soft_keyboard.js @@ -48,6 +48,7 @@ module.exports = function (screen, input) { // sends the difference between the last and the new composition string let sendInputDelta = function (newValue) { + if (newValue === null) newValue = '' // this sometimes happens, why? let resend = false if (newValue.length > lastCompositionString.length) { if (newValue.startsWith(lastCompositionString)) { diff --git a/js/term/themes.js b/js/term/themes.js index a37dab4..8fb47ac 100644 --- a/js/term/themes.js +++ b/js/term/themes.js @@ -1,58 +1,70 @@ +const $ = require('../lib/chibi') +const { rgb255ToHex } = require('../lib/color_utils') const themes = exports.themes = [ - [ // Tango + [ // 0 - Tango - terminator '#111213', '#CC0000', '#4E9A06', '#C4A000', '#3465A4', '#75507B', '#06989A', '#D3D7CF', '#555753', '#EF2929', '#8AE234', '#FCE94F', '#729FCF', '#AD7FA8', '#34E2E2', '#EEEEEC' ], - [ // Linux (CGA) + [ // 1 - Linux (CGA) - terminator '#000000', '#aa0000', '#00aa00', '#aa5500', '#0000aa', '#aa00aa', '#00aaaa', '#aaaaaa', '#555555', '#ff5555', '#55ff55', '#ffff55', '#5555ff', '#ff55ff', '#55ffff', '#ffffff' ], - [ // xterm + [ // 2 - xterm - terminator '#000000', '#cd0000', '#00cd00', '#cdcd00', '#0000ee', '#cd00cd', '#00cdcd', '#e5e5e5', '#7f7f7f', '#ff0000', '#00ff00', '#ffff00', '#5c5cff', '#ff00ff', '#00ffff', '#ffffff' ], - [ // rxvt + [ // 3 - rxvt - terminator '#000000', '#cd0000', '#00cd00', '#cdcd00', '#0000cd', '#cd00cd', '#00cdcd', '#faebd7', '#404040', '#ff0000', '#00ff00', '#ffff00', '#0000ff', '#ff00ff', '#00ffff', '#ffffff' ], - [ // Ambience + [ // 4 - Ambience - terminator '#2e3436', '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', '#d3d7cf', '#555753', '#ef2929', '#8ae234', '#fce94f', '#729fcf', '#ad7fa8', '#34e2e2', '#eeeeec' ], - [ // Solarized light + [ // 5 - Solarized Dark - terminator '#073642', '#dc322f', '#859900', '#b58900', '#268bd2', '#d33682', '#2aa198', '#eee8d5', '#002b36', '#cb4b16', '#586e75', '#657b83', '#839496', '#6c71c4', '#93a1a1', '#fdf6e3' ], - [ // CGA NTSC + [ // 6 - CGA NTSC - wikipedia '#000000', '#69001A', '#117800', '#769100', '#1A00A6', '#8019AB', '#289E76', '#A4A4A4', '#484848', '#C54E76', '#6DD441', '#D2ED46', '#765BFF', '#DC75FF', '#84FAD2', '#FFFFFF' ], - [ // ZX Spectrum + [ // 7 - ZX Spectrum - wikipedia '#000000', '#aa0000', '#00aa00', '#aaaa00', '#0000aa', '#aa00aa', '#00aaaa', '#aaaaaa', '#000000', '#ff0000', '#00FF00', '#ffff00', '#0000ff', '#ff00ff', '#00ffff', '#ffffff' ], - [ // Apple II + [ // 8 - Apple II - wikipedia '#000000', '#722640', '#0E5940', '#808080', '#40337F', '#E434FE', '#1B9AFE', '#BFB3FF', '#404C00', '#E46501', '#1BCB01', '#BFCC80', '#808080', '#F1A6BF', '#8DD9BF', '#ffffff' ], - [ // Commodore + [ // 9 - Commodore - wikipedia '#000000', '#8D3E37', '#55A049', '#AAB95D', '#40318D', '#80348B', '#72C1C8', '#D59F74', '#8B5429', '#B86962', '#94E089', '#FFFFB2', '#8071CC', '#AA5FB6', '#87D6DD', '#ffffff' + ], + [ // 10 - Solarized Light - https://github.com/sgerrand/xfce4-terminal-colors-solarized + '#eee8d5', '#dc322f', '#859900', '#b58900', '#268bd2', '#d33682', '#2aa198', '#073642', + '#fdf6e3', '#cb4b16', '#93a1a1', '#839496', '#657b83', '#6c71c4', '#586e75', '#002b36' + ], + [ // 11 - Solarized Dark High contrast - https://github.com/sgerrand/xfce4-terminal-colors-solarized + '#073642', '#dc322f', '#859900', '#b58900', '#268bd2', '#d33682', '#2aa198', '#fdf6e3', + '#002b36', '#cb4b16', '#657b83', '#839496', '#93a1a1', '#6c71c4', '#eee8d5', '#fdf6e3' ] ] exports.fgbgThemes = [ - ['#AAAAAA', '#000000'], // GREY_ON_BLACK - ['#EFF0F1', '#31363B'], // BREEZE - ['#FFFFFF', '#000000'], // WHITE_ON_BLACK - ['#00FF00', '#000000'], // GREEN_ON_BLACK - ['#E53C00', '#000000'], // ORANGE_ON_BLACK - ['#FFFFFF', '#300A24'], // AMBIENCE - ['#839496', '#002B36'], // SOLARIZED_DARK - ['#657B83', '#FDF6E3'], // SOLARIZED_LIGHT - ['#000000', '#FFFFDD'], // BLACK_ON_YELLOW - ['#000000', '#FFFFFF'] // BLACK_ON_WHITE + ['#AAAAAA', '#000000', 'Lnx', 'Linux'], + ['#FFFFFF', '#000000', 'W+K', 'White on Black'], + ['#00FF00', '#000000', 'Lim', 'Lime'], + ['#E53C00', '#000000', 'Nix', 'Nixie'], + ['#EFF0F1', '#31363B', 'Brz', 'Breeze'], + ['#FFFFFF', '#300A24', 'Amb', 'Ambiance'], + ['#839496', '#002B36', 'SoD', 'Solarized Dark'], + ['#93a1a1', '#002b36', 'SoH', 'Solarized Dark (High Contrast)'], + ['#657B83', '#FDF6E3', 'SoL', 'Solarized Light'], + ['#000000', '#FFD75F', 'Wsp', 'Wasp'], + ['#000000', '#FFFFDD', 'K+Y', 'Black on Yellow'], + ['#000000', '#FFFFFF', 'K+W', 'Black on White'] ] let colorTable256 = null @@ -62,7 +74,7 @@ exports.buildColorTable = function () { // 256color lookup table // should not be used to look up 0-15 (will return transparent) - colorTable256 = new Array(16).fill('rgba(0, 0, 0, 0)') + colorTable256 = new Array(16).fill('#000000') // fill color table // colors 16-231 are a 6x6x6 color cube @@ -72,14 +84,14 @@ exports.buildColorTable = function () { let redValue = red * 40 + (red ? 55 : 0) let greenValue = green * 40 + (green ? 55 : 0) let blueValue = blue * 40 + (blue ? 55 : 0) - colorTable256.push(`rgb(${redValue}, ${greenValue}, ${blueValue})`) + colorTable256.push(rgb255ToHex(redValue, greenValue, blueValue)) } } } // colors 232-255 are a grayscale ramp, sans black and white for (let gray = 0; gray < 24; gray++) { let value = gray * 10 + 8 - colorTable256.push(`rgb(${value}, ${value}, ${value})`) + colorTable256.push(rgb255ToHex(value, value, value)) } return colorTable256 @@ -88,24 +100,26 @@ exports.buildColorTable = function () { exports.SELECTION_FG = '#333' exports.SELECTION_BG = '#b2d7fe' -function resolveColor (themeN, shade) { - shade = +shade - if (shade < 16) shade = themes[themeN][shade] - else { - shade = exports.buildColorTable()[shade] - } - return shade -} - -exports.themePreview = function (n) { - document.querySelectorAll('[data-fg]').forEach((elem) => { +exports.themePreview = function (themeN) { + $('[data-fg]').forEach((elem) => { let shade = elem.dataset.fg - if (/^\d+$/.test(shade)) shade = resolveColor(n, shade) + if (/^\d+$/.test(shade)) shade = exports.toHex(shade, themeN) elem.style.color = shade }) - document.querySelectorAll('[data-bg]').forEach((elem) => { + $('[data-bg]').forEach((elem) => { let shade = elem.dataset.bg - if (/^\d+$/.test(shade)) shade = resolveColor(n, shade) + if (/^\d+$/.test(shade)) shade = exports.toHex(shade, themeN) elem.style.backgroundColor = shade }) } + +exports.toHex = function (shade, themeN) { + if (/^\d+$/.test(shade)) { + shade = +shade + if (shade < 16) shade = themes[themeN][shade] + else { + shade = exports.buildColorTable()[shade] + } + } + return shade +} diff --git a/js/term/upload.js b/js/term/upload.js index 6632755..226826e 100644 --- a/js/term/upload.js +++ b/js/term/upload.js @@ -44,7 +44,7 @@ module.exports = function (conn, input, screen) { lines = v.split('\n') line_i = 0 inline_pos = 0 // offset in line - send_delay_ms = qs('#fu_delay').value + send_delay_ms = +qs('#fu_delay').value // sanitize - 0 causes overflows if (send_delay_ms < 0) { @@ -92,13 +92,18 @@ module.exports = function (conn, input, screen) { } } + let maxChunk = +qs('#fu_chunk').value + if (maxChunk === 0 || maxChunk > MAX_LINE_LEN) { + maxChunk = MAX_LINE_LEN + } + let chunk - if ((curLine.length - inline_pos) <= MAX_LINE_LEN) { - chunk = curLine.substr(inline_pos, MAX_LINE_LEN) + if ((curLine.length - inline_pos) <= maxChunk) { + chunk = curLine.substr(inline_pos, maxChunk) inline_pos = 0 } else { - chunk = curLine.substr(inline_pos, MAX_LINE_LEN) - inline_pos += MAX_LINE_LEN + chunk = curLine.substr(inline_pos, maxChunk) + inline_pos += maxChunk } if (!input.sendString(chunk)) { @@ -154,19 +159,19 @@ module.exports = function (conn, input, screen) { reader.readAsText(file) }, false) - qs('#term-fu-open').addEventListener('click', function () { + qs('#term-fu-open').addEventListener('click', e => { + e.preventDefault() openUploadDialog() - return false }) - qs('#term-fu-start').addEventListener('click', function () { + qs('#term-fu-start').addEventListener('click', e => { + e.preventDefault() startUpload() - return false }) - qs('#term-fu-close').addEventListener('click', function () { + qs('#term-fu-close').addEventListener('click', e => { + e.preventDefault() fuClose() - return false }) }, open: openUploadDialog, diff --git a/js/term_conf.js b/js/term_conf.js new file mode 100644 index 0000000..87d9d70 --- /dev/null +++ b/js/term_conf.js @@ -0,0 +1,105 @@ +const ColorTriangle = require('./lib/colortriangle') +const $ = require('./lib/chibi') +const themes = require('./term/themes') +const { qs } = require('./utils') + +function selectedTheme () { + return +$('#theme').val() +} + +exports.init = function () { + $('#theme').on('change', showColor) + + $('#default_fg').on('input', showColor) + $('#default_bg').on('input', showColor) + + let opts = { + padding: 10, + event: 'drag', + uppercase: true, + trianglePointerSize: 20, + // wheelPointerSize: 12, + size: 200, + parseColor: (color) => { + return themes.toHex(color, selectedTheme()) + } + } + + ColorTriangle.initInput(qs('#default_fg'), opts) + ColorTriangle.initInput(qs('#default_bg'), opts) + + $('.colorprev.bg span').on('click', function () { + const bg = this.dataset.bg + if (typeof bg != 'undefined') $('#default_bg').val(bg) + showColor() + }) + + $('.colorprev.fg span').on('click', function () { + const fg = this.dataset.fg + if (typeof fg != 'undefined') $('#default_fg').val(fg) + showColor() + }) + + let $presets = $('#fgbg_presets') + for (let i = 0; i < themes.fgbgThemes.length; i++) { + const thm = themes.fgbgThemes[i] + const fg = thm[0] + const bg = thm[1] + const lbl = thm[2] + const tit = thm[3] + $presets.htmlAppend( + ' ' + lbl + ' ') + + if ((i + 1) % 5 === 0) $presets.htmlAppend('
') + } + + $('.preset').on('click', function () { + $('#default_fg').val(this.dataset.xfg) + $('#default_bg').val(this.dataset.xbg) + showColor() + }) + + showColor() +} + +function showColor () { + let ex = qs('.color-example') + let fg = $('#default_fg').val() + let bg = $('#default_bg').val() + + if (/^\d+$/.test(fg)) { + fg = +fg + } else if (!/^#[\da-f]{6}$/i.test(fg)) { + fg = 'black' + } + + if (/^\d+$/.test(bg)) { + bg = +bg + } else if (!/^#[\da-f]{6}$/i.test(bg)) { + bg = 'black' + } + + const themeN = selectedTheme() + ex.dataset.fg = fg + ex.dataset.bg = bg + + themes.themePreview(themeN) + + $('.colorprev.fg span').css('background', themes.toHex(bg, themeN)) +} + +exports.nextTheme = () => { + let sel = qs('#theme') + let i = sel.selectedIndex + sel.options[++i % sel.options.length].selected = true + showColor() +} + +exports.prevTheme = () => { + let sel = qs('#theme') + let i = sel.selectedIndex + sel.options[(sel.options.length + (--i)) % sel.options.length].selected = true + showColor() +} diff --git a/js/utils.js b/js/utils.js old mode 100755 new mode 100644 index 9a9e973..914f726 --- a/js/utils.js +++ b/js/utils.js @@ -26,11 +26,6 @@ exports.cr = function cr (hdl) { } } -/** Convert any to bool safely */ -exports.bool = function bool (x) { - return (x === 1 || x === '1' || x === true || x === 'true') -} - /** Decode number from 2B encoding */ exports.parse2B = function parse2B (s, i = 0) { return (s.charCodeAt(i++) - 1) + (s.charCodeAt(i) - 1) * 127 diff --git a/js/wifi.js b/js/wifi.js index 9728a6f..16d5e06 100644 --- a/js/wifi.js +++ b/js/wifi.js @@ -1,9 +1,11 @@ const $ = require('./lib/chibi') -const { mk, bool } = require('./utils') +const { mk } = require('./utils') const tr = require('./lang') -;(function (w) { - const authStr = ['Open', 'WEP', 'WPA', 'WPA2', 'WPA/WPA2'] +{ + const w = window.WiFi = {} + + const authTypes = ['Open', 'WEP', 'WPA', 'WPA2', 'WPA/WPA2'] let curSSID // Get XX % for a slider input @@ -20,9 +22,13 @@ const tr = require('./lang') $('#sta-nw-nil').toggleClass('hidden', name.length > 0) $('#sta-nw .essid').html($.htmlEscape(name)) - const nopw = !password || password.length === 0 - $('#sta-nw .passwd').toggleClass('hidden', nopw) - $('#sta-nw .nopasswd').toggleClass('hidden', !nopw) + const hasPassword = !!password + + // (the following is kind of confusing with the double-double negations, + // but it works) + $('#sta-nw .passwd').toggleClass('hidden', !hasPassword) + $('#sta-nw .nopasswd').toggleClass('hidden', hasPassword) + $('#sta-nw .ip').html(ip.length > 0 ? tr('wifi.connected_ip_is') + ip : tr('wifi.not_conn')) } @@ -40,7 +46,7 @@ const tr = require('./lang') if (status !== 200) { // bad response - rescan(5000) // wait 5sm then retry + rescan(5000) // wait 5s then retry return } @@ -52,7 +58,7 @@ const tr = require('./lang') return } - const done = !bool(resp.result.inProgress) && (resp.result.APs.length > 0) + const done = !resp.result.inProgress && resp.result.APs.length > 0 rescan(done ? 15000 : 1000) if (!done) return // no redraw yet @@ -65,9 +71,7 @@ const tr = require('./lang') $('#ap-loader').toggleClass('hidden', done) // scan done - resp.result.APs.sort(function (a, b) { - return b.rssi - a.rssi - }).forEach(function (ap) { + resp.result.APs.sort((a, b) => b.rssi - a.rssi).forEach(function (ap) { ap.enc = parseInt(ap.enc) if (ap.enc > 4) return // hide unsupported auths @@ -90,7 +94,7 @@ const tr = require('./lang') $(inner).addClass('inner') .htmlAppend(`
${ap.rssi_perc}
`) .htmlAppend(`
${escapedSSID}
`) - .htmlAppend(`
${authStr[ap.enc]}
`) + .htmlAppend(`
${authTypes[ap.enc]}
`) $item.on('click', function () { let $th = $(this) @@ -164,4 +168,4 @@ const tr = require('./lang') w.init = wifiInit w.startScanning = startScanning -})(window.WiFi = {}) +} diff --git a/lang/_js-dump.php b/lang/_js-dump.php new file mode 100755 index 0000000..411d6f2 --- /dev/null +++ b/lang/_js-dump.php @@ -0,0 +1,14 @@ +#! /usr/bin/env php + obj.request) + .concat([this.resource]).join('!') + } + + let currentRequest = this.currentRequest + if (!currentRequest) { + remainingRequest = this.loaders.slice(this.loaderIndex) + .map(obj => obj.request) + .concat([this.resource]).join('!') + } + + let map = { + version: 3, + file: currentRequest, + sourceRoot: '', + sources: [remainingRequest], + sourcesContent: [source], + names: [], + mappings: 'AAAA;AAAA' + } + + this.callback(null, + `/* Generated language file */\n` + + `module.exports=${JSON.stringify(data)}\n`, map) +} diff --git a/lang/common.php b/lang/common.php new file mode 100644 index 0000000..f2d6c75 --- /dev/null +++ b/lang/common.php @@ -0,0 +1,19 @@ + 'ESPTerm', + 'appname_demo' => 'ESPTerm DEMO', + + // not used - api etc. Added to suppress warnings + 'menu.term_set' => '', + 'menu.wifi_connstatus' => '', + 'menu.wifi_set' => '', + 'menu.wifi_scan' => '', + 'menu.network_set' => '', + 'menu.system_set' => '', + 'menu.write_defaults' => '', + 'menu.restore_defaults' => '', + 'menu.restore_hard' => '', + 'menu.reset_screen' => '', + 'menu.index' => '', +]; diff --git a/lang/cs.php b/lang/cs.php new file mode 100644 index 0000000..f281fee --- /dev/null +++ b/lang/cs.php @@ -0,0 +1,271 @@ + 'Nastavení WiFi', + 'menu.cfg_network' => 'Nastavení sítě', + 'menu.cfg_term' => 'Nastavení terminalu', + 'menu.about' => 'About', + 'menu.help' => 'Nápověda', + 'menu.term' => 'Zpět k terminálu', + 'menu.cfg_system' => 'Nastavení systému', + 'menu.cfg_wifi_conn' => 'Připojování', + 'menu.settings' => 'Nastavení', + + // Terminal page + + 'title.term' => 'Terminál', // page title of the terminal page + + 'term_nav.fullscreen' => 'Celá obr.', + 'term_nav.config' => 'Nastavení', + 'term_nav.wifi' => 'WiFi', + 'term_nav.help' => 'Nápověda', + 'term_nav.about' => 'About', + 'term_nav.paste' => 'Vložit', + 'term_nav.upload' => 'Nahrát', + 'term_nav.keybd' => 'Klávesnice', + 'term_nav.paste_prompt' => 'Vložte text k~odeslání:', + + 'term_conn.connecting' => 'Připojuji se', + 'term_conn.waiting_content' => 'Čekám na data', + 'term_conn.disconnected' => 'Odpojen', + 'term_conn.waiting_server' => 'Čekám na server', + 'term_conn.reconnecting' => 'Obnova spojení', + + // Terminal settings page + + 'term.defaults' => 'Výchozí nastavení', + 'term.expert' => 'Pokročilé volby', + 'term.explain_initials' => ' + Tato nastavení jsou použita po spuštění a při resetu obrazovky + (příkaz RIS, \ec). Tyto volby lze měnit za běhu + pomocí řídicích sekvencí. + ', + 'term.explain_expert' => ' + Interní parametry terminálu. Změnou časování lze dosáhnout kratší + latence a~rychlejšího překreslování, hodnoty záleží na konkrétní + aplikaci. Timeout parseru je čas do automatického zrušení započaté + řídicí sekvence.', + + 'term.example' => 'Náhled výchozích barev', + + 'term.explain_scheme' => ' + Výchozí barvu textu a pozadí vyberete kliknutím na barvy v~paletě. + Dále lze použít ANSI barvy 0-255 a hex ve formátu #FFFFFF. + ', + + 'term.fgbg_presets' => 'Předvolby výchozích
barev textu a pozadí', + 'term.color_scheme' => 'Barevné schéma', + 'term.reset_screen' => 'Resetovat obrazovku a parser', + 'term.term_title' => 'Nadpis', + 'term.term_width' => 'Šířka', + 'term.term_height' => 'Výška', + 'term.buttons' => 'Text tlačítke', + 'term.theme' => 'Barevná paleta', + 'term.cursor_shape' => 'Styl kurzoru', + 'term.parser_tout_ms' => 'Timeout parseru', + 'term.display_tout_ms' => 'Prodleva překreslení', + 'term.display_cooldown_ms' => 'Min. čas překreslení', + 'term.allow_decopt_12' => 'Povolit \e?12h/l', + 'term.fn_alt_mode' => 'SS3 Fx klávesy', + 'term.show_config_links' => 'Menu pod obrazovkou', + 'term.show_buttons' => 'Zobrazit tlačítka', + 'term.loopback' => 'Loopback (SRM)', + 'term.crlf_mode' => 'Enter = CR+LF (LNM)', + 'term.want_all_fn' => 'Zachytávat F5, F11, F12', + 'term.button_msgs' => 'Reporty tlačítek
(dek. ASCII CSV)', + 'term.color_fg' => 'Výchozí text', + 'term.color_bg' => 'Výchozí pozadí', + 'term.color_fg_prev' => 'Barva textu', + 'term.color_bg_prev' => 'Barva pozadí', + 'term.colors_preview' => '', +// 'term.debugbar' => 'Ladění ', +// 'term.ascii_debug' => 'Použít debug parser', + + 'cursor.block_blink' => 'Blok, blikající', + 'cursor.block_steady' => 'Blok, stálý', + 'cursor.underline_blink' => 'Podtržítko, blikající', + 'cursor.underline_steady' => 'Podtržítko, stálé', + 'cursor.bar_blink' => 'Svislice, blikající', + 'cursor.bar_steady' => 'Svislice, stálá', + + // Text upload dialog + + 'upload.title' => 'Upload textu', + 'upload.prompt' => 'Načíst ze souboru:', + 'upload.endings' => 'Konce řádku:', + 'upload.endings.cr' => 'CR (klávesa Enter)', + 'upload.endings.crlf' => 'CR LF (Windows)', + 'upload.endings.lf' => 'LF (Linux)', + 'upload.chunk_delay' => 'Prodleva (ms):', + 'upload.chunk_size' => 'Délka úseku (0=řádek):', + 'upload.progress' => 'Proběh:', + + // Network config page + + 'net.explain_sta' => ' + Odškrtněte "Použít dynamickou IP" pro nastavení statické IP adresy.', + + 'net.explain_ap' => ' + Tato nastavení ovlivňují interní DHCP server v AP režimu (hotspot).', + + 'net.ap_dhcp_time' => 'Doba zapůjčení adresy', + 'net.ap_dhcp_start' => 'Začátek IP poolu', + 'net.ap_dhcp_end' => 'Konec IP poolu', + 'net.ap_addr_ip' => 'Vlastní IP adresa', + 'net.ap_addr_mask' => 'Maska podsítě', + + 'net.sta_dhcp_enable' => 'Použít dynamickou IP', + 'net.sta_addr_ip' => 'Statická IP modulu', + 'net.sta_addr_mask' => 'Maska podsítě', + 'net.sta_addr_gw' => 'Gateway', + + 'net.ap' => 'DHCP server (AP)', + 'net.sta' => 'DHCP klient', + 'net.sta_mac' => 'MAC adresa klienta', + 'net.ap_mac' => 'MAC adresa AP', + 'net.details' => 'MAC adresy', + + // Wifi config page + + 'wifi.ap' => 'WiFi hotspot', + 'wifi.sta' => 'Připojení k~externí síti', + + 'wifi.enable' => 'Zapnuto', + 'wifi.tpw' => 'Vysílací výkon', + 'wifi.ap_channel' => 'WiFi kanál', + 'wifi.ap_ssid' => 'Jméno hotspotu', + 'wifi.ap_password' => 'Přístupové heslo', + 'wifi.ap_hidden' => 'Skrýt síť', + 'wifi.sta_info' => 'Zvolená síť', + + 'wifi.not_conn' => 'Nepřipojen.', + 'wifi.sta_none' => 'Žádná', + 'wifi.sta_active_pw' => '🔒 Uložené heslo', + 'wifi.sta_active_nopw' => '🔓 Bez hesla', + 'wifi.connected_ip_is' => 'Připojen, IP: ', + 'wifi.sta_password' => 'Heslo:', + + 'wifi.scanning' => 'Hledám sítě', + 'wifi.scan_now' => 'Klikněte pro vyhledání sítí!', + 'wifi.cant_scan_no_sta' => 'Klikněte pro zapnutí režimu klienta a vyhledání sítí!', + 'wifi.select_ssid' => 'Dostupné sítě:', + 'wifi.enter_passwd' => 'Zadejte heslo pro ":ssid:"', + 'wifi.sta_explain' => 'Vyberte síť a připojte se tlačítkem vpravo nahoře.', + + // Wifi connecting status page + + 'wificonn.status' => 'Stav:', + 'wificonn.back_to_config' => 'Zpět k~nastavení WiFi', + 'wificonn.telemetry_lost' => 'Spojení bylo přerušeno; připojování selhalo, nebo jste byli odpojeni od sítě.', + 'wificonn.explain_android_sucks' => ' + Pokud ESPTerm konfigurujete pomocí mobilu nebo z~externí sítě, může se stát + že některé ze zařízení změní síť a~ukazatel průběhu přestane fungovat. + Počkejte ~15s a pak zkontrolujte, zda se připojení zdařilo. + ', + + 'wificonn.explain_reset' => ' + Interní hotspot lze kdykoliv vynutit podržením tlačítka BOOT, až modrá LED začne blikat. + Podržíte-li tlačítko déle (LED začne blikat rychleji), dojde k~obnovení do výchozích anstavení.', + + 'wificonn.disabled' => "Režim klienta není povolen.", + 'wificonn.idle' => "Žádná IP adresa, připojování neprobíhá.", + 'wificonn.success' => "Připijen! IP adresa je ", + 'wificonn.working' => "Připojuji k zvolené síti", + 'wificonn.fail' => "Připojení selhalo, zkontrolujte nastavení a~pokus opakujte. Důvod: ", + + // Access restrictions form + + 'pwlock.title' => 'Omezení přístupu', + 'pwlock.explain' => ' + Části webového rozhraní lze chránit heslem. Nemáte-li v úmyslu heslo měnit, + do jeho políčka nic nevyplňujte.
+ Výchozí přístupové heslo je "%def_access_pw%". + ', + 'pwlock.region' => 'Chránit heslem', + 'pwlock.region.none' => 'Nic, vše volně přístupné', + 'pwlock.region.settings_noterm' => 'Nastavení, mimo terminál', + 'pwlock.region.settings' => 'Všechna nastavení', + 'pwlock.region.menus' => 'Celá admin. sekce', + 'pwlock.region.all' => 'Vše, včetně terminálu', + 'pwlock.new_access_pw' => 'Nové přístupové heslo', + 'pwlock.new_access_pw2' => 'Zopakujte nové heslo', + 'pwlock.admin_pw' => 'Systémové heslo', + 'pwlock.access_name' => 'Uživatelské jméno', + + // Setting admin password + + 'adminpw.title' => 'Změna systémového hesla', + 'adminpw.explain' => + ' + Systémové heslo slouží k úpravám uložených výchozích nastavení + a ke změně přístupových oprávnění. + Toto heslo je uloženo mimo ostatní data, obnovení do výchozách nastavení + na něj nemá vliv. + Toto heslo nelze jednoduše obnovit, v případě zapomenutí vymažte flash paměť a obnovte firmware.
+ Vychozí systémové heslo je "%def_admin_pw%". + ', + 'adminpw.new_admin_pw' => 'Nové systémové heslo', + 'adminpw.new_admin_pw2' => 'Zopakujte nové heslo', + 'adminpw.old_admin_pw' => 'Původní systémové heslo', + + // Persist form + + 'persist.title' => 'Záloha a~obnovení konfigurace', + 'persist.explain' => ' + Všechna nastavení jsou ukládána do flash paměti. V~paměti jsou + vyhrazené dva oddíly, aktivní nastavení a záloha. Zálohu lze přepsat + za použití systémového hesla, původní nastavení z ní pak můžete kdykoliv obnovit. + Pro obnovení ze zálohy stačí podržet tlačítko BOOT, až modrá LED začne rychle blikat. + ', + 'persist.confirm_restore' => 'Chcete obnovit všechna nastavení?', + 'persist.confirm_restore_hard' => + 'Opravdu chcete načíst tovární nastavení? Všechna nastavení kromě zálohy a systémového hesla + budou přepsána, včetně nastavení WiFi!', + 'persist.confirm_store_defaults' => + 'Zadejte systémové heslo pro přepsání zálohy aktuálními parametry.', + 'persist.password' => 'Systémové heslo:', + 'persist.restore_defaults' => 'Obnovit ze zálohy', + 'persist.write_defaults' => 'Zálohovat aktuální nastavení', + 'persist.restore_hard' => 'Načíst tovární nastavení', + 'persist.restore_hard_explain' => + '(Tímto vymažete nastavení WiFi! Záloha a systémové heslo zůstanou beze změny.)', + + // UART settings form + + 'uart.title' => 'Sériový port', + 'uart.explain' => ' + Tímto formulářem můžete upravit nastavení komunikačního UARTu. + Ladicí výpisy jsou na pinu P2 s~pevnými parametry: 115200 baud, 1 stop bit, žádná parita. + ', + 'uart.baud' => 'Rychlost', + 'uart.parity' => 'Parita', + 'uart.parity.none' => 'Źádná', + 'uart.parity.odd' => 'Lichá', + 'uart.parity.even' => 'Sudá', + 'uart.stop_bits' => 'Stop-bity', + 'uart.stop_bits.one' => '1', + 'uart.stop_bits.one_and_half' => '1.5', + 'uart.stop_bits.two' => '2', + + // HW tuning form + + 'hwtuning.title' => 'Tuning hardwaru', + 'hwtuning.explain' => ' + ESP8266 lze přetaktovat z~80~MHz na 160~MHz. Vyšší rychlost umožní rychlejší překreslování + obrazovky a stránky se budou načítat rychleji. Nevýhodou je vyšší spotřeba a citlivost k~rušení. + ', + 'hwtuning.overclock' => 'Přetaktovat na 160~MHz', + + // Generic button / dialog labels + + 'apply' => 'Uložit!', + 'start' => 'Start', + 'cancel' => 'Zrušit', + 'enabled' => 'Zapnuto', + 'disabled' => 'Vypnuto', + 'yes' => 'Ano', + 'no' => 'Ne', + 'confirm' => 'OK', + 'copy' => 'Kopírovat', + 'form_errors' => 'Neplatné hodnoty:', +]; diff --git a/lang/de.php b/lang/de.php new file mode 100644 index 0000000..1847dae --- /dev/null +++ b/lang/de.php @@ -0,0 +1,270 @@ + 'WLAN-Einstellungen', + 'menu.cfg_network' => 'Netzwerkeinstellungen', + 'menu.cfg_term' => 'Terminaleinstellungen', + 'menu.about' => 'Über ESPTerm', + 'menu.help' => 'Schnellreferenz', + 'menu.term' => 'Zurück zum Terminal', + 'menu.cfg_system' => 'Systemeinstellungen', + 'menu.cfg_wifi_conn' => 'Verbinden mit dem Netzwerk', + 'menu.settings' => 'Einstellungen', + + // Terminal page + + 'title.term' => 'Terminal', // page title of the terminal page + + 'term_nav.fullscreen' => 'Vollbild', + 'term_nav.config' => 'Konfiguration', + 'term_nav.wifi' => 'WLAN', + 'term_nav.help' => 'Hilfe', + 'term_nav.about' => 'Info', + 'term_nav.paste' => 'Einfügen', + 'term_nav.upload' => 'Hochladen', + 'term_nav.keybd' => 'Tastatur', + 'term_nav.paste_prompt' => 'Text einfügen zum Versenden:', + + 'term_conn.connecting' => 'Verbinden', + 'term_conn.waiting_content' => 'Warten auf Inhalt', + 'term_conn.disconnected' => 'Nicht verbunden', + 'term_conn.waiting_server' => 'Warten auf Server', + 'term_conn.reconnecting' => 'Verbinden', + + // Terminal settings page + + 'term.defaults' => 'Anfangseinstellungen', + 'term.expert' => 'Expertenoptionen', + 'term.explain_initials' => ' + Dies sind die Anfangseinstellungen, die benutzt werden, nachdem ESPTerm startet, + oder wenn der Bildschirm mit dem \ec-Kommando zurückgesetzt wird. + Sie können durch Escape-Sequenzen verändert werden. + ', + 'term.explain_expert' => ' + Dies sind erweiterte Konfigurationsoptionen, die meistens nicht verändert + werden müssen. Bearbeite sie nur, wenn du weißt, was du tust.', + + 'term.example' => 'Standardfarbenvorschau', + + 'term.explain_scheme' => ' + Um die Standardtextfarbe und Standardhintergrundfarbe auszuwählen, klicke auf + die Vorschaupalette, oder benutze die Zahlen 0-15 für die Themafarben, 16-255 + für Standardfarben, oder Hexadezimal (#FFFFFF) für True Color (24-bit). + ', + + 'term.fgbg_presets' => 'Voreinstellungen', + 'term.color_scheme' => 'Farbschema', + 'term.reset_screen' => 'Bildschirm & Parser zurücksetzen', + 'term.term_title' => 'Titeltext', + 'term.term_width' => 'Breite', + 'term.term_height' => 'Höhe', + 'term.buttons' => 'Tastentext', + 'term.theme' => 'Farbthema', + 'term.cursor_shape' => 'Cursorstil', + 'term.parser_tout_ms' => 'Parser-Auszeit', + 'term.display_tout_ms' => 'Zeichenverzögerung', + 'term.display_cooldown_ms' => 'Zeichenabkühlzeit', + 'term.allow_decopt_12' => '\e?12h/l erlauben', + 'term.fn_alt_mode' => 'SS3 Fn-Tasten', + 'term.show_config_links' => 'Links anzeigen', + 'term.show_buttons' => 'Tasten anzeigen', + 'term.loopback' => 'Lokales Echo (SRM)', + 'term.crlf_mode' => 'Enter = CR+LF (LNM)', + 'term.want_all_fn' => 'F5, F11, F12 erfassen', + 'term.button_msgs' => 'Tastencodes
(ASCII, dec, CSV)', + 'term.color_fg' => 'Standardvordergr.', + 'term.color_bg' => 'Standardhintergr.', + 'term.color_fg_prev' => 'Vordergrund', + 'term.color_bg_prev' => 'Hintergrund', + 'term.colors_preview' => '', + 'term.debugbar' => 'Debug-Leiste anzeigen', + 'term.ascii_debug' => 'Kontrollcodes anzeigen', + + 'cursor.block_blink' => 'Block, blinkend', + 'cursor.block_steady' => 'Block, ruhig', + 'cursor.underline_blink' => 'Unterstrich, blinkend', + 'cursor.underline_steady' => 'Unterstrich, ruhig', + 'cursor.bar_blink' => 'Balken, blinkend', + 'cursor.bar_steady' => 'Balken, ruhig', + + // Text upload dialog + + 'upload.title' => 'Text Hochladen', + 'upload.prompt' => 'Eine Textdatei laden:', + 'upload.endings' => 'Zeilenumbruch:', + 'upload.endings.cr' => 'CR (Enter-Taste)', + 'upload.endings.crlf' => 'CR LF (Windows)', + 'upload.endings.lf' => 'LF (Linux)', + 'upload.chunk_delay' => 'Datenblockverzögerung (ms):', + 'upload.chunk_size' => 'Datenblockgröße (0=Linie):', + 'upload.progress' => 'Hochladen:', + + // Network config page + + 'net.explain_sta' => ' + Schalte Dynamische IP aus um die statische IP-Addresse zu konfigurieren.', + + 'net.explain_ap' => ' + Diese Einstellungen beeinflussen den eingebauten DHCP-Server im AP-Modus.', + + 'net.ap_dhcp_time' => 'Leasezeit', + 'net.ap_dhcp_start' => 'Pool Start-IP', + 'net.ap_dhcp_end' => 'Pool End-IP', + 'net.ap_addr_ip' => 'Eigene IP-Addresse', + 'net.ap_addr_mask' => 'Subnet-Maske', + + 'net.sta_dhcp_enable' => 'Dynamische IP', + 'net.sta_addr_ip' => 'ESPTerm statische IP', + 'net.sta_addr_mask' => 'Subnet-Maske', + 'net.sta_addr_gw' => 'Gateway-IP', + + 'net.ap' => 'DHCP Server (AP)', + 'net.sta' => 'DHCP Client (Station)', + 'net.sta_mac' => 'Station MAC', + 'net.ap_mac' => 'AP MAC', + 'net.details' => 'MAC-Addressen', + + // Wifi config page + + 'wifi.ap' => 'Eingebauter Access Point', + 'wifi.sta' => 'Bestehendes Netzwerk beitreten', + + 'wifi.enable' => 'Aktiviert', + 'wifi.tpw' => 'Sendeleistung', + 'wifi.ap_channel' => 'Kanal', + 'wifi.ap_ssid' => 'AP SSID', + 'wifi.ap_password' => 'Passwort', + 'wifi.ap_hidden' => 'SSID verbergen', + 'wifi.sta_info' => 'Ausgewählt', + + 'wifi.not_conn' => 'Nicht verbunden.', + 'wifi.sta_none' => 'Keine', + 'wifi.sta_active_pw' => '🔒 Passwort gespeichert', + 'wifi.sta_active_nopw' => '🔓 Offen', + 'wifi.connected_ip_is' => 'Verbunden, IP ist ', + 'wifi.sta_password' => 'Passwort:', + + 'wifi.scanning' => 'Scannen', + 'wifi.scan_now' => 'Klicke hier um zu scannen!', + 'wifi.cant_scan_no_sta' => 'Klicke hier um Client-Modus zu aktivieren und zu scannen!', + 'wifi.select_ssid' => 'Verfügbare Netzwerke:', + 'wifi.enter_passwd' => 'Passwort für ":ssid:"', + 'wifi.sta_explain' => + 'Nach dem Auswählen eines Netzwerks, drücke Bestätigen, um dich zu verbinden.', + + // Wifi connecting status page + + 'wificonn.status' => 'Status:', + 'wificonn.back_to_config' => 'Zurück zur WLAN-Konfiguration', + 'wificonn.telemetry_lost' => 'Telemetrie verloren; etwas lief schief, oder dein Gerät wurde getrennt.', + 'wificonn.explain_android_sucks' => ' + Wenn du gerade ESPTerm mit einem Handy oder über ein anderes externes Netzwerk + konfigurierst, kann dein Gerät die Verbindung verlieren und diese Fortschrittsanzeige + wird nicht funktionieren. Bitte warte eine Weile (etwa 15 Sekunden) und prüfe dann, + ob die Verbindung gelangen ist.', + + 'wificonn.explain_reset' => ' + Um den eingebauten AP zur Aktivierung zu zwingen, halte den BOOT-Knopf gedrückt bis die + blaue LED beginnt, zu blinken. Halte ihn länger gedrückt (bis die LED schnell blinkt) + um eine "Werksrückstellung" zu vollziehen.', + + 'wificonn.disabled' => "Stationsmodus ist deaktiviert.", + 'wificonn.idle' => "Nicht verbunden und ohne IP.", + 'wificonn.success' => "Verbunden! Empfangene IP: ", + 'wificonn.working' => "Verbinden mit dem ausgewählten AP", + 'wificonn.fail' => "Verbindung fehlgeschlagen; prüfe die Einstellungen und versuche es erneut. Grund: ", + + // Access restrictions form + + 'pwlock.title' => 'Zugriffsbeschränkungen', + 'pwlock.explain' => ' + Manche, oder alle Teile des Web-Interface können mit einem Passwort geschützt werden. + Lass die Passwortfelder leer wenn du es sie verändern möchtest.
+ Das voreingestellte Passwort ist "%def_access_pw%".', + 'pwlock.region' => 'Geschützte Seiten', + 'pwlock.region.none' => 'Keine, alles offen', + 'pwlock.region.settings_noterm' => 'WLAN-, Netzwerk- & Systemeinstellungen', + 'pwlock.region.settings' => 'Alle Einstellungsseiten', + 'pwlock.region.menus' => 'Dieser ganze Menüabschnitt', + 'pwlock.region.all' => 'Alles, sogar das Terminal', + 'pwlock.new_access_pw' => 'Neues Passwort', + 'pwlock.new_access_pw2' => 'Wiederholen', + 'pwlock.admin_pw' => 'Systempasswort', + 'pwlock.access_name' => 'Benutzername', + + // Setting admin password + + 'adminpw.title' => 'Systempasswort ändern', + 'adminpw.explain' =>' + Das "Systempasswort" wird benutzt, um die gespeicherten Standardeinstellungen + und die Zugriffsbeschränkungen zu verändern. Dieses Passwort wird nicht als Teil + der Hauptkonfiguration gespeichert, d.h. Speichern / Wiederherstellen wird das + Passwort nicht beeinflussen. Wenn das Systempasswort vergessen wird, ist + die einfachste Weise, wieder Zugriff zu erhalten, ein Re-flash des Chips.
+ Das voreingestellte Systempasswort ist "%def_admin_pw%". + ', + 'adminpw.new_admin_pw' => 'Neues Systempasswort', + 'adminpw.new_admin_pw2' => 'Wiederholen', + 'adminpw.old_admin_pw' => 'Altes Systempasswort', + + // Persist form + + 'persist.title' => 'Speichern & Wiederherstellen', + 'persist.explain' => ' + ESPTerm speichert alle Einstellungen im Flash-Speicher. Die aktiven Einstellungen + können in den “Voreinstellungsbereich” kopiert werden und später wiederhergestellt + werden mit der Taste unten.', + 'persist.confirm_restore' => 'Alle Einstellungen zu den Voreinstellungen zurücksetzen?', + 'persist.confirm_restore_hard' => ' + Zurücksetzen zu den Firmware-Voreinstellungen? Dies wird alle aktiven + Einstellungen zürucksetzen und den AP-Modus aktivieren mit der Standard-SSID.', + 'persist.confirm_store_defaults' => + 'Systempasswort eingeben um Voreinstellungen zu überschreiben', + 'persist.password' => 'Systempasswort:', + 'persist.restore_defaults' => 'Zu gespeicherten Voreinstellungen zurücksetzen', + 'persist.write_defaults' => 'Aktive Einstellungen als Voreinstellungen speichern', + 'persist.restore_hard' => 'Aktive Einstellungen zu Werkseinstellungen zurücksetzen', + 'persist.restore_hard_explain' => ' + (Dies löscht die WLAN-Konfiguration! Beeinflusst die gespeicherten Voreinstellungen + oder das Systempasswort nicht.)', + + // UART settings form + + 'uart.title' => 'Serieller Port Parameter', + 'uart.explain' => ' + Dies steuert den Kommunikations-UART. Der Debug-UART ist auf 115.200 baud fest + eingestellt mit einem Stop-Bit und keiner Parität. + ', + 'uart.baud' => 'Baudrate', + 'uart.parity' => 'Parität', + 'uart.parity.none' => 'Keine', + 'uart.parity.odd' => 'Ungerade', + 'uart.parity.even' => 'Gerade', + 'uart.stop_bits' => 'Stop-Bits', + 'uart.stop_bits.one' => 'Eins', + 'uart.stop_bits.one_and_half' => 'Eineinhalb', + 'uart.stop_bits.two' => 'Zwei', + + // HW tuning form + + 'hwtuning.title' => 'Hardware-Tuning', + 'hwtuning.explain' => ' + ESP8266 kann übertaktet werden von 80 MHz auf 160 MHz. + Alles wird etwas schneller sein, aber mit höherem Stromverbrauch, + und eventuell auch mit höherer Interferenz. Mit Sorgfalt benutzen. + ', + 'hwtuning.overclock' => 'Übertakten', + + // Generic button / dialog labels + + 'apply' => 'Bestätigen!', + 'start' => 'Starten', + 'cancel' => 'Abbrechen', + 'enabled' => 'Aktiviert', + 'disabled' => 'Deaktiviert', + 'yes' => 'Ja', + 'no' => 'Nein', + 'confirm' => 'OK', + 'copy' => 'Kopieren', + 'form_errors' => 'Gültigkeitsfehler für:', +]; diff --git a/lang/en.php b/lang/en.php index 9333add..ad6bfb7 100644 --- a/lang/en.php +++ b/lang/en.php @@ -1,9 +1,6 @@ 'ESPTerm', - 'appname_demo' => 'ESPTerm DEMO', - 'menu.cfg_wifi' => 'WiFi Settings', 'menu.cfg_network' => 'Network Settings', 'menu.cfg_term' => 'Terminal Settings', @@ -14,23 +11,11 @@ return [ 'menu.cfg_wifi_conn' => 'Connecting to Network', 'menu.settings' => 'Settings', - // not used - api etc. Added to suppress warnings - 'menu.term_set' => '', - 'menu.wifi_connstatus' => '', - 'menu.wifi_set' => '', - 'menu.wifi_scan' => '', - 'menu.network_set' => '', - 'menu.system_set' => '', - 'menu.write_defaults' => '', - 'menu.restore_defaults' => '', - 'menu.restore_hard' => '', - 'menu.reset_screen' => '', - 'menu.index' => '', - // Terminal page 'title.term' => 'Terminal', // page title of the terminal page + 'term_nav.fullscreen' => 'Fullscreen', 'term_nav.config' => 'Config', 'term_nav.wifi' => 'WiFi', 'term_nav.help' => 'Help', @@ -40,14 +25,20 @@ return [ 'term_nav.keybd' => 'Keyboard', 'term_nav.paste_prompt' => 'Paste text to send:', + 'term_conn.connecting' => 'Connecting', + 'term_conn.waiting_content' => 'Waiting for content', + 'term_conn.disconnected' => 'Disconnected', + 'term_conn.waiting_server' => 'Waiting for server', + 'term_conn.reconnecting' => 'Reconnecting', + // Terminal settings page 'term.defaults' => 'Initial Settings', 'term.expert' => 'Expert Options', 'term.explain_initials' => ' - Those are the initial settings used after ESPTerm powers on, or when the screen - reset command is received (\ec). They can be changed by the - terminal application using escape sequences. + Those are the initial settings used after ESPTerm powers on, + or when the screen reset command is received (\ec). + They can be changed by the terminal application using escape sequences. ', 'term.explain_expert' => ' Those are advanced config options that usually don\'t need to be changed. @@ -57,34 +48,37 @@ return [ 'term.explain_scheme' => ' To select default text and background color, click on the - preview palette. Alternatively, use numbers 0-15 for theme colors, 16-255 for standard - colors and hex (#FFFFFF) for True Color (24-bit). + preview palette. Alternatively, use numbers 0-15 for theme colors, + 16-255 for standard colors and hex (#FFFFFF) for True Color (24-bit). ', - 'term.fgbg_presets' => 'Presets', + 'term.fgbg_presets' => 'Defaults Presets', 'term.color_scheme' => 'Color Scheme', 'term.reset_screen' => 'Reset screen & parser', - 'term.term_title' => 'Header text', + 'term.term_title' => 'Header Text', 'term.term_width' => 'Width', 'term.term_height' => 'Height', - 'term.buttons' => 'Button labels', - 'term.theme' => 'Color palette', - 'term.cursor_shape' => 'Cursor style', - 'term.parser_tout_ms' => 'Parser timeout', - 'term.display_tout_ms' => 'Redraw delay', - 'term.display_cooldown_ms' => 'Redraw cooldown', + 'term.buttons' => 'Button Labels', + 'term.theme' => 'Color Palette', + 'term.cursor_shape' => 'Cursor Style', + 'term.parser_tout_ms' => 'Parser Timeout', + 'term.display_tout_ms' => 'Redraw Delay', + 'term.display_cooldown_ms' => 'Redraw Cooldown', + 'term.allow_decopt_12' => 'Allow \e?12h/l', 'term.fn_alt_mode' => 'SS3 Fn keys', 'term.show_config_links' => 'Show nav links', 'term.show_buttons' => 'Show buttons', 'term.loopback' => 'Local Echo (SRM)', 'term.crlf_mode' => 'Enter = CR+LF (LNM)', - 'term.want_all_fn' => 'Capture all keys
(F5, F11, F12…)', + 'term.want_all_fn' => 'Capture F5, F11, F12', 'term.button_msgs' => 'Button codes
(ASCII, dec, CSV)', - 'term.color_fg' => 'Default fg.', - 'term.color_bg' => 'Default bg.', + 'term.color_fg' => 'Default Fg.', + 'term.color_bg' => 'Default Bg.', 'term.color_fg_prev' => 'Foreground', 'term.color_bg_prev' => 'Background', - 'term.colors_preview' => 'Defaults', + 'term.colors_preview' => '', + 'term.debugbar' => 'Debug internal state', + 'term.ascii_debug' => 'Display control codes', 'cursor.block_blink' => 'Block, blinking', 'cursor.block_steady' => 'Block, steady', @@ -93,6 +87,18 @@ return [ 'cursor.bar_blink' => 'I-bar, blinking', 'cursor.bar_steady' => 'I-bar, steady', + // Text upload dialog + + 'upload.title' => 'Text Upload', + 'upload.prompt' => 'Load a text file:', + 'upload.endings' => 'Line endings:', + 'upload.endings.cr' => 'CR (Enter key)', + 'upload.endings.crlf' => 'CR LF (Windows)', + 'upload.endings.lf' => 'LF (Linux)', + 'upload.chunk_delay' => 'Chunk delay (ms):', + 'upload.chunk_size' => 'Chunk size (0=line):', + 'upload.progress' => 'Upload:', + // Network config page 'net.explain_sta' => ' @@ -151,15 +157,15 @@ return [ 'wificonn.back_to_config' => 'Back to WiFi config', 'wificonn.telemetry_lost' => 'Telemetry lost; something went wrong, or your device disconnected.', 'wificonn.explain_android_sucks' => ' - If you\'re configuring ESPTerm via a smartphone, or were connected - from another external network, your device may lose connection and this - progress indicator won\'t work. Please wait a while (~ 15 seconds), + If you\'re configuring ESPTerm via a smartphone, or were connected + from another external network, your device may lose connection and + this progress indicator won\'t work. Please wait a while (~ 15 seconds), then check if the connection succeeded.', 'wificonn.explain_reset' => ' - To force enable the built-in AP, hold the BOOT - button until the blue LED starts flashing. Hold the button longer (until the LED - flashes rapidly) for a "factory reset".', + To force enable the built-in AP, hold the BOOT button until the blue LED + starts flashing. Hold the button longer (until the LED flashes rapidly) + for a "factory reset".', 'wificonn.disabled' =>"Station mode is disabled.", 'wificonn.idle' =>"Idle, not connected and has no IP.", @@ -191,10 +197,10 @@ return [ 'adminpw.title' => 'Change Admin Password', 'adminpw.explain' => ' - The "admin password" is used to manipulate the stored default settings + The "admin password" is used to manipulate the stored default settings and to change access restrictions. This password is not saved as part of the main config, i.e. using save / restore does not affect this - password. When the admin password is forgotten, the easiest way to + password. When the admin password is forgotten, the easiest way to re-gain access is to wipe and re-flash the chip.
The default admin password is "%def_admin_pw%". ', @@ -219,13 +225,14 @@ return [ 'persist.restore_defaults' => 'Reset to saved defaults', 'persist.write_defaults' => 'Save active settings as defaults', 'persist.restore_hard' => 'Reset active settings to factory defaults', - 'persist.restore_hard_explain' => '(This clears the WiFi config! Does not affect saved defaults or admin password.)', + 'persist.restore_hard_explain' => + '(This clears the WiFi config! Does not affect saved defaults or admin password.)', // UART settings form 'uart.title' => 'Serial Port Parameters', 'uart.explain' => ' - This form controls the communication UART. The debug UART is fixed + This form controls the communication UART. The debug UART is fixed at 115.200 baud, one stop-bit and no parity. ', 'uart.baud' => 'Baud rate', @@ -242,20 +249,23 @@ return [ 'hwtuning.title' => 'Hardware Tuning', 'hwtuning.explain' => ' - ESP8266 can be overclocked from 80 MHz to 160 MHz. - This will make it more responsive and allow faster screen updates - at the expense of slightly higher power consumption. This can also make - it more susceptible to interference. Use with care. + ESP8266 can be overclocked from 80 MHz to 160 MHz. This will make + it more responsive and allow faster screen updates at the expense of slightly + higher power consumption. This can also make it more susceptible to interference. + Use with care. ', 'hwtuning.overclock' => 'Overclock to 160MHz', // Generic button / dialog labels 'apply' => 'Apply!', + 'start' => 'Start', + 'cancel' => 'Cancel', 'enabled' => 'Enabled', 'disabled' => 'Disabled', 'yes' => 'Yes', 'no' => 'No', 'confirm' => 'OK', + 'copy' => 'Copy', 'form_errors' => 'Validation errors for:', ]; diff --git a/lang/js-keys.js b/lang/js-keys.js new file mode 100644 index 0000000..967aa6f --- /dev/null +++ b/lang/js-keys.js @@ -0,0 +1,12 @@ +// define language keys used by JS here +module.exports = [ + 'wifi.connected_ip_is', + 'wifi.not_conn', + 'wifi.enter_passwd', + 'term_nav.fullscreen', + 'term_conn.connecting', + 'term_conn.waiting_content', + 'term_conn.disconnected', + 'term_conn.waiting_server', + 'term_conn.reconnecting' +] diff --git a/pages/_head.php b/pages/_head.php index af974b9..8ca2eb1 100644 --- a/pages/_head.php +++ b/pages/_head.php @@ -5,8 +5,8 @@ <?= $_GET['PAGE_TITLE'] ?> - - + + diff --git a/pages/cfg_wifi_conn.php b/pages/cfg_wifi_conn.php old mode 100755 new mode 100644 diff --git a/pages/help.php b/pages/help.php index e577a72..a7529a4 100644 --- a/pages/help.php +++ b/pages/help.php @@ -16,6 +16,7 @@ +