Merge branch 'work'

pull/2/head
Ondřej Hruška 7 years ago
commit 4a032ee3b5
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 6
      .babelrc
  2. 3
      .eslintignore
  3. 2
      .eslintrc
  4. 4
      _build_common.sh
  5. 2
      _build_css.sh
  6. 2
      _build_html.sh
  7. 2
      _build_js.sh
  8. 2
      _debug_replacements.php
  9. 2
      _pages.php
  10. 25
      base.php
  11. 4
      build.sh
  12. 21
      compile_html.php
  13. 22
      dump_js_lang.php
  14. 8
      js/appcommon.js
  15. 2
      js/index.js
  16. 11
      js/lang.js
  17. 0
      js/lib/chibi.js
  18. 118
      js/lib/color_utils.js
  19. 572
      js/lib/colortriangle.js
  20. 57
      js/term/buttons.js
  21. 130
      js/term/connection.js
  22. 237
      js/term/debug_screen.js
  23. 525
      js/term/demo.js
  24. 89
      js/term/index.js
  25. 12
      js/term/input.js
  26. 75
      js/term/screen.js
  27. 548
      js/term/screen_parser.js
  28. 186
      js/term/screen_renderer.js
  29. 1
      js/term/soft_keyboard.js
  30. 88
      js/term/themes.js
  31. 27
      js/term/upload.js
  32. 105
      js/term_conf.js
  33. 5
      js/utils.js
  34. 30
      js/wifi.js
  35. 14
      lang/_js-dump.php
  36. 54
      lang/_js-lang-loader.js
  37. 19
      lang/common.php
  38. 271
      lang/cs.php
  39. 270
      lang/de.php
  40. 96
      lang/en.php
  41. 12
      lang/js-keys.js
  42. 4
      pages/_head.php
  43. 130
      pages/cfg_term.php
  44. 0
      pages/cfg_wifi_conn.php
  45. 1
      pages/help.php
  46. 92
      pages/help/cmd_d2d.php
  47. 2
      pages/help/input.php
  48. 2
      pages/help/sgr_colors.php
  49. 2
      pages/help/sgr_styles.php
  50. 7
      pages/help/troubleshooting.php
  51. 48
      pages/term.php
  52. 0
      sass/_grid-settings.scss
  53. 0
      sass/_normalize.scss
  54. 0
      sass/_utils.scss
  55. 3
      sass/app.scss
  56. 0
      sass/form/_buttons.scss
  57. 0
      sass/form/_fancy_button_mixins.scss
  58. 2
      sass/form/_form_elements.scss
  59. 0
      sass/form/_form_layout.scss
  60. 0
      sass/form/_index.scss
  61. 0
      sass/form/_select.scss
  62. 4
      sass/layout/_base.scss
  63. 0
      sass/layout/_box.scss
  64. 0
      sass/layout/_content.scss
  65. 0
      sass/layout/_index.scss
  66. 0
      sass/layout/_menu.scss
  67. 0
      sass/layout/_modal.scss
  68. 0
      sass/layout/_outer-wrap.scss
  69. 0
      sass/lib/bourbon/_bourbon-deprecated-upcoming.scss
  70. 0
      sass/lib/bourbon/_bourbon.scss
  71. 0
      sass/lib/bourbon/addons/_border-color.scss
  72. 0
      sass/lib/bourbon/addons/_border-radius.scss
  73. 0
      sass/lib/bourbon/addons/_border-style.scss
  74. 0
      sass/lib/bourbon/addons/_border-width.scss
  75. 0
      sass/lib/bourbon/addons/_buttons.scss
  76. 0
      sass/lib/bourbon/addons/_clearfix.scss
  77. 0
      sass/lib/bourbon/addons/_ellipsis.scss
  78. 0
      sass/lib/bourbon/addons/_font-stacks.scss
  79. 0
      sass/lib/bourbon/addons/_hide-text.scss
  80. 0
      sass/lib/bourbon/addons/_margin.scss
  81. 0
      sass/lib/bourbon/addons/_padding.scss
  82. 0
      sass/lib/bourbon/addons/_position.scss
  83. 0
      sass/lib/bourbon/addons/_prefixer.scss
  84. 0
      sass/lib/bourbon/addons/_retina-image.scss
  85. 0
      sass/lib/bourbon/addons/_size.scss
  86. 0
      sass/lib/bourbon/addons/_text-inputs.scss
  87. 0
      sass/lib/bourbon/addons/_timing-functions.scss
  88. 0
      sass/lib/bourbon/addons/_triangle.scss
  89. 0
      sass/lib/bourbon/addons/_word-wrap.scss
  90. 0
      sass/lib/bourbon/css3/_animation.scss
  91. 0
      sass/lib/bourbon/css3/_appearance.scss
  92. 0
      sass/lib/bourbon/css3/_backface-visibility.scss
  93. 0
      sass/lib/bourbon/css3/_background-image.scss
  94. 0
      sass/lib/bourbon/css3/_background.scss
  95. 0
      sass/lib/bourbon/css3/_border-image.scss
  96. 0
      sass/lib/bourbon/css3/_calc.scss
  97. 0
      sass/lib/bourbon/css3/_columns.scss
  98. 0
      sass/lib/bourbon/css3/_filter.scss
  99. 0
      sass/lib/bourbon/css3/_flex-box.scss
  100. 0
      sass/lib/bourbon/css3/_font-face.scss
  101. Some files were not shown because too many files have changed in this diff Show More

@ -6,13 +6,9 @@
"last 2 versions",
"> 4%",
"ie 11",
"safari 8",
"android 4.4"
"safari 8"
]
}
}],
["minify", {
"mergeVars": false
}]
]
}

@ -2,7 +2,8 @@
out/**/*
# libraries
js/lib/*
js/lib/chibi.js
js/lib/polyfills.js
# php generated file
js/lang.js

@ -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"],

@ -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

@ -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"

@ -3,4 +3,4 @@ source "_build_common.sh"
echo 'Building HTML...'
php ./compile_html.php
php ./compile_html.php $@

@ -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

@ -93,4 +93,6 @@ return [
'theme' => 0,
'pwlock' => 0,
'access_name' => 'espterm',
'allow_decopt_12' => 0,
];

@ -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', '', '/', '');

@ -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', <<<APS
@ -58,9 +59,11 @@ APS
);
}
define('LOCALE', isset($_GET['locale']) ? $_GET['locale'] : 'en');
define('LOCALE', isset($_GET['locale']) ? $_GET['locale'] : (getenv('ESP_LANG') ?: 'en'));
$_messages = require(__DIR__ . '/lang/' . LOCALE . '.php');
$_messages_fallback = require(__DIR__ . '/lang/en.php');
$_messages_common = require(__DIR__ . '/lang/common.php');
$_pages = require(__DIR__ . '/_pages.php');
define('APP_NAME', 'ESPTerm');
@ -96,12 +99,26 @@ function je($s)
function tr($key)
{
global $_messages;
if (isset($_messages[$key])) return $_messages[$key];
else {
global $_messages, $_messages_fallback, $_messages_common;
if (isset($_messages[$key])) {
$str = $_messages[$key];
}
else if (isset($_messages_fallback[$key])) {
$str = $_messages_fallback[$key];
}
else if (isset($_messages_common[$key])) {
$str = $_messages_common[$key];
}
else{
ob_end_clean();
die('??' . $key . '??');
}
// allow tildes in translation
$str = preg_replace('/(?<=[^ \\\\])~(?=[^ ])/', '&nbsp;', $str);
$str = str_replace('\~', '~', $str);
return $str;
}
/** Like eval, but allows <?php and ?> */

@ -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'

@ -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();

@ -1,22 +0,0 @@
<?php
/* This script is run on demand to generate JS version of tr() */
require_once __DIR__ . '/base.php';
$selected = [
'wifi.connected_ip_is',
'wifi.not_conn',
'wifi.enter_passwd',
];
$out = [];
foreach ($selected as $key) {
$out[$key] = $_messages[$key];
}
file_put_contents(__DIR__. '/js/lang.js',
"// Generated from PHP locale file\n" .
'let _tr = ' . json_encode($out, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) . ";\n\n" .
"module.exports = function tr (key) { return _tr[key] || '?' + key + '?' }\n"
);

@ -61,7 +61,7 @@ $.ready(function () {
}, 1000)
// flipping number boxes with the mouse wheel
$('input[type=number]').on('mousewheel', function (e) {
$('input[type=number]').on('wheel', function (e) {
let $this = $(this)
let val = +$this.val()
if (isNaN(val)) val = 1
@ -69,14 +69,14 @@ $.ready(function () {
const step = +($this.attr('step') || 1)
const min = +$this.attr('min')
const max = +$this.attr('max')
if (e.wheelDelta > 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) {

@ -14,3 +14,5 @@ window.$ = $
window.qs = qs
window.themes = require('./term/themes')
window.TermConf = require('./term_conf')

@ -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}?`
}

@ -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('')
}

@ -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
})
}
}

@ -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 }
}

@ -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)
}
}

@ -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()
})
}

@ -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 <command>\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)

@ -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

@ -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) {

@ -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)

@ -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) : '&nbsp;'
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':

@ -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'

@ -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)) {

@ -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
}

@ -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,

@ -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(
'<span class="preset" ' +
'data-xfg="' + fg + '" data-xbg="' + bg + '" ' +
'style="color:' + fg + ';background:' + bg + '" title="' + tit + '">&nbsp;' + lbl + '&nbsp;</span>')
if ((i + 1) % 5 === 0) $presets.htmlAppend('<br>')
}
$('.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()
}

@ -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

@ -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(`<div class="rssi">${ap.rssi_perc}</div>`)
.htmlAppend(`<div class="essid" title="${escapedSSID}">${escapedSSID}</div>`)
.htmlAppend(`<div class="auth">${authStr[ap.enc]}</div>`)
.htmlAppend(`<div class="auth">${authTypes[ap.enc]}</div>`)
$item.on('click', function () {
let $th = $(this)
@ -164,4 +168,4 @@ const tr = require('./lang')
w.init = wifiInit
w.startScanning = startScanning
})(window.WiFi = {})
}

@ -0,0 +1,14 @@
#! /usr/bin/env php
<?php
require_once __DIR__ . '/../base.php';
$selected = array_slice($argv, 1);
$output = [];
foreach ($selected as $key) {
$output[$key] = tr($key);
}
fwrite(STDOUT, json_encode($output, JSON_UNESCAPED_UNICODE));

@ -0,0 +1,54 @@
/*
* This is a Webpack loader that loads the language data by running
* dump_selected.php.
*/
const { spawnSync } = require('child_process')
const path = require('path')
const selectedKeys = require('./js-keys')
module.exports = function (source) {
let child = spawnSync(path.resolve(__dirname, '_js-dump.php'), selectedKeys, {
timeout: 1000
})
let data
try {
data = JSON.parse(child.stdout.toString().trim())
} catch (err) {
console.error(`\x1b[31;1m[lang-loader] Failed to parse JSON:`)
console.error(child.stdout.toString().trim())
console.error(`\x1b[m`)
if (err) throw err
}
// adapted from webpack/loader-utils
let remainingRequest = this.remainingRequest
if (!remainingRequest) {
remainingRequest = this.loaders.slice(this.loaderIndex + 1)
.map(obj => 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)
}

@ -0,0 +1,19 @@
<?php
return [
'appname' => 'ESPTerm',
'appname_demo' => 'ESPTerm<sup> DEMO</sup>',
// 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' => '',
];

@ -0,0 +1,271 @@
<?php
return [
'menu.cfg_wifi' => '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, <code>\ec</code>). 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<br>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 (<span style="text-decoration:overline">SRM</span>)',
'term.crlf_mode' => 'Enter = CR+LF (LNM)',
'term.want_all_fn' => 'Zachytávat F5, F11, F12',
'term.button_msgs' => 'Reporty tlačítek<br>(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.<br>
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.<br>
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:',
];

@ -0,0 +1,270 @@
<?php
return [
'menu.cfg_wifi' => '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 <code>\ec</code>-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 (<span style="text-decoration:overline">SRM</span>)',
'term.crlf_mode' => 'Enter = CR+LF (LNM)',
'term.want_all_fn' => 'F5, F11, F12 erfassen',
'term.button_msgs' => 'Tastencodes<br>(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.<br>
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.<br>
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&nbsp;MHz auf 160&nbsp;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:',
];

@ -1,9 +1,6 @@
<?php
return [
'appname' => 'ESPTerm',
'appname_demo' => 'ESPTerm<sup> DEMO</sup>',
'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 (<code>\ec</code>). 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 (<code>\ec</code>).
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 (<span style="text-decoration:overline">SRM</span>)',
'term.crlf_mode' => 'Enter = CR+LF (LNM)',
'term.want_all_fn' => 'Capture all keys<br>(F5, F11, F12…)',
'term.want_all_fn' => 'Capture F5, F11, F12',
'term.button_msgs' => 'Button codes<br>(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' => '
@ -152,14 +158,14 @@ return [
'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),
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.",
@ -219,7 +225,8 @@ 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
@ -242,20 +249,23 @@ return [
'hwtuning.title' => 'Hardware Tuning',
'hwtuning.explain' => '
ESP8266 can be overclocked from 80&nbsp;MHz to 160&nbsp;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&nbsp;MHz to 160&nbsp;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:',
];

@ -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'
]

@ -5,8 +5,8 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, maximum-scale=1">
<title><?= $_GET['PAGE_TITLE'] ?></title>
<link href="/css/app.<?= GIT_HASH ?>.css" rel="stylesheet">
<script src="/js/app.<?= GIT_HASH ?>.js"></script>
<link href="/css/app.<?= GIT_HASH . '-' . LOCALE ?>.css" rel="stylesheet">
<script src="/js/app.<?= GIT_HASH . '-' . LOCALE ?>.js"></script>
<script>
var _root = <?= JS_WEB_ROOT ?>;
var _demo = <?= (int)ESP_DEMO ?>;

@ -12,18 +12,22 @@
<div class="Row">
<label for="theme"><?= tr("term.theme") ?></label>
<select name="theme" id="theme" class="short" onchange="showColor()">
<select name="theme" id="theme">
<option value="0">Tango</option>
<option value="1">Linux (CGA)</option>
<option value="2">XTerm</option>
<option value="3">Rxvt</option>
<option value="4">Ambience</option>
<option value="5">Solarized</option>
<option value="5">Solarized Dark</option>
<option value="11">Solarized Dark, high contrast</option>
<option value="10">Solarized Light</option>
<option value="6">CGA NTSC</option>
<option value="7">ZX Spectrum</option>
<option value="8">Apple II</option>
<option value="9">Commodore</option>
</select>
<span onclick="TermConf.prevTheme()" class="mq-no-phone theme-nav-btn"></span>
<span onclick="TermConf.nextTheme()" class="mq-no-phone theme-nav-btn"></span>
</div>
<div class="Row color-preview">
@ -57,25 +61,25 @@
<label><?= tr("term.color_fg_prev") ?></label>
<div>
<div class="colorprev fg">
<span data-fg=0 data-bg=0 style="text-shadow: 0 0 4px white;">0</span><!--
--><span data-fg=1 data-bg=0>1</span><!--
--><span data-fg=2 data-bg=0>2</span><!--
--><span data-fg=3 data-bg=0>3</span><!--
--><span data-fg=4 data-bg=0>4</span><!--
--><span data-fg=5 data-bg=0>5</span><!--
--><span data-fg=6 data-bg=0>6</span><!--
--><span data-fg=7 data-bg=0>7</span>
<span data-fg=0>0</span><!--
--><span data-fg=1>1</span><!--
--><span data-fg=2>2</span><!--
--><span data-fg=3>3</span><!--
--><span data-fg=4>4</span><!--
--><span data-fg=5>5</span><!--
--><span data-fg=6>6</span><!--
--><span data-fg=7>7</span>
</div>
<div class="colorprev fg">
<span data-fg=8 data-bg=0>8</span><!--
--><span data-fg=9 data-bg=0>9</span><!--
--><span data-fg=10 data-bg=0>10</span><!--
--><span data-fg=11 data-bg=0>11</span><!--
--><span data-fg=12 data-bg=0>12</span><!--
--><span data-fg=13 data-bg=0>13</span><!--
--><span data-fg=14 data-bg=0>14</span><!--
--><span data-fg=15 data-bg=0>15</span>
<span data-fg=8>8</span><!--
--><span data-fg=9>9</span><!--
--><span data-fg=10>10</span><!--
--><span data-fg=11>11</span><!--
--><span data-fg=12>12</span><!--
--><span data-fg=13>13</span><!--
--><span data-fg=14>14</span><!--
--><span data-fg=15>15</span>
</div>
</div>
</div>
@ -266,6 +270,19 @@
<span class="mq-no-phone">&nbsp;ms</span>
</div>
<div class="Row checkbox" >
<label><?= tr('term.debugbar') ?></label><!--
--><span class="box" tabindex=0 role=checkbox></span>
<input type="hidden" id="debugbar" name="debugbar" value="%debugbar%">
</div>
<div class="Row checkbox" >
<label><?= tr('term.ascii_debug') ?></label><!--
--><span class="box" tabindex=0 role=checkbox></span>
<input type="hidden" id="ascii_debug" name="ascii_debug" value="%ascii_debug%">
</div>
<div class="Row checkbox" >
<label><?= tr('term.fn_alt_mode') ?></label><!--
--><span class="box" tabindex=0 role=checkbox></span>
@ -284,72 +301,25 @@
<input type="hidden" id="show_config_links" name="show_config_links" value="%show_config_links%">
</div>
<div class="Row checkbox" >
<label><?= tr('term.allow_decopt_12') ?></label><!--
--><span class="box" tabindex=0 role=checkbox></span>
<input type="hidden" id="allow_decopt_12" name="allow_decopt_12" value="%allow_decopt_12%">
</div>
<div class="Row buttons">
<a class="button icn-ok" href="#" onclick="qs('#form-expert').submit()"><?= tr('apply') ?></a>
</div>
</form>
<script>
$('#cursor_shape').val(%cursor_shape%);
$('#theme').val(%theme%);
$('#uart_baud').val(%uart_baud%);
$('#uart_parity').val(%uart_parity%);
$('#uart_stopbits').val(%uart_stopbits%);
function showColor() {
var ex = qs('.color-example');
var fg = $('#default_fg').val();
var 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';
}
ex.dataset.fg = fg;
ex.dataset.bg = bg;
themes.themePreview(+$('#theme').val())
}
showColor();
$('#default_fg').on('input', showColor)
$('#default_bg').on('input', showColor)
$('.colorprev.bg span').on('click', function() {
var bg = this.dataset.bg;
if (typeof bg != 'undefined') $('#default_bg').val(bg);
showColor()
});
$('.colorprev.fg span').on('click', function() {
var fg = this.dataset.fg;
if (typeof fg != 'undefined') $('#default_fg').val(fg);
showColor()
});
var $presets = $('#fgbg_presets');
for(var i = 0; i < themes.fgbgThemes.length; i++) {
fg = themes.fgbgThemes[i][0];
bg = themes.fgbgThemes[i][1];
$presets
.htmlAppend(
'<span class="preset" ' +
'data-xfg="'+fg+'" data-xbg="'+bg+'" ' +
'style="color:'+fg+';background:'+bg+'">&nbsp;['+i+']&nbsp;</span>');
if ((i+1)%5==0) $presets.htmlAppend('<br>');
}
$('.preset').on('click', function() {
$('#default_fg').val(this.dataset.xfg)
$('#default_bg').val(this.dataset.xbg)
showColor()
});
$.ready(function () {
$('#cursor_shape').val('%cursor_shape%');
$('#theme').val('%theme%');
$('#uart_baud').val('%uart_baud%');
$('#uart_parity').val('%uart_parity%');
$('#uart_stopbits').val('%uart_stopbits%');
TermConf.init();
});
</script>

@ -16,6 +16,7 @@
<?php require __DIR__ . "/help/sgr_colors.php"; ?>
<?php require __DIR__ . "/help/cmd_cursor.php"; ?>
<?php require __DIR__ . "/help/cmd_screen.php"; ?>
<?php require __DIR__ . "/help/cmd_d2d.php"; ?>
<?php require __DIR__ . "/help/cmd_system.php"; ?>
<script>

@ -0,0 +1,92 @@
<div class="Box fold">
<h2>Commands: Networking</h2>
<div class="Row v">
<p>
ESPTerm implements commands for device-to-device messaging and for requesting external
servers. This can be used e.g. for remote control, status reporting or data upload / download.
</p>
<p>
Networking commands use the format `\e^...\a`, a Privacy Message (PM).
PM is similar to OSC, which uses `]` in place of `^`. The PM payload (text between `\e^` and `\a`)
must be shorter than 256 bytes, and should not contain any control characters (ASCII < 32).
</p>
<h3>Device-to-device Messaging</h3>
<p>
To send a message to another ESPTerm module, use: `\e^M;<i>DestIP</i>;<i>message</i>\a`.
</p>
<p>
This command sends a POST request to `http://<i>&lt;DestIP&gt;</i>/api/v1/msg`.
The IP address may be appended by a port, if needed (eg. :8080). In addition to POST,
a GET request can also be used. In that case, any GET arguments (`/api/v1/msg?<i>arguments</i>`)
will be used instead of the request body. This is intended for external access
when sending POST requests is not convenient.
</p>
<p>
Each ESPTerm listens for such requests and relays them to UART:
`\e^m;<i>SrcIP</i>;L=<i>length</i>;<i>message</i>\a`, with _length_ being the byte length of
_message_, as ASCII.
</p>
<p>
Notice a pattern with the first letter: capital is always a command, lower case a response.
This is followed with the HTTP commands and any networking commands added in the future.
</p>
<p>
*Example:* Node 192.168.0.10 sends a message to 192.168.0.19: `\e^M;192.168.0.19;Hello\a`.
Node 192.168.0.19 receives `\e^m;192.168.0.10;L=5;Hello\a` on the UART. Note that the IP
address in the reception message is that of the first node, thus it can be used to send a message back.
</p>
<h3>External HTTP requests</h3>
<p>
To request an external server, use `\e^H;<i>method</i>;<i>options</i>;<i>url</i>\n<i>body</i>\a`.
</p>
<ul>
<li>`_method_` - can be any usual HTTP verb, such as `GET`, `POST`, `PUT`, `HEAD`.
<li>`_options_` - is a comma-separated list of flags and parameters:
<ul>
<li>`H` - get response headers
<li>`B` - get response body
<li>`X` - ignore the response, return nothing
<li>`N=<i>nonce</i>` - a custom string that will be added in the options field of the response message.
Use this to keep track of which request a response belongs to.
<li>`T=<i>ms</i>` - request timeout (default 5000~ms), in milliseconds
<li>`L=<i>bytes</i>` - limit response length (default 0 = don't limit). Applies to the head, body, or both combined, depending on the `H` and `B` flags
<li>`l=<i>bytes</i>` - limit the response buffer size (default 5000~B).
This can reduce RAM usage, however it shouldn't be set too small, as this buffer
is used for both headers and the response body.
</ul>
<li>`_url_` - full request URL, including `http://`. Port may be specified if different from :80,
and GET arguments may be appended to the URL if needed.
<li>`_body_` - optional, separated from `_url_` by a single line feed character (`\n`).
This can be used for POST and PUT requests. Note: the command may be truncated to the
maximum total length of 256 characters if too long.
</ul>
<p>The response has the following format: `\e^h;<i>status</i>;<i>options</i>;<i>response</i>\a`</p>
<ul>
<li>`_status_` - a HTTP status code, eg. 200 is OK, 404 Not found.
<li>`_options_` - similar to those in the request, here describing the response data.
This field can contain comma-separated `B`, `H` and `L=<i>bytes</i>` and `N=<i>nonce</i>`.
<li>`_response_` - the response, as requested. If both headers and body are received,
they will be separated by an empty line (i.e. `\r\n\r\n`). Response can be up to several
kilobytes long, depending on the `L=` and `l=` options.
</ul>
<p>
*Example:* `\e^H;GET;B;http://wtfismyip.com/text\a` - get the body of a web page
(wtfismyip.com is a service that sends back your IP address).
A response could be `\e^h;200;B,L=11;80.70.60.50\a`.
</p>
</div>
</div>

@ -21,6 +21,7 @@
Here are some examples of control key codes:
</p>
<div class="tscroll">
<table>
<thead><tr><th>Key</th><th>Code</th><th>Key</th><th>Code</th></tr></thead>
<tr>
@ -108,6 +109,7 @@
<td>ASCII 1-26</td>
</tr>
</table>
</div>
<h3>Action buttons</h3>

@ -1,6 +1,6 @@
<div class="Box fold">
<h2>Commands: Color attributes</h2>
<h2>Commands: Color Attributes</h2>
<div class="Row v">
<p>

@ -1,5 +1,5 @@
<div class="Box fold">
<h2>Commands: Style attributes</h2>
<h2>Commands: Style Attributes</h2>
<div class="Row v">
<p>

@ -12,15 +12,14 @@
appear on the screen. Set _Parser Timeout = 0_ in <a href="<?= url('cfg_term') ?>">Terminal Settings</a>
to be able to manually enter escape sequences.
<li>There is very little RAM available to the webserver, and it can support at most 4 connections at the same time.
Each terminal session (open window with the terminal screen) uses one persistent connection for screen updates.
*Avoid leaving unused windows open*, or either the RAM or connections may be exhausted.
<li>Use Ctrl+F12 to open a screen debug panel. Additional debugging can be enabled in the
<a href="<?= url('cfg_term') ?>">Terminal Settings</a> (near the bottom).
<li>*For best performance*, use the module in Client mode (connected to external network) and minimize the number
of simultaneous connections. Enabling AP consumes extra RAM because the DHCP server and Captive Portal
DNS server are started.
<li>In AP mode, *check that the WiFi channel used is clear*; interference may cause flaky connection.
<li>*Check that the WiFi channel used is clear*; interference may cause flaky connection.
A good mobile app to use for this is
<a href="https://play.google.com/store/apps/details?id=com.farproc.wifi.analyzer">WiFi Analyzer (Google Play)</a>.
Adjust the hotspot strength and range using the _Tx Power setting_.

@ -13,51 +13,53 @@
<div class="Modal light hidden" id="fu_modal">
<div id="fu_form" class="Dialog">
<div class="fu-content">
<h2>Text Upload</h2>
<h2><?= tr('upload.title') ?></h2>
<p>
<label for="fu_file">Load a text file:</label>
<label for="fu_file"><?= tr('upload.prompt') ?></label>
<input type="file" id="fu_file" accept="text/*" /><br>
<textarea id="fu_text"></textarea>
</p>
<p>
<label for="fu_crlf">Line Endings:</label>
<label for="fu_crlf"><?= tr('upload.endings') ?></label>
<select id="fu_crlf">
<option value="CR">CR (Enter key)</option>
<option value="CRLF">CR LF (Windows)</option>
<option value="LF">LF (Linux)</option>
<option value="CR"><?= tr('upload.endings.cr') ?></option>
<option value="CRLF"><?= tr('upload.endings.crlf') ?></option>
<option value="LF"><?= tr('upload.endings.lf') ?></option>
</select>
</p>
<p>
<label for="fu_delay">Line Delay (ms):</label>
<label for="fu_delay"><?= tr('upload.chunk_delay') ?></label>
<input id="fu_delay" type="number" value=1 min=0>
</p>
<p>
<label for="fu_chunk"><?= tr('upload.chunk_size') ?></label>
<input id="fu_chunk" type="number" value=0 min=0 max=100>
</p>
</div>
<div class="fu-buttons">
<button id="term-fu-start" class="icn-ok x-fu-go">Start</button>&nbsp;
<button id="term-fu-close" class="icn-cancel x-fu-cancel">Cancel</button>&nbsp;
<i class="fu-prog-box">Upload: <span id="fu_prog"></span></i>
<button id="term-fu-start" class="icn-ok x-fu-go"><?= tr('start') ?></button>&nbsp;
<button id="term-fu-close" class="icn-cancel x-fu-cancel"><?= tr('cancel') ?></button>&nbsp;
<i class="fu-prog-box"><?= tr('upload.progress') ?> <span id="fu_prog"></span></i>
</div>
</div>
</div>
<h1 id="screen-title"><!-- Screen title is loaded here by JS --></h1>
<h1 id="screen-title"><!-- JS, title --></h1>
<a href="#" id="term-fit-screen" class="mq-tablet-max"><i id="resize-button-icon" class="icn-resize-small"></i></a>
<div id="term-wrap">
<div id="screen">
<input id="softkb-input" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
<div id="touch-select-menu">
<button id="touch-select-copy-btn">Copy</button>
<button id="touch-select-copy-btn"><?= tr('copy') ?></button>
</div>
<div class="screen-margin top"></div>
<div class="screen-margin left"></div>
<div class="screen-margin right"></div>
<div class="screen-margin bottom"></div>
</div>
<div id="action-buttons">
<button data-n="1"></button><!--
--><button data-n="2"></button><!--
--><button data-n="3"></button><!--
--><button data-n="4"></button><!--
--><button data-n="5"></button>
</div>
<div id="action-buttons"><!-- JS, buttons --></div>
</div>
<nav id="term-nav">
@ -72,13 +74,7 @@
<script>
try {
window.noAutoShow = true;
termInit({
labels: '%j:labels_seq%',
theme: +'%theme%',
defaultFg: +'%default_fg%',
defaultBg: +'%default_bg%',
allFn: !!+'%want_all_fn%',
});
termInit({ allFn: !!+'%want_all_fn%', });
} catch (e) {
console.error(e);
<?php if (!DEBUG): ?>

@ -7,7 +7,7 @@
@import "utils";
$form-label-w: 180px;
$form-label-w: 220px;
$form-label-gap: 8px;
$form-field-w: 250px;
@ -20,6 +20,7 @@ $c-form-highlight-a: #2ea1f9;
$c-modal-bg: #242426;
$font-stack: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif;
$screen-stack: "DejaVu Sans Mono", "Liberation Mono", "Inconsolata", "Menlo", monospace;
@function dist($x) {

@ -17,7 +17,7 @@ input.tiny, select.tiny {
border-bottom: 2px solid $c-form-highlight;
background-color: $c-form-field-bg;
color: $c-form-field-fg;
padding: 6px;
padding: 4px 6px;
line-height: 1em;
//outline: 0 none !important;
//-moz-outline: 0 none !important;

@ -1,10 +1,10 @@
html {
font-family: Arial, sans-serif;
font-family: $font-stack;
color: #D0D0D0;
background: #131315;
}
html, body {
background: #131315;
@include naked();
width: 100%;
height: 100%;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save