Compare commits
404 Commits
legacy-scr
...
master
@ -0,0 +1,14 @@ |
||||
{ |
||||
"presets": [ |
||||
["env", { |
||||
"targets": { |
||||
"browsers": [ |
||||
"last 2 versions", |
||||
"> 4%", |
||||
"ie 11", |
||||
"safari 8" |
||||
] |
||||
} |
||||
}] |
||||
] |
||||
} |
@ -0,0 +1,9 @@ |
||||
# possibly minified output |
||||
out/**/* |
||||
|
||||
# libraries |
||||
js/lib/chibi.js |
||||
js/lib/polyfills.js |
||||
|
||||
# php generated file |
||||
js/lang.js |
@ -0,0 +1,191 @@ |
||||
{ |
||||
"parserOptions": { |
||||
"ecmaVersion": 8, |
||||
"ecmaFeatures": { |
||||
"experimentalObjectRestSpread": true, |
||||
"jsx": true |
||||
}, |
||||
"sourceType": "module" |
||||
}, |
||||
|
||||
"env": { |
||||
"es6": true, |
||||
"node": true |
||||
}, |
||||
|
||||
"plugins": [ |
||||
"import", |
||||
"node", |
||||
"promise", |
||||
"standard" |
||||
], |
||||
|
||||
"globals": { |
||||
"document": false, |
||||
"navigator": false, |
||||
"window": false |
||||
}, |
||||
|
||||
"rules": { |
||||
"accessor-pairs": "error", |
||||
"arrow-spacing": ["error", { "before": true, "after": true }], |
||||
"block-spacing": ["error", "always"], |
||||
"brace-style": ["warn", "1tbs", { "allowSingleLine": true }], |
||||
"camelcase": ["off", { "properties": "never" }], |
||||
"comma-dangle": ["error", { |
||||
"arrays": "never", |
||||
"objects": "never", |
||||
"imports": "never", |
||||
"exports": "never", |
||||
"functions": "never" |
||||
}], |
||||
"comma-spacing": ["error", { "before": false, "after": true }], |
||||
"comma-style": ["error", "last"], |
||||
"constructor-super": "error", |
||||
"curly": ["error", "multi-line"], |
||||
"dot-location": ["error", "property"], |
||||
"eol-last": "error", |
||||
"eqeqeq": ["error", "smart"], |
||||
"func-call-spacing": ["error", "never"], |
||||
"generator-star-spacing": ["error", { "before": true, "after": true }], |
||||
"handle-callback-err": ["error", "^(err|error)$" ], |
||||
"indent": ["error", 2, { "SwitchCase": 1 }], |
||||
"key-spacing": ["error", { "beforeColon": false, "afterColon": true }], |
||||
"keyword-spacing": ["error", { "before": true, "after": true }], |
||||
"new-cap": ["error", { "newIsCap": true, "capIsNew": false }], |
||||
"new-parens": "error", |
||||
"no-array-constructor": "error", |
||||
"no-caller": "error", |
||||
"no-class-assign": "error", |
||||
"no-compare-neg-zero": "error", |
||||
"no-cond-assign": "error", |
||||
"no-const-assign": "error", |
||||
"no-constant-condition": ["error", { "checkLoops": false }], |
||||
"no-control-regex": "error", |
||||
"no-debugger": "error", |
||||
"no-delete-var": "error", |
||||
"no-dupe-args": "error", |
||||
"no-dupe-class-members": "error", |
||||
"no-dupe-keys": "error", |
||||
"no-duplicate-case": "error", |
||||
"no-empty-character-class": "error", |
||||
"no-empty-pattern": "error", |
||||
"no-eval": "error", |
||||
"no-ex-assign": "error", |
||||
"no-extend-native": "warn", |
||||
"no-extra-bind": "error", |
||||
"no-extra-boolean-cast": "error", |
||||
"no-extra-parens": ["error", "functions"], |
||||
"no-fallthrough": "error", |
||||
"no-floating-decimal": "error", |
||||
"no-func-assign": "error", |
||||
"no-global-assign": "error", |
||||
"no-implied-eval": "error", |
||||
"no-inner-declarations": ["error", "functions"], |
||||
"no-invalid-regexp": "error", |
||||
"no-irregular-whitespace": "error", |
||||
"no-iterator": "error", |
||||
"no-label-var": "error", |
||||
"no-labels": ["error", { "allowLoop": false, "allowSwitch": false }], |
||||
"no-lone-blocks": "warn", |
||||
"no-mixed-operators": ["error", { |
||||
"groups": [ |
||||
["==", "!=", "===", "!==", ">", ">=", "<", "<="], |
||||
["&&", "||"], |
||||
["in", "instanceof"] |
||||
], |
||||
"allowSamePrecedence": true |
||||
}], |
||||
"no-mixed-spaces-and-tabs": "error", |
||||
"no-multi-spaces": "warn", |
||||
"no-multi-str": "error", |
||||
"no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 0 }], |
||||
"no-negated-in-lhs": "error", |
||||
"no-new": "error", |
||||
"no-new-func": "error", |
||||
"no-new-object": "error", |
||||
"no-new-require": "error", |
||||
"no-new-symbol": "error", |
||||
"no-new-wrappers": "error", |
||||
"no-obj-calls": "error", |
||||
"no-octal": "error", |
||||
"no-octal-escape": "error", |
||||
"no-path-concat": "error", |
||||
"no-proto": "error", |
||||
"no-redeclare": "error", |
||||
"no-regex-spaces": "error", |
||||
"no-return-assign": ["error", "except-parens"], |
||||
"no-return-await": "error", |
||||
"no-self-assign": "error", |
||||
"no-self-compare": "error", |
||||
"no-sequences": "error", |
||||
"no-shadow-restricted-names": "error", |
||||
"no-sparse-arrays": "error", |
||||
"no-tabs": "error", |
||||
"no-template-curly-in-string": "error", |
||||
"no-this-before-super": "error", |
||||
"no-throw-literal": "error", |
||||
"no-trailing-spaces": "off", |
||||
"no-undef": "warn", |
||||
"no-undef-init": "error", |
||||
"no-unexpected-multiline": "error", |
||||
"no-unmodified-loop-condition": "error", |
||||
"no-unneeded-ternary": ["error", { "defaultAssignment": false }], |
||||
"no-unreachable": "error", |
||||
"no-unsafe-finally": "error", |
||||
"no-unsafe-negation": "error", |
||||
"no-unused-expressions": ["warn", { "allowShortCircuit": true, "allowTernary": true, "allowTaggedTemplates": true }], |
||||
"no-unused-vars": ["warn", { "vars": "local", "args": "none", "ignoreRestSiblings": true }], |
||||
"no-use-before-define": ["error", { "functions": false, "classes": false, "variables": false }], |
||||
"no-useless-call": "error", |
||||
"no-useless-computed-key": "error", |
||||
"no-useless-constructor": "error", |
||||
"no-useless-escape": "error", |
||||
"no-useless-rename": "error", |
||||
"no-useless-return": "error", |
||||
"no-whitespace-before-property": "error", |
||||
"no-with": "error", |
||||
"object-property-newline": ["error", { "allowMultiplePropertiesPerLine": true }], |
||||
"one-var": ["error", { "initialized": "never" }], |
||||
"operator-linebreak": ["error", "after", { "overrides": { "?": "before", ":": "before" } }], |
||||
"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"], |
||||
"semi": ["error", "never"], |
||||
"semi-spacing": ["error", { "before": false, "after": true }], |
||||
"space-before-blocks": ["error", "always"], |
||||
"space-before-function-paren": ["error", "always"], |
||||
"space-in-parens": ["error", "never"], |
||||
"space-infix-ops": "error", |
||||
"space-unary-ops": ["error", { "words": true, "nonwords": false }], |
||||
"spaced-comment": ["error", "always", { |
||||
"line": { "markers": ["*package", "!", "/", ","] }, |
||||
"block": { "balanced": true, "markers": ["*package", "!", ",", ":", "::", "flow-include"], "exceptions": ["*"] } |
||||
}], |
||||
"symbol-description": "error", |
||||
"template-curly-spacing": ["error", "never"], |
||||
"template-tag-spacing": ["error", "never"], |
||||
"unicode-bom": ["error", "never"], |
||||
"use-isnan": "error", |
||||
"valid-typeof": ["error", { "requireStringLiterals": true }], |
||||
"wrap-iife": ["error", "any", { "functionPrototypeMethods": true }], |
||||
"yield-star-spacing": ["error", "both"], |
||||
"yoda": ["error", "never"], |
||||
|
||||
"import/export": "error", |
||||
"import/first": "error", |
||||
"import/no-duplicates": "error", |
||||
"import/no-webpack-loader-syntax": "error", |
||||
|
||||
"node/no-deprecated-api": "error", |
||||
"node/process-exit-as-throw": "error", |
||||
|
||||
"promise/param-names": "error", |
||||
|
||||
"standard/array-bracket-even-spacing": ["error", "either"], |
||||
"standard/computed-property-even-spacing": ["error", "even"], |
||||
"standard/no-callback-literal": "error", |
||||
"standard/object-curly-even-spacing": ["error", "either"] |
||||
} |
||||
} |
@ -0,0 +1,16 @@ |
||||
#!/bin/bash |
||||
source "_build_common.sh" |
||||
|
||||
echo 'Copying resources...' |
||||
|
||||
cp -r img out/img |
||||
cp favicon.ico out/ |
||||
|
||||
if [[ $ESP_PROD ]]; then |
||||
echo 'Cleaning junk files...' |
||||
find out/ -name "*.orig" -delete |
||||
find out/ -name "*.xcf" -delete |
||||
find out/ -name "*~" -delete |
||||
find out/ -name "*.bak" -delete |
||||
find out/ -name "*.map" -delete |
||||
fi |
@ -0,0 +1,7 @@ |
||||
#!/bin/bash |
||||
|
||||
export FRONT_END_HASH=$(git rev-parse --short HEAD) |
||||
|
||||
if [ -z "$ESP_LANG" ]; then |
||||
export ESP_LANG=en |
||||
fi |
@ -0,0 +1,13 @@ |
||||
#!/bin/bash |
||||
source "_build_common.sh" |
||||
|
||||
echo 'Building CSS...' |
||||
|
||||
if [[ $ESP_PROD ]]; then |
||||
stylearg=compressed |
||||
else |
||||
stylearg=expanded |
||||
fi |
||||
|
||||
mkdir -p out/css |
||||
npm run sass -- --output-style ${stylearg} sass/app.scss "out/css/app.$FRONT_END_HASH-$ESP_LANG.css" |
@ -0,0 +1,6 @@ |
||||
#!/bin/bash |
||||
source "_build_common.sh" |
||||
|
||||
echo 'Building HTML...' |
||||
|
||||
php ./compile_html.php $@ |
@ -0,0 +1,7 @@ |
||||
#!/bin/bash |
||||
source "_build_common.sh" |
||||
|
||||
mkdir -p out/js |
||||
|
||||
echo 'Processing JS...' |
||||
npm run webpack |
@ -0,0 +1,13 @@ |
||||
<?php |
||||
|
||||
if (preg_match('/\\/(?:js|css)/', $_SERVER["REQUEST_URI"])) { |
||||
$path = pathinfo($_SERVER["REQUEST_URI"]); |
||||
if ($path["extension"] == "js") { |
||||
header("Content-Type: application/javascript"); |
||||
} else if ($path["extension"] == "css") { |
||||
header("Content-Type: text/css"); |
||||
} |
||||
readfile("out" . $_SERVER["REQUEST_URI"]); |
||||
} else { |
||||
return false; |
||||
} |
@ -1,25 +1,14 @@ |
||||
#!/bin/bash |
||||
|
||||
echo "Packing JS..." |
||||
cd $(dirname $0) |
||||
|
||||
cat jssrc/chibi.js \ |
||||
jssrc/keymaster.js \ |
||||
jssrc/utils.js \ |
||||
jssrc/modal.js \ |
||||
jssrc/notif.js \ |
||||
jssrc/appcommon.js \ |
||||
jssrc/lang.js \ |
||||
jssrc/wifi.js \ |
||||
jssrc/term_* \ |
||||
jssrc/term.js > js/app-full.js |
||||
source "_build_common.sh" |
||||
|
||||
yuicompressor js/app-full.js > js/app.js |
||||
rm -fr out/* |
||||
|
||||
echo "Building CSS..." |
||||
./_build_css.sh |
||||
./_build_js.sh $@ |
||||
./_build_html.sh $@ |
||||
./_build_assets.sh |
||||
|
||||
sass --style=compressed sass/app.scss css/app.css |
||||
|
||||
echo "Building HTML..." |
||||
php ./build_html.php |
||||
|
||||
echo "ESPTerm front-end ready" |
||||
echo 'ESPTerm front-end ready' |
||||
|
@ -1,50 +0,0 @@ |
||||
<?php |
||||
|
||||
require_once __DIR__ . '/base.php'; |
||||
|
||||
function process_html($s) { |
||||
$pattern = '/<!--(.*)-->/Uis'; |
||||
$s = preg_replace($pattern, '', $s); |
||||
|
||||
$pattern = '/(?:(?:\/\*(?:[^*]|(?:\*+[^*\/]))*\*+\/)|(?:(?<!\:|\\\|\'|\")\/\/.*))/'; |
||||
$s = preg_replace($pattern, '', $s); |
||||
|
||||
$pattern = '/\s+/s'; |
||||
$s = preg_replace($pattern, ' ', $s); |
||||
return $s; |
||||
} |
||||
|
||||
$no_tpl_files = ['help', 'cfg_wifi_conn']; |
||||
|
||||
$dest = __DIR__ . '/out/'; |
||||
|
||||
ob_start(); |
||||
foreach($_pages as $_k => $p) { |
||||
if ($p->bodyclass == 'api') { |
||||
if (ESP_DEMO) { |
||||
$target = 'term.html'; |
||||
echo "Generating: ~$_k.html -> $target\n"; |
||||
$s = "<!DOCTYPE HTML><meta http-equiv=\"refresh\" content=\"0;url=$target\">"; |
||||
} else { |
||||
continue; |
||||
} |
||||
} else { |
||||
echo "Generating: $_k ($p->title)\n"; |
||||
|
||||
$_GET['page'] = $_k; |
||||
ob_flush(); // print the message |
||||
ob_clean(); // clean up |
||||
include(__DIR__ . '/index.php'); |
||||
$s = ob_get_contents(); // grab the output |
||||
|
||||
// remove newlines and comments |
||||
// as tests have shown, it saves just a couple kilobytes, |
||||
// 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 |
||||
} |
||||
|
||||
ob_flush(); |
@ -0,0 +1,85 @@ |
||||
<?php |
||||
|
||||
require_once __DIR__ . '/base.php'; |
||||
|
||||
function process_html($s) { |
||||
$pattern = '/<!--(.*)-->/Uis'; |
||||
$s = preg_replace($pattern, '', $s); |
||||
|
||||
$pattern = '/(?:(?:\/\*(?:[^*]|(?:\*+[^*\/]))*\*+\/)|(?:(?<!\:|\\\|\'|\")\/\/.*))/'; |
||||
$s = preg_replace($pattern, '', $s); |
||||
|
||||
$pattern = '/\s+/s'; |
||||
$s = preg_replace($pattern, ' ', $s); |
||||
return $s; |
||||
} |
||||
|
||||
$no_tpl_files = ['help', 'cfg_wifi_conn']; |
||||
|
||||
$dest = __DIR__ . '/out/'; |
||||
|
||||
ob_start(); |
||||
foreach($_pages as $_k => $p) { |
||||
if ($p->bodyclass == 'api') { |
||||
if (ESP_DEMO) { |
||||
echo "Generating: ~$_k.html (bounce)\n"; |
||||
|
||||
if ($_k=='index') { |
||||
$s = "<!DOCTYPE HTML><meta http-equiv=\"refresh\" content=\"0;url=term.html\">"; |
||||
} |
||||
else { |
||||
$s = "<!DOCTYPE HTML> |
||||
<script> |
||||
var ref = document.referrer; |
||||
var qat = document.referrer.indexOf('?'); |
||||
if (qat !== -1) ref = ref.substring(0, qat) |
||||
location.href=ref+'?msg=Request ignored, this is a demo.'; |
||||
</script>"; |
||||
} |
||||
|
||||
|
||||
} else { |
||||
continue; |
||||
} |
||||
} else { |
||||
echo "Generating: $_k ($p->title)\n"; |
||||
|
||||
$_GET['page'] = $_k; |
||||
ob_flush(); // print the message |
||||
ob_clean(); // clean up |
||||
include(__DIR__ . '/index.php'); |
||||
$s = ob_get_contents(); // grab the output |
||||
|
||||
// remove newlines and comments |
||||
// as tests have shown, it saves just a couple kilobytes, |
||||
// making it not a very big improvement at the expense of ugly html. |
||||
// $s = process_html($s); |
||||
ob_clean(); |
||||
} |
||||
|
||||
$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); |
||||
system('npm run html-minifier --'. |
||||
' --remove-optional-tags'. |
||||
' --remove-script-type-attributes'. |
||||
' --remove-style-link-type-attributes'. |
||||
' --remove-comments'. |
||||
' --collapse-whitespace'. |
||||
' --collapse-boolean-attributes'. |
||||
' --html5'. |
||||
//' --max-line-length 120'. |
||||
' -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,23 +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', |
||||
'wifi.passwd_saved', |
||||
]; |
||||
|
||||
$out = []; |
||||
foreach ($selected as $key) { |
||||
$out[$key] = $_messages[$key]; |
||||
} |
||||
|
||||
file_put_contents(__DIR__. '/jssrc/lang.js', |
||||
"// Generated from PHP locale file\n" . |
||||
'var _tr = ' . json_encode($out, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) . ";\n\n" . |
||||
"function tr(key) { return _tr[key] || '?'+key+'?'; }\n" |
||||
); |
Binary file not shown.
@ -0,0 +1,205 @@ |
||||
const $ = require('./lib/chibi') |
||||
const { mk, qs, qsa, cr } = require('./utils') |
||||
const modal = require('./modal') |
||||
const notify = require('./notif') |
||||
|
||||
/** Global generic init */ |
||||
$.ready(function () { |
||||
// Opening menu on mobile / narrow screen
|
||||
function menuOpen () { |
||||
$('#menu').toggleClass('expanded') |
||||
} |
||||
$('#brand') |
||||
.on('click', menuOpen) |
||||
.on('keypress', cr(menuOpen)) |
||||
|
||||
// Checkbox UI (checkbox CSS and hidden input with int value)
|
||||
$('.Row.checkbox').forEach(function (x) { |
||||
let inp = x.querySelector('input') |
||||
let box = x.querySelector('.box') |
||||
|
||||
$(box).toggleClass('checked', inp.value) |
||||
|
||||
let hdl = function () { |
||||
inp.value = 1 - inp.value |
||||
$(box).toggleClass('checked', inp.value) |
||||
} |
||||
|
||||
$(x).on('click', hdl).on('keypress', cr(hdl)) |
||||
}) |
||||
|
||||
// Expanding boxes on mobile
|
||||
$('.Box.mobcol,.Box.fold').forEach(function (x) { |
||||
let h = x.querySelector('h2') |
||||
|
||||
let hdl = function () { |
||||
if ($(x).hasClass('d-expanded')) { |
||||
$(x).removeClass('d-expanded') |
||||
} else { |
||||
$(x).toggleClass('expanded') |
||||
} |
||||
} |
||||
$(h).on('click', hdl).on('keypress', cr(hdl)) |
||||
}) |
||||
|
||||
$('form').forEach(function (x) { |
||||
$(x).on('keypress', function (e) { |
||||
if ((e.keyCode === 10 || e.keyCode === 13) && e.ctrlKey) { |
||||
x.submit() |
||||
} |
||||
}) |
||||
}) |
||||
|
||||
// loader dots...
|
||||
setInterval(function () { |
||||
$('.anim-dots').each(function (x) { |
||||
let $x = $(x) |
||||
let dots = $x.html() + '.' |
||||
if (dots.length === 5) dots = '.' |
||||
$x.html(dots) |
||||
}) |
||||
}, 1000) |
||||
|
||||
// flipping number boxes with the mouse wheel
|
||||
$('input[type=number]').on('wheel', function (e) { |
||||
let $this = $(this) |
||||
let val = +$this.val() |
||||
if (isNaN(val)) val = 1 |
||||
|
||||
const step = +($this.attr('step') || 1) |
||||
const min = +$this.attr('min') |
||||
const max = +$this.attr('max') |
||||
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) |
||||
$this.val(val) |
||||
|
||||
if ('createEvent' in document) { |
||||
let evt = document.createEvent('HTMLEvents') |
||||
evt.initEvent('change', false, true) |
||||
$this[0].dispatchEvent(evt) |
||||
} else { |
||||
$this[0].fireEvent('onchange') |
||||
} |
||||
|
||||
e.preventDefault() |
||||
}) |
||||
|
||||
try { |
||||
do { |
||||
let msgAt, box |
||||
// populate the form errors box from GET arg ?err=...
|
||||
// (a way to pass errors back from server via redirect)
|
||||
msgAt = window.location.search.indexOf('err=') |
||||
if (msgAt !== -1 && qs('.Box.errors')) { |
||||
let errs = decodeURIComponent(window.location.search.substr(msgAt + 4)).split(',') |
||||
let humanReadableErrors = [] |
||||
errs.forEach(function (er) { |
||||
if (er.length === 0) return |
||||
let lbls = qsa('label[for="' + er + '"]') |
||||
if (lbls && lbls.length > 0) { |
||||
for (let i = 0; i < lbls.length; i++) { |
||||
let lbl = lbls[i] |
||||
lbl.classList.add('error') |
||||
if (i === 0) humanReadableErrors.push(lbl.childNodes[0].textContent.trim().replace(/: ?$/, '')) |
||||
} |
||||
} else { |
||||
console.log(JSON.stringify(er)) |
||||
humanReadableErrors.push(er) |
||||
} |
||||
}) |
||||
|
||||
qs('.Box.errors .list').innerHTML = humanReadableErrors.join(', ') |
||||
qs('.Box.errors').classList.remove('hidden') |
||||
break |
||||
} |
||||
|
||||
let fademsgbox = function (box, time) { |
||||
box.classList.remove('hidden') |
||||
setTimeout(() => { |
||||
box.classList.add('hiding') |
||||
setTimeout(() => { |
||||
box.classList.add('hidden') |
||||
}, 1000) |
||||
}, time) |
||||
} |
||||
|
||||
msgAt = window.location.search.indexOf('errmsg=') |
||||
box = qs('.Box.errmessage') |
||||
if (msgAt !== -1 && box) { |
||||
let msg = decodeURIComponent(window.location.search.substr(msgAt + 7)) |
||||
box.innerHTML = msg |
||||
fademsgbox(box, 3000) |
||||
break |
||||
} |
||||
|
||||
msgAt = window.location.search.indexOf('msg=') |
||||
box = qs('.Box.message') |
||||
if (msgAt !== -1 && box) { |
||||
let msg = decodeURIComponent(window.location.search.substr(msgAt + 4)) |
||||
box.innerHTML = msg |
||||
fademsgbox(box, 2000) |
||||
break |
||||
} |
||||
} while (0) |
||||
} catch (e) { |
||||
console.error(e) |
||||
} |
||||
|
||||
modal.init() |
||||
notify.init() |
||||
|
||||
// remove tabindices from h2 if wide
|
||||
if (window.innerWidth > 550) { |
||||
$('.Box h2').forEach(function (x) { |
||||
x.removeAttribute('tabindex') |
||||
}) |
||||
|
||||
// brand works as a link back to term in widescreen mode
|
||||
let br = qs('#brand') |
||||
br && br.addEventListener('click', function () { |
||||
window.location.href = '/' // go to terminal
|
||||
}) |
||||
} |
||||
}) |
||||
|
||||
// setup the ajax loader
|
||||
$._loader = function (vis) { |
||||
$('#loader').toggleClass('show', vis) |
||||
} |
||||
|
||||
let pageShown = false |
||||
// reveal content on load
|
||||
function showPage () { |
||||
pageShown = true |
||||
$('#content').addClass('load') |
||||
} |
||||
// HACKITY HACK: fix this later
|
||||
window.showPage = showPage |
||||
|
||||
// Auto reveal pages other than the terminal (sets window.noAutoShow)
|
||||
$.ready(function () { |
||||
if (window.noAutoShow === true) { |
||||
setTimeout(function () { |
||||
if (!pageShown) { |
||||
let bnr = mk('P') |
||||
bnr.id = 'load-failed' |
||||
bnr.innerHTML = |
||||
'Server connection failed! Trying again' + |
||||
'<span class="anim-dots" style="width:1.5em;text-align:left;display:inline-block">.</span>' |
||||
qs('#screen').appendChild(bnr) |
||||
qs('#screen').classList.add('failed') |
||||
showPage() |
||||
} |
||||
}, 2000) |
||||
} else { |
||||
setTimeout(function () { |
||||
showPage() |
||||
}, 1) |
||||
} |
||||
}) |
@ -0,0 +1,18 @@ |
||||
require('./lib/polyfills') |
||||
require('./modal') |
||||
require('./notif') |
||||
require('./appcommon') |
||||
try { require('./term/demo') } catch (err) {} |
||||
require('./wifi') |
||||
|
||||
const $ = require('./lib/chibi') |
||||
const { qs } = require('./utils') |
||||
|
||||
/* Export stuff to the global scope for inline scripts */ |
||||
window.termInit = require('./term') |
||||
window.$ = $ |
||||
window.qs = qs |
||||
|
||||
window.themes = require('./term/themes') |
||||
|
||||
window.TermConf = require('./term_conf') |
@ -0,0 +1,5 @@ |
||||
let data = require('locale-data') |
||||
|
||||
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,63 @@ |
||||
/*! http://mths.be/fromcodepoint v0.1.0 by @mathias */ |
||||
if (!String.fromCodePoint) { |
||||
(function () { |
||||
var defineProperty = (function () { |
||||
// IE 8 only supports `Object.defineProperty` on DOM elements
|
||||
try { |
||||
var object = {}; |
||||
var $defineProperty = Object.defineProperty; |
||||
var result = $defineProperty(object, object, object) && $defineProperty; |
||||
} catch (error) { |
||||
} |
||||
return result; |
||||
}()); |
||||
var stringFromCharCode = String.fromCharCode; |
||||
var floor = Math.floor; |
||||
var fromCodePoint = function () { |
||||
var MAX_SIZE = 0x4000; |
||||
var codeUnits = []; |
||||
var highSurrogate; |
||||
var lowSurrogate; |
||||
var index = -1; |
||||
var length = arguments.length; |
||||
if (!length) { |
||||
return ''; |
||||
} |
||||
var result = ''; |
||||
while (++index < length) { |
||||
var codePoint = Number(arguments[index]); |
||||
if ( |
||||
!isFinite(codePoint) || // `NaN`, `+Infinity`, or `-Infinity`
|
||||
codePoint < 0 || // not a valid Unicode code point
|
||||
codePoint > 0x10FFFF || // not a valid Unicode code point
|
||||
floor(codePoint) != codePoint // not an integer
|
||||
) { |
||||
throw RangeError('Invalid code point: ' + codePoint); |
||||
} |
||||
if (codePoint <= 0xFFFF) { // BMP code point
|
||||
codeUnits.push(codePoint); |
||||
} else { // Astral code point; split in surrogate halves
|
||||
// http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
|
||||
codePoint -= 0x10000; |
||||
highSurrogate = (codePoint >> 10) + 0xD800; |
||||
lowSurrogate = (codePoint % 0x400) + 0xDC00; |
||||
codeUnits.push(highSurrogate, lowSurrogate); |
||||
} |
||||
if (index + 1 == length || codeUnits.length > MAX_SIZE) { |
||||
result += stringFromCharCode.apply(null, codeUnits); |
||||
codeUnits.length = 0; |
||||
} |
||||
} |
||||
return result; |
||||
}; |
||||
if (defineProperty) { |
||||
defineProperty(String, 'fromCodePoint', { |
||||
'value': fromCodePoint, |
||||
'configurable': true, |
||||
'writable': true |
||||
}); |
||||
} else { |
||||
String.fromCodePoint = fromCodePoint; |
||||
} |
||||
}()); |
||||
} |
@ -0,0 +1,44 @@ |
||||
const $ = require('./lib/chibi') |
||||
|
||||
/** Module for toggling a modal overlay */ |
||||
let modal = {} |
||||
let curCloseCb = null |
||||
|
||||
modal.show = function (sel, closeCb) { |
||||
let $m = $(sel) |
||||
$m.removeClass('hidden visible') |
||||
setTimeout(function () { |
||||
$m.addClass('visible') |
||||
}, 1) |
||||
curCloseCb = closeCb |
||||
} |
||||
|
||||
modal.hide = function (sel) { |
||||
let $m = $(sel) |
||||
$m.removeClass('visible') |
||||
setTimeout(function () { |
||||
$m.addClass('hidden') |
||||
if (curCloseCb) curCloseCb() |
||||
}, 500) // transition time
|
||||
} |
||||
|
||||
modal.init = function () { |
||||
// close modal by click outside the dialog
|
||||
$('.Modal').on('click', function () { |
||||
if ($(this).hasClass('no-close')) return // this is a no-close modal
|
||||
modal.hide(this) |
||||
}) |
||||
|
||||
$('.Dialog').on('click', function (e) { |
||||
e.stopImmediatePropagation() |
||||
}) |
||||
|
||||
// Hide all modals on esc
|
||||
$(window).on('keydown', function (e) { |
||||
if (e.which === 27) { |
||||
modal.hide('.Modal') |
||||
} |
||||
}) |
||||
} |
||||
|
||||
module.exports = modal |
@ -0,0 +1,65 @@ |
||||
const $ = require('./lib/chibi') |
||||
const modal = require('./modal') |
||||
|
||||
let nt = {} |
||||
const sel = '#notif' |
||||
let $balloon |
||||
|
||||
let timerHideBegin // timeout to start hiding (transition)
|
||||
let timerHideEnd // timeout to add the hidden class
|
||||
let canCancel = false |
||||
|
||||
let stopTimeouts = function () { |
||||
clearTimeout(timerHideBegin) |
||||
clearTimeout(timerHideEnd) |
||||
} |
||||
|
||||
nt.show = function (message, timeout, isError) { |
||||
$balloon.toggleClass('error', isError === true) |
||||
$balloon.html(message) |
||||
modal.show($balloon) |
||||
stopTimeouts() |
||||
|
||||
if (!timeout || timeout <= 0) { |
||||
timeout = 2500 |
||||
} |
||||
|
||||
timerHideBegin = setTimeout(nt.hide, timeout) |
||||
|
||||
canCancel = false |
||||
setTimeout(() => { |
||||
canCancel = true |
||||
}, 500) |
||||
} |
||||
|
||||
nt.hide = function () { |
||||
let $m = $(sel) |
||||
$m.removeClass('visible') |
||||
timerHideEnd = setTimeout(function () { |
||||
$m.addClass('hidden') |
||||
}, 250) // transition time
|
||||
} |
||||
|
||||
nt.init = function () { |
||||
$balloon = $(sel) |
||||
|
||||
// close by click outside
|
||||
$(document).on('click', function () { |
||||
if (!canCancel) return |
||||
nt.hide(this) |
||||
}) |
||||
|
||||
// click caused by selecting, prevent it from bubbling
|
||||
$balloon.on('click', function (e) { |
||||
e.stopImmediatePropagation() |
||||
return false |
||||
}) |
||||
|
||||
// stop fading if moused
|
||||
$balloon.on('mouseenter', function () { |
||||
stopTimeouts() |
||||
$balloon.removeClass('hidden').addClass('visible') |
||||
}) |
||||
} |
||||
|
||||
module.exports = nt |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,112 @@ |
||||
const { getColor } = require('./themes') |
||||
const { qs } = require('../utils') |
||||
const { rgb2hsl, hex2rgb, rgb2hex, hsl2rgb } = require('../lib/color_utils') |
||||
|
||||
module.exports = function initButtons (input) { |
||||
let container = qs('#action-buttons') |
||||
|
||||
// current color palette
|
||||
let palette = [] |
||||
|
||||
// button labels
|
||||
let labels = [] |
||||
|
||||
// button colors
|
||||
let colors = {} |
||||
|
||||
// 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) |
||||
|
||||
e.target.blur() // if it keeps focus, spacebar will push it
|
||||
}) |
||||
|
||||
// this prevents button retaining focus after half-click/drag-away
|
||||
button.addEventListener('mouseleave', e => { |
||||
e.target.blur() |
||||
}) |
||||
|
||||
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 = buttons.length; i > labels.length; i--) { |
||||
popButton() |
||||
} |
||||
} |
||||
|
||||
for (let i = 0; i < labels.length; i++) { |
||||
let label = labels[i].trim() |
||||
let button = buttons[i] |
||||
let color = colors[i] |
||||
|
||||
button.textContent = label || '\u00a0' // label or nbsp
|
||||
|
||||
if (!label) button.classList.add('inactive') |
||||
else button.classList.remove('inactive') |
||||
|
||||
// 0 or undefined can be used to disable custom color
|
||||
if (Number.isFinite(color) && color !== 0) { |
||||
const clr = getColor(color, palette) |
||||
button.style.background = clr |
||||
|
||||
// darken the color a bit for the 3D side
|
||||
const hsl = rgb2hsl(...hex2rgb(clr)) |
||||
const hex = rgb2hex(...hsl2rgb(hsl[0], hsl[1], hsl[2] * 0.7)) |
||||
button.style.boxShadow = `0 3px 0 ${hex}` |
||||
} else { |
||||
button.style.background = null |
||||
button.style.boxShadow = null |
||||
} |
||||
} |
||||
} |
||||
|
||||
return { |
||||
update, |
||||
get labels () { |
||||
return labels |
||||
}, |
||||
set labels (value) { |
||||
labels = value |
||||
update() |
||||
}, |
||||
get colors () { |
||||
return colors |
||||
}, |
||||
set colors (value) { |
||||
colors = value |
||||
update() |
||||
}, |
||||
get palette () { |
||||
return palette |
||||
}, |
||||
set palette (value) { |
||||
palette = value |
||||
update() |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,239 @@ |
||||
const EventEmitter = require('events') |
||||
const $ = require('../lib/chibi') |
||||
let demo |
||||
try { demo = require('./demo') } catch (err) {} |
||||
|
||||
const RECONN_DELAY = 2000 |
||||
const HEARTBEAT_TIME = 3000 |
||||
const HTTPS = window.location.protocol.match(/s:/) |
||||
|
||||
/** Handle connections */ |
||||
module.exports = class TermConnection extends EventEmitter { |
||||
constructor (screen) { |
||||
super() |
||||
|
||||
this.screen = screen |
||||
this.ws = null |
||||
this.heartbeatTimeout = null |
||||
this.pingInterval = null |
||||
this.xoff = false |
||||
this.autoXoffTimeout = null |
||||
this.reconnTimeout = null |
||||
this.forceClosing = false |
||||
this.queue = [] |
||||
|
||||
try { |
||||
this.blobReader = new window.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 |
||||
|
||||
document.addEventListener('visibilitychange', () => { |
||||
if (document.hidden === true) { |
||||
console.info('Window lost focus, freeing socket') |
||||
// Delayed, avoid disconnecting if the background time is short
|
||||
this.disconnectTimeout = setTimeout(() => { |
||||
this.closeSocket() |
||||
clearTimeout(this.heartbeatTimeout) |
||||
}, 1000) |
||||
} else { |
||||
clearTimeout(this.disconnectTimeout) |
||||
console.info('Window got focus, re-connecting') |
||||
this.init() |
||||
} |
||||
}, false) |
||||
} |
||||
|
||||
onWSOpen (evt) { |
||||
console.log('CONNECTED') |
||||
this.heartbeat() |
||||
this.send('i') |
||||
this.forceClosing = false |
||||
|
||||
this.emit('connect') |
||||
} |
||||
|
||||
onWSClose (evt) { |
||||
if (this.forceClosing) { |
||||
this.forceClosing = false |
||||
return |
||||
} |
||||
console.warn('SOCKET CLOSED, code ' + evt.code + '. Reconnecting...') |
||||
if (evt.code < 1000) { |
||||
console.error('Bad code from socket!') |
||||
// this sometimes happens for unknown reasons, code < 1000 is invalid
|
||||
// location.reload()
|
||||
} |
||||
|
||||
clearTimeout(this.reconnTimeout) |
||||
this.reconnTimeout = setTimeout(() => this.init(), RECONN_DELAY) |
||||
|
||||
this.emit('disconnect', evt.code) |
||||
} |
||||
|
||||
onDecodedWSMessage (str) { |
||||
switch (str.charAt(0)) { |
||||
case '.': |
||||
// heartbeat, no-op message
|
||||
break |
||||
|
||||
case '-': |
||||
// console.log('xoff');
|
||||
this.xoff = true |
||||
this.autoXoffTimeout = setTimeout(() => { |
||||
this.xoff = false |
||||
this.flushQueue() |
||||
}, 250) |
||||
break |
||||
|
||||
case '+': |
||||
// console.log('xon');
|
||||
this.xoff = false |
||||
this.flushQueue() |
||||
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) |
||||
} |
||||
} |
||||
} |
||||
|
||||
canSend () { |
||||
return !this.xoff |
||||
} |
||||
|
||||
send (message) { |
||||
if (window._demo) { |
||||
if (typeof window.demoInterface !== 'undefined') { |
||||
demo.input(message) |
||||
} else { |
||||
console.log(`TX: ${JSON.stringify(message)}`) |
||||
} |
||||
return true // Simulate success
|
||||
} |
||||
if (this.xoff) { |
||||
console.log("Can't send, flood control. Queueing") |
||||
this.queue.push(message) |
||||
return false |
||||
} |
||||
|
||||
if (!this.ws) return false // for dry testing
|
||||
if (this.ws.readyState !== 1) { |
||||
console.error('Socket not ready') |
||||
return false |
||||
} |
||||
if (typeof message !== 'string') { |
||||
message = JSON.stringify(message) |
||||
} |
||||
this.ws.send(message) |
||||
return true |
||||
} |
||||
|
||||
flushQueue () { |
||||
console.log('Flushing input queue') |
||||
for (let message of this.queue) this.send(message) |
||||
this.queue = [] |
||||
} |
||||
|
||||
/** Safely close the socket */ |
||||
closeSocket () { |
||||
if (this.ws) { |
||||
this.forceClosing = true |
||||
if (this.ws.readyState === 1) this.ws.close() |
||||
this.ws = null |
||||
} |
||||
} |
||||
|
||||
init () { |
||||
if (window._demo) { |
||||
if (typeof window.demoInterface === 'undefined') { |
||||
window.alert('Demoing non-demo build!') // this will catch mistakes when deploying to the website
|
||||
} else { |
||||
demo.init(this.screen) |
||||
} |
||||
return |
||||
} |
||||
|
||||
clearTimeout(this.reconnTimeout) |
||||
clearTimeout(this.heartbeatTimeout) |
||||
|
||||
this.closeSocket() |
||||
|
||||
this.ws = new window.WebSocket(`${HTTPS ? 'wss' : 'ws'}://${window._root}/term/update.ws`) |
||||
this.ws.addEventListener('open', (...args) => this.onWSOpen(...args)) |
||||
this.ws.addEventListener('close', (...args) => this.onWSClose(...args)) |
||||
this.ws.addEventListener('message', (...args) => this.onWSMessage(...args)) |
||||
console.log('Opening socket.') |
||||
this.heartbeat() |
||||
|
||||
this.emit('open') |
||||
} |
||||
|
||||
heartbeat () { |
||||
this.emit('heartbeat') |
||||
clearTimeout(this.heartbeatTimeout) |
||||
this.heartbeatTimeout = setTimeout(() => this.onHeartbeatFail(), HEARTBEAT_TIME) |
||||
} |
||||
|
||||
sendPing () { |
||||
console.log('> ping') |
||||
this.emit('ping') |
||||
$.get(`${HTTPS ? 'https' : '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 () { |
||||
this.closeSocket() |
||||
this.emit('silence') |
||||
console.error('Heartbeat lost, probing server...') |
||||
clearInterval(this.pingInterval) |
||||
this.pingInterval = setInterval(() => { this.sendPing() }, 1000) |
||||
|
||||
// first ping, if this gets through, it'll will reduce delay
|
||||
setTimeout(() => { this.sendPing() }, 200) |
||||
} |
||||
} |
@ -0,0 +1,539 @@ |
||||
const { getColor } = require('./themes') |
||||
const { |
||||
ATTR_FG, |
||||
ATTR_BG, |
||||
ATTR_BOLD, |
||||
ATTR_UNDERLINE, |
||||
ATTR_BLINK, |
||||
ATTR_ITALIC, |
||||
ATTR_STRIKE, |
||||
ATTR_OVERLINE, |
||||
ATTR_FAINT, |
||||
ATTR_FRAKTUR |
||||
} = require('./screen_attr_bits') |
||||
|
||||
// debug toolbar, tooltip and screen
|
||||
module.exports = function attachDebugger (screen, connection) { |
||||
// debug screen overlay
|
||||
const debugCanvas = document.createElement('canvas') |
||||
debugCanvas.classList.add('debug-canvas') |
||||
const ctx = debugCanvas.getContext('2d') |
||||
|
||||
// debug toolbar
|
||||
const toolbar = document.createElement('div') |
||||
toolbar.classList.add('debug-toolbar') |
||||
|
||||
// debug tooltip
|
||||
const tooltip = document.createElement('div') |
||||
tooltip.classList.add('debug-tooltip') |
||||
tooltip.classList.add('hidden') |
||||
|
||||
// update functions, defined somewhere below
|
||||
let updateTooltip |
||||
let updateToolbar |
||||
|
||||
// tooltip cell
|
||||
let selectedCell = null |
||||
|
||||
// update tooltip cell when mouse moves
|
||||
const onMouseMove = (e) => { |
||||
if (e.target !== screen.layout.canvas) { |
||||
selectedCell = null |
||||
return |
||||
} |
||||
selectedCell = screen.layout.screenToGrid(e.offsetX, e.offsetY) |
||||
updateTooltip() |
||||
} |
||||
|
||||
// hide tooltip when mouse leaves
|
||||
const onMouseOut = (e) => { |
||||
selectedCell = null |
||||
tooltip.classList.add('hidden') |
||||
} |
||||
|
||||
// updates debug canvas size
|
||||
const updateCanvasSize = function () { |
||||
let { width, height, devicePixelRatio } = screen.layout.window |
||||
let cellSize = screen.layout.getCellSize() |
||||
let padding = Math.round(screen.layout._padding) |
||||
debugCanvas.width = (width * cellSize.width + 2 * padding) * devicePixelRatio |
||||
debugCanvas.height = (height * cellSize.height + 2 * padding) * devicePixelRatio |
||||
debugCanvas.style.width = `${width * cellSize.width + 2 * screen.layout._padding}px` |
||||
debugCanvas.style.height = `${height * cellSize.height + 2 * screen.layout._padding}px` |
||||
} |
||||
|
||||
// defined somewhere below
|
||||
let startDrawLoop |
||||
|
||||
let screenAttached = false |
||||
|
||||
// node to which events were bound (kept here for when they need to be removed)
|
||||
let eventNode |
||||
|
||||
// attaches/detaches debug screen overlay to/from DOM
|
||||
const setScreenAttached = function (attached) { |
||||
if (attached && !debugCanvas.parentNode) { |
||||
screen.layout.canvas.parentNode.appendChild(debugCanvas) |
||||
eventNode = debugCanvas.parentNode |
||||
eventNode.addEventListener('mousemove', onMouseMove) |
||||
eventNode.addEventListener('mouseout', onMouseOut) |
||||
screen.layout.on('size-update', updateCanvasSize) |
||||
updateCanvasSize() |
||||
screenAttached = true |
||||
startDrawLoop() |
||||
} else if (!attached && debugCanvas.parentNode) { |
||||
debugCanvas.parentNode.removeChild(debugCanvas) |
||||
eventNode.removeEventListener('mousemove', onMouseMove) |
||||
eventNode.removeEventListener('mouseout', onMouseOut) |
||||
screen.layout.removeListener('size-update', updateCanvasSize) |
||||
screenAttached = false |
||||
} |
||||
} |
||||
|
||||
// attaches/detaches toolbar and tooltip to/from DOM
|
||||
const setToolbarAttached = function (attached) { |
||||
if (attached && !toolbar.parentNode) { |
||||
screen.layout.canvas.parentNode.appendChild(toolbar) |
||||
screen.layout.canvas.parentNode.appendChild(tooltip) |
||||
updateToolbar() |
||||
} else if (!attached && toolbar.parentNode) { |
||||
screen.layout.canvas.parentNode.removeChild(toolbar) |
||||
screen.layout.canvas.parentNode.removeChild(tooltip) |
||||
} |
||||
} |
||||
|
||||
// attach/detach toolbar when debug mode is enabled/disabled
|
||||
screen.on('update-window:debug', enabled => { |
||||
setToolbarAttached(enabled) |
||||
}) |
||||
|
||||
// ditto ^
|
||||
screen.layout.on('update-window:debug', enabled => { |
||||
setScreenAttached(enabled) |
||||
}) |
||||
|
||||
let drawData = { |
||||
// last draw reason
|
||||
reason: '', |
||||
|
||||
// when true, will show colored cell update overlays
|
||||
showUpdates: false, |
||||
|
||||
// draw start time in milliseconds
|
||||
startTime: 0, |
||||
|
||||
// end time
|
||||
endTime: 0, |
||||
|
||||
// partial update frames
|
||||
frames: [], |
||||
|
||||
// cell data
|
||||
cells: new Map(), |
||||
|
||||
// scroll region
|
||||
scrollRegion: null |
||||
} |
||||
|
||||
// debug interface
|
||||
screen._debug = screen.layout.renderer._debug = { |
||||
drawStart (reason) { |
||||
drawData.reason = reason |
||||
drawData.startTime = window.performance.now() |
||||
}, |
||||
drawEnd () { |
||||
drawData.endTime = window.performance.now() |
||||
}, |
||||
setCell (cell, flags) { |
||||
drawData.cells.set(cell, [flags, window.performance.now()]) |
||||
}, |
||||
pushFrame (frame) { |
||||
drawData.frames.push([...frame, window.performance.now()]) |
||||
} |
||||
} |
||||
|
||||
let isDrawing = false |
||||
let drawLoop = function () { |
||||
// draw while the screen is attached
|
||||
if (screenAttached) window.requestAnimationFrame(drawLoop) |
||||
else isDrawing = false |
||||
|
||||
let now = window.performance.now() |
||||
|
||||
let { width, height, devicePixelRatio } = screen.layout.window |
||||
let padding = Math.round(screen.layout._padding) |
||||
let cellSize = screen.layout.getCellSize() |
||||
|
||||
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0) |
||||
ctx.clearRect(0, 0, width * cellSize.width + 2 * padding, height * cellSize.height + 2 * padding) |
||||
ctx.translate(padding, padding) |
||||
|
||||
ctx.lineWidth = 2 |
||||
ctx.lineJoin = 'round' |
||||
|
||||
if (drawData.showUpdates) { |
||||
const cells = drawData.cells |
||||
for (let cell = 0; cell < width * height; cell++) { |
||||
// cell does not exist or has no flags set
|
||||
if (!cells.has(cell) || cells.get(cell)[0] === 0) continue |
||||
|
||||
const [flags, timestamp] = cells.get(cell) |
||||
let elapsedTime = (now - timestamp) / 1000 |
||||
|
||||
if (elapsedTime > 1) { |
||||
cells.delete(cell) |
||||
continue |
||||
} |
||||
|
||||
ctx.globalAlpha = 0.5 * Math.max(0, 1 - elapsedTime) |
||||
|
||||
let x = cell % width |
||||
let y = Math.floor(cell / width) |
||||
|
||||
if (flags & 2) { |
||||
// updated
|
||||
ctx.fillStyle = '#0f0' |
||||
} else if (flags & 1) { |
||||
// redrawn
|
||||
ctx.fillStyle = '#f0f' |
||||
} |
||||
|
||||
if (!(flags & 4)) { |
||||
// outside a clipped region
|
||||
ctx.fillStyle = '#0ff' |
||||
} |
||||
|
||||
if (flags & 16) { |
||||
// was filled to speed up rendering
|
||||
ctx.globalAlpha /= 2 |
||||
} |
||||
|
||||
ctx.fillRect(x * cellSize.width, y * cellSize.height, cellSize.width, cellSize.height) |
||||
|
||||
if (flags & 8) { |
||||
// wide cell
|
||||
ctx.strokeStyle = '#f00' |
||||
ctx.beginPath() |
||||
ctx.moveTo(x * cellSize.width, (y + 1) * cellSize.height) |
||||
ctx.lineTo((x + 1) * cellSize.width, (y + 1) * cellSize.height) |
||||
ctx.stroke() |
||||
} |
||||
} |
||||
|
||||
let framesToDelete = [] |
||||
for (let frame of drawData.frames) { |
||||
let timestamp = frame[4] |
||||
let elapsedTime = (now - timestamp) / 1000 |
||||
if (elapsedTime > 1) framesToDelete.push(frame) |
||||
else { |
||||
ctx.globalAlpha = 1 - elapsedTime |
||||
ctx.strokeStyle = '#ff0' |
||||
ctx.strokeRect(frame[0] * cellSize.width, frame[1] * cellSize.height, |
||||
frame[2] * cellSize.width, frame[3] * cellSize.height) |
||||
} |
||||
} |
||||
for (let frame of framesToDelete) { |
||||
drawData.frames.splice(drawData.frames.indexOf(frame), 1) |
||||
} |
||||
} |
||||
|
||||
if (selectedCell !== null) { |
||||
// draw a dashed outline around the selected cell
|
||||
let [x, y] = selectedCell |
||||
|
||||
ctx.save() |
||||
ctx.globalAlpha = 0.5 |
||||
ctx.lineWidth = 1 |
||||
|
||||
// draw X line
|
||||
ctx.beginPath() |
||||
ctx.moveTo(0, y * cellSize.height) |
||||
ctx.lineTo(x * cellSize.width, y * cellSize.height) |
||||
ctx.strokeStyle = '#f00' |
||||
ctx.setLineDash([cellSize.width]) |
||||
ctx.stroke() |
||||
|
||||
// draw Y line
|
||||
ctx.beginPath() |
||||
ctx.moveTo(x * cellSize.width, 0) |
||||
ctx.lineTo(x * cellSize.width, y * cellSize.height) |
||||
ctx.strokeStyle = '#0f0' |
||||
ctx.setLineDash([cellSize.height]) |
||||
ctx.stroke() |
||||
|
||||
ctx.globalAlpha = 1 |
||||
ctx.lineWidth = 1 + 0.5 * Math.sin((now / 1000) * 10) |
||||
ctx.strokeStyle = '#fff' |
||||
ctx.lineJoin = 'round' |
||||
ctx.setLineDash([2, 2]) |
||||
ctx.lineDashOffset = (now / 1000) * 10 |
||||
ctx.strokeRect(x * cellSize.width, y * cellSize.height, cellSize.width, cellSize.height) |
||||
ctx.lineDashOffset += 2 |
||||
ctx.strokeStyle = '#000' |
||||
ctx.strokeRect(x * cellSize.width, y * cellSize.height, cellSize.width, cellSize.height) |
||||
ctx.restore() |
||||
} |
||||
|
||||
if (drawData.scrollRegion !== null) { |
||||
// draw two lines marking the scroll region bounds
|
||||
let [start, end] = drawData.scrollRegion |
||||
|
||||
ctx.save() |
||||
ctx.globalAlpha = 1 |
||||
ctx.strokeStyle = '#00f' |
||||
ctx.lineWidth = 2 |
||||
ctx.setLineDash([2, 2]) |
||||
|
||||
ctx.beginPath() |
||||
ctx.moveTo(0, start * cellSize.height) |
||||
ctx.lineTo(width * cellSize.width, start * cellSize.height) |
||||
ctx.stroke() |
||||
|
||||
ctx.beginPath() |
||||
ctx.moveTo(0, (end + 1) * cellSize.height) |
||||
ctx.lineTo(width * cellSize.width, (end + 1) * cellSize.height) |
||||
ctx.stroke() |
||||
|
||||
ctx.restore() |
||||
} |
||||
} |
||||
|
||||
startDrawLoop = function () { |
||||
if (isDrawing) return |
||||
isDrawing = true |
||||
drawLoop() |
||||
} |
||||
|
||||
let pad2 = i => ('00' + i.toString()).substr(-2) |
||||
let formatColor = color => color < 256 |
||||
? color.toString() |
||||
: '#' + pad2(color >> 16) + pad2((color >> 8) & 0xFF) + pad2(color & 0xFF) |
||||
|
||||
let makeSpan = (text, styles) => { |
||||
let span = document.createElement('span') |
||||
span.textContent = text |
||||
Object.assign(span.style, styles || {}) |
||||
return span |
||||
} |
||||
let formatAttributes = (target, attrs) => { |
||||
if (attrs & ATTR_FG) target.appendChild(makeSpan('HasFG')) |
||||
if (attrs & ATTR_BG) target.appendChild(makeSpan('HasBG')) |
||||
if (attrs & ATTR_BOLD) target.appendChild(makeSpan('Bold', { fontWeight: 'bold' })) |
||||
if (attrs & ATTR_UNDERLINE) target.appendChild(makeSpan('Uline', { textDecoration: 'underline' })) |
||||
if (attrs & ATTR_BLINK) target.appendChild(makeSpan('Blink')) |
||||
if (attrs & ATTR_ITALIC) target.appendChild(makeSpan('Italic', { fontStyle: 'italic' })) |
||||
if (attrs & ATTR_STRIKE) target.appendChild(makeSpan('Strike', { textDecoration: 'line-through' })) |
||||
if (attrs & ATTR_OVERLINE) target.appendChild(makeSpan('Oline', { textDecoration: 'overline' })) |
||||
if (attrs & ATTR_FAINT) target.appendChild(makeSpan('Faint', { opacity: 0.5 })) |
||||
if (attrs & ATTR_FRAKTUR) target.appendChild(makeSpan('Fraktur')) |
||||
} |
||||
|
||||
updateTooltip = function () { |
||||
// TODO: make this not destroy and recreate the same nodes every time
|
||||
tooltip.classList.remove('hidden') |
||||
tooltip.innerHTML = '' |
||||
let cell = selectedCell[1] * screen.window.width + selectedCell[0] |
||||
if (!screen.screen[cell]) return |
||||
|
||||
let foreground = document.createElement('span') |
||||
foreground.textContent = formatColor(screen.screenFG[cell]) |
||||
let preview = document.createElement('span') |
||||
preview.textContent = ' ●' |
||||
preview.style.color = getColor(screen.screenFG[cell], screen.layout.renderer.palette) |
||||
foreground.appendChild(preview) |
||||
|
||||
let background = document.createElement('span') |
||||
background.textContent = formatColor(screen.screenBG[cell]) |
||||
let bgPreview = document.createElement('span') |
||||
bgPreview.textContent = ' ●' |
||||
bgPreview.style.color = getColor(screen.screenBG[cell], screen.layout.renderer.palette) |
||||
background.appendChild(bgPreview) |
||||
|
||||
let character = screen.screen[cell] |
||||
let codePoint = character.codePointAt(0) |
||||
let formattedCodePoint = codePoint.toString(16).length <= 4 |
||||
? `0000${codePoint.toString(16)}`.substr(-4) |
||||
: codePoint.toString(16) |
||||
|
||||
let attributes = document.createElement('span') |
||||
attributes.classList.add('attributes') |
||||
formatAttributes(attributes, screen.screenAttrs[cell]) |
||||
|
||||
let data = { |
||||
Cell: `col ${selectedCell[0] + 1}, ln ${selectedCell[1] + 1} (${cell})`, |
||||
Foreground: foreground, |
||||
Background: background, |
||||
Character: `U+${formattedCodePoint}`, |
||||
Attributes: attributes |
||||
} |
||||
|
||||
let table = document.createElement('table') |
||||
|
||||
for (let name in data) { |
||||
let row = document.createElement('tr') |
||||
let label = document.createElement('td') |
||||
label.appendChild(new window.Text(name)) |
||||
label.classList.add('label') |
||||
|
||||
let value = document.createElement('td') |
||||
value.appendChild(typeof data[name] === 'string' ? new window.Text(data[name]) : data[name]) |
||||
value.classList.add('value') |
||||
|
||||
row.appendChild(label) |
||||
row.appendChild(value) |
||||
table.appendChild(row) |
||||
} |
||||
|
||||
tooltip.appendChild(table) |
||||
|
||||
let cellSize = screen.layout.getCellSize() |
||||
// add 3 to the position because for some reason the corner is off
|
||||
let posX = (selectedCell[0] + 1) * cellSize.width + 3 |
||||
let posY = (selectedCell[1] + 1) * cellSize.height + 3 |
||||
tooltip.style.transform = `translate(${posX}px, ${posY}px)` |
||||
} |
||||
|
||||
let toolbarData = null |
||||
let toolbarNodes = {} |
||||
|
||||
// construct the toolbar if it wasn't already
|
||||
const initToolbar = function () { |
||||
if (toolbarData) return |
||||
|
||||
let showUpdates = document.createElement('input') |
||||
showUpdates.type = 'checkbox' |
||||
showUpdates.addEventListener('change', e => { |
||||
drawData.showUpdates = showUpdates.checked |
||||
}) |
||||
|
||||
let fancyGraphics = document.createElement('input') |
||||
fancyGraphics.type = 'checkbox' |
||||
fancyGraphics.value = !!screen.layout.renderer.graphics |
||||
fancyGraphics.addEventListener('change', e => { |
||||
screen.layout.renderer.graphics = +fancyGraphics.checked |
||||
}) |
||||
|
||||
toolbarData = { |
||||
cursor: { |
||||
title: 'Cursor', |
||||
Position: '', |
||||
Style: '', |
||||
Visible: true, |
||||
Hanging: false |
||||
}, |
||||
internal: { |
||||
Flags: '', |
||||
'Cursor Attributes': '', |
||||
'Code Page': '', |
||||
Heap: 0, |
||||
Clients: 0 |
||||
}, |
||||
drawing: { |
||||
title: 'Drawing', |
||||
'Last Update': '', |
||||
'Show Updates': showUpdates, |
||||
'Fancy Graphics': fancyGraphics, |
||||
'Redraw Screen': () => { |
||||
screen.layout.renderer.resetDrawn() |
||||
screen.layout.renderer.draw('debug-redraw') |
||||
} |
||||
} |
||||
} |
||||
|
||||
for (let i in toolbarData) { |
||||
let group = toolbarData[i] |
||||
let table = document.createElement('table') |
||||
table.classList.add('toolbar-group') |
||||
|
||||
toolbarNodes[i] = {} |
||||
|
||||
for (let key in group) { |
||||
let item = document.createElement('tr') |
||||
let name = document.createElement('td') |
||||
name.classList.add('name') |
||||
let value = document.createElement('td') |
||||
value.classList.add('value') |
||||
|
||||
toolbarNodes[i][key] = { name, value } |
||||
|
||||
if (key === 'title') { |
||||
name.textContent = group[key] |
||||
name.classList.add('title') |
||||
} else { |
||||
name.textContent = key |
||||
if (group[key] instanceof Function) { |
||||
name.textContent = '' |
||||
let button = document.createElement('button') |
||||
name.classList.add('has-button') |
||||
name.appendChild(button) |
||||
button.textContent = key |
||||
button.addEventListener('click', e => group[key](e)) |
||||
} else if (group[key] instanceof window.Node) value.appendChild(group[key]) |
||||
else value.textContent = group[key] |
||||
} |
||||
|
||||
item.appendChild(name) |
||||
item.appendChild(value) |
||||
table.appendChild(item) |
||||
} |
||||
|
||||
toolbar.appendChild(table) |
||||
} |
||||
|
||||
let heartbeat = toolbarNodes.heartbeat = document.createElement('div') |
||||
heartbeat.classList.add('heartbeat') |
||||
heartbeat.textContent = '❤' |
||||
toolbar.appendChild(heartbeat) |
||||
} |
||||
|
||||
connection.on('heartbeat', () => { |
||||
if (screenAttached && toolbarNodes.heartbeat) { |
||||
toolbarNodes.heartbeat.classList.remove('beat') |
||||
window.requestAnimationFrame(() => { |
||||
toolbarNodes.heartbeat.classList.add('beat') |
||||
}) |
||||
} |
||||
}) |
||||
|
||||
updateToolbar = function () { |
||||
initToolbar() |
||||
|
||||
Object.assign(toolbarData.cursor, { |
||||
Position: `col ${screen.cursor.x + 1}, ln ${screen.cursor.y + 1}`, |
||||
Style: screen.cursor.style + (screen.cursor.blinking ? ', blink' : ''), |
||||
Visible: screen.cursor.visible, |
||||
Hanging: screen.cursor.hanging |
||||
}) |
||||
|
||||
let drawTime = Math.round((drawData.endTime - drawData.startTime) * 100) / 100 |
||||
toolbarData.drawing['Last Update'] = `${drawData.reason} (${drawTime}ms)` |
||||
toolbarData.drawing['Fancy Graphics'].checked = !!screen.layout.renderer.graphics |
||||
|
||||
for (let i in toolbarData) { |
||||
let group = toolbarData[i] |
||||
let nodes = toolbarNodes[i] |
||||
for (let key in group) { |
||||
if (key === 'title') continue |
||||
let value = nodes[key].value |
||||
if (!(group[key] instanceof window.Node) && !(group[key] instanceof Function)) { |
||||
value.textContent = group[key] |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
screen.on('update', updateToolbar) |
||||
screen.on('internal', data => { |
||||
if (screenAttached && toolbarData) { |
||||
Object.assign(toolbarData.internal, { |
||||
Flags: data.flags.toString(2), |
||||
'Cursor Attributes': data.cursorAttrs.toString(2), |
||||
'Code Page': `${data.charsetGx} (${data.charsetG0}, ${data.charsetG1})`, |
||||
Heap: data.freeHeap, |
||||
Clients: data.clientCount |
||||
}) |
||||
drawData.scrollRegion = [data.regionStart, data.regionEnd] |
||||
updateToolbar() |
||||
} |
||||
}) |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,250 @@ |
||||
const $ = require('../lib/chibi') |
||||
const { qs, mk } = require('../utils') |
||||
const localize = require('../lang') |
||||
const Notify = require('../notif') |
||||
const TermScreen = require('./screen') |
||||
const TermConnection = require('./connection') |
||||
const TermInput = require('./input') |
||||
const TermUpload = require('./upload') |
||||
const initSoftKeyboard = require('./soft_keyboard') |
||||
const attachDebugger = require('./debug') |
||||
const initButtons = require('./buttons') |
||||
|
||||
/** Init the terminal sub-module - called from HTML */ |
||||
module.exports = function (opts) { |
||||
const screen = new TermScreen() |
||||
const conn = new TermConnection(screen) |
||||
const input = TermInput(conn, screen) |
||||
const termUpload = TermUpload(conn, input, screen) |
||||
input.termUpload = termUpload |
||||
|
||||
// forward screen input events
|
||||
screen.on('mousedown', (...args) => input.onMouseDown(...args)) |
||||
screen.on('mousemove', (...args) => input.onMouseMove(...args)) |
||||
screen.on('mouseup', (...args) => input.onMouseUp(...args)) |
||||
screen.on('mousewheel', (...args) => input.onMouseWheel(...args)) |
||||
screen.on('input-alts', (...args) => input.setAlts(...args)) |
||||
screen.on('mouse-mode', (...args) => input.setMouseMode(...args)) |
||||
|
||||
// touch selection menu (the Copy button)
|
||||
$.ready(() => { |
||||
const touchSelectMenu = qs('#touch-select-menu') |
||||
screen.on('show-touch-select-menu', (x, y) => { |
||||
let rect = touchSelectMenu.getBoundingClientRect() |
||||
x -= rect.width / 2 |
||||
y -= rect.height / 2 |
||||
|
||||
touchSelectMenu.classList.add('open') |
||||
touchSelectMenu.style.transform = `translate(${x}px,${y}px)` |
||||
}) |
||||
screen.on('hide-touch-select-menu', () => touchSelectMenu.classList.remove('open')) |
||||
|
||||
const copyButton = qs('#touch-select-copy-btn') |
||||
if (copyButton) { |
||||
copyButton.addEventListener('click', () => { |
||||
screen.copySelectionToClipboard() |
||||
}) |
||||
} |
||||
}) |
||||
|
||||
// buttons
|
||||
const buttons = initButtons(input) |
||||
screen.on('buttons-update', update => { |
||||
buttons.labels = update.labels |
||||
buttons.colors = update.colors |
||||
}) |
||||
// TODO: don't access the renderer here
|
||||
buttons.palette = screen.layout.renderer.palette |
||||
screen.layout.renderer.on('palette-update', palette => { |
||||
buttons.palette = palette |
||||
}) |
||||
|
||||
screen.on('full-load', () => { |
||||
let scr = qs('#screen') |
||||
let errmsg = qs('#load-failed') |
||||
if (scr) scr.classList.remove('failed') |
||||
if (errmsg) errmsg.parentNode.removeChild(errmsg) |
||||
}) |
||||
|
||||
let setLinkVisibility = visible => { |
||||
let buttons = [...document.querySelectorAll('.x-term-conf-btn')] |
||||
if (visible) buttons.forEach(x => x.classList.remove('hidden')) |
||||
else buttons.forEach(x => x.classList.add('hidden')) |
||||
} |
||||
let setButtonVisibility = visible => { |
||||
if (visible) qs('#action-buttons').classList.remove('hidden') |
||||
else qs('#action-buttons').classList.add('hidden') |
||||
} |
||||
|
||||
screen.on('opts-update', () => { |
||||
setLinkVisibility(screen.showLinks) |
||||
setButtonVisibility(screen.showButtons) |
||||
}) |
||||
|
||||
screen.on('title-update', text => { |
||||
qs('#screen-title').textContent = text |
||||
if (!text) text = 'Terminal' |
||||
qs('title').textContent = `${text} :: ESPTerm` |
||||
}) |
||||
|
||||
// connection status
|
||||
|
||||
let showSplashTimeout = null |
||||
let showSplash = (obj, delay = 250) => { |
||||
clearTimeout(showSplashTimeout) |
||||
showSplashTimeout = setTimeout(() => { |
||||
screen.window.statusScreen = obj |
||||
}, delay) |
||||
} |
||||
|
||||
conn.on('open', () => { |
||||
// console.log('*open')
|
||||
showSplash({ title: localize('term_conn.connecting'), loading: true }) |
||||
}) |
||||
conn.on('connect', () => { |
||||
// console.log('*connect')
|
||||
showSplash({ title: localize('term_conn.waiting_content'), loading: true }) |
||||
}) |
||||
screen.on('load', () => { |
||||
// console.log('*load')
|
||||
clearTimeout(showSplashTimeout) |
||||
if (screen.window.statusScreen) screen.window.statusScreen = null |
||||
}) |
||||
conn.on('disconnect', () => { |
||||
// console.log('*disconnect')
|
||||
showSplash({ title: localize('term_conn.disconnected') }, 500) |
||||
screen.screen = [] |
||||
screen.screenFG = [] |
||||
screen.screenBG = [] |
||||
screen.screenAttrs = [] |
||||
}) |
||||
conn.on('silence', () => { |
||||
// console.log('*silence')
|
||||
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: localize('term_conn.reconnecting'), loading: true }, 0) |
||||
}) |
||||
|
||||
conn.init() |
||||
input.init(opts) |
||||
termUpload.init() |
||||
Notify.init() |
||||
|
||||
window.onerror = function (errorMsg, file, line, col) { |
||||
Notify.show(`<b>JS ERROR!</b><br>${errorMsg}<br>at ${file}:${line}:${col}`, 10000, true) |
||||
return false |
||||
} |
||||
|
||||
qs('#screen').appendChild(screen.layout.canvas) |
||||
|
||||
initSoftKeyboard(screen, input) |
||||
if (attachDebugger) attachDebugger(screen, conn) |
||||
|
||||
// fullscreen mode
|
||||
|
||||
let fullscreenIcon = {} // dummy
|
||||
let isFullscreen = false |
||||
let properFullscreen = false |
||||
let fitScreen = false |
||||
let screenPadding = screen.layout.window.padding |
||||
let fitScreenIfNeeded = function fitScreenIfNeeded () { |
||||
if (isFullscreen) { |
||||
fullscreenIcon.className = 'icn-resize-small' |
||||
if (properFullscreen) { |
||||
screen.layout.window.fitIntoWidth = window.screen.width |
||||
screen.layout.window.fitIntoHeight = window.screen.height |
||||
screen.layout.window.padding = 0 |
||||
} else { |
||||
screen.layout.window.fitIntoWidth = window.innerWidth |
||||
if (qs('#term-nav').classList.contains('hidden')) { |
||||
screen.layout.window.fitIntoHeight = window.innerHeight |
||||
} else { |
||||
screen.layout.window.fitIntoHeight = window.innerHeight - 24 |
||||
} |
||||
screen.layout.window.padding = 0 |
||||
} |
||||
} else { |
||||
fullscreenIcon.className = 'icn-resize-full' |
||||
screen.layout.window.fitIntoWidth = fitScreen ? window.innerWidth - 4 : 0 |
||||
screen.layout.window.fitIntoHeight = fitScreen ? window.innerHeight : 0 |
||||
screen.layout.window.padding = screenPadding |
||||
} |
||||
} |
||||
fitScreenIfNeeded() |
||||
window.addEventListener('resize', fitScreenIfNeeded) |
||||
|
||||
let toggleFitScreen = function () { |
||||
fitScreen = !fitScreen |
||||
const resizeButtonIcon = qs('#resize-button-icon') |
||||
if (fitScreen) { |
||||
resizeButtonIcon.classList.remove('icn-resize-small') |
||||
resizeButtonIcon.classList.add('icn-resize-full') |
||||
} else { |
||||
resizeButtonIcon.classList.remove('icn-resize-full') |
||||
resizeButtonIcon.classList.add('icn-resize-small') |
||||
} |
||||
fitScreenIfNeeded() |
||||
} |
||||
|
||||
qs('#term-fit-screen').addEventListener('click', function () { |
||||
toggleFitScreen() |
||||
return false |
||||
}) |
||||
|
||||
// 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)) { |
||||
isFullscreen = false |
||||
fitScreenIfNeeded() |
||||
} |
||||
} |
||||
setInterval(checkForFullscreen, 500) |
||||
} |
||||
|
||||
// (why are the buttons anchors?)
|
||||
let button = mk('a') |
||||
button.id = 'fullscreen-button' |
||||
button.href = '#' |
||||
button.addEventListener('click', e => { |
||||
e.preventDefault() |
||||
|
||||
if (document.body.classList.contains('pseudo-fullscreen')) { |
||||
document.body.classList.remove('pseudo-fullscreen') |
||||
isFullscreen = false |
||||
fitScreenIfNeeded() |
||||
return |
||||
} |
||||
|
||||
isFullscreen = true |
||||
fitScreenIfNeeded() |
||||
screen.layout.updateSize() |
||||
|
||||
if (properFullscreen) { |
||||
if (screen.layout.canvas.requestFullscreen) screen.layout.canvas.requestFullscreen() |
||||
else screen.layout.canvas.webkitRequestFullscreen() |
||||
} 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 |
||||
window.buttons = buttons |
||||
window.conn = conn |
||||
window.input = input |
||||
window.termUpl = termUpload |
||||
} |
@ -0,0 +1,461 @@ |
||||
const { encode2B } = require('../utils') |
||||
|
||||
/** |
||||
* User input |
||||
* |
||||
* --- Rx messages: --- |
||||
* S - screen content (binary encoding of the entire screen with simple compression) |
||||
* T - text labels - Title and buttons, \0x01-separated |
||||
* B - beep |
||||
* . - heartbeat |
||||
* |
||||
* --- Tx messages --- |
||||
* s - string |
||||
* b - action button |
||||
* p - mb press |
||||
* r - mb release |
||||
* m - mouse move |
||||
*/ |
||||
module.exports = function (conn, screen) { |
||||
// handle for input object
|
||||
let input |
||||
|
||||
const KEY_NAMES = { |
||||
0x03: 'Cancel', |
||||
0x06: 'Help', |
||||
0x08: 'Backspace', |
||||
0x09: 'Tab', |
||||
0x0C: 'Clear', |
||||
0x0D: 'Enter', |
||||
0x10: 'Shift', |
||||
0x11: 'Control', |
||||
0x12: 'Alt', |
||||
0x13: 'Pause', |
||||
0x14: 'CapsLock', |
||||
0x1B: 'Escape', |
||||
0x20: ' ', |
||||
0x21: 'PageUp', |
||||
0x22: 'PageDown', |
||||
0x23: 'End', |
||||
0x24: 'Home', |
||||
0x25: 'ArrowLeft', |
||||
0x26: 'ArrowUp', |
||||
0x27: 'ArrowRight', |
||||
0x28: 'ArrowDown', |
||||
0x29: 'Select', |
||||
0x2A: 'Print', |
||||
0x2B: 'Execute', |
||||
0x2C: 'PrintScreen', |
||||
0x2D: 'Insert', |
||||
0x2E: 'Delete', |
||||
0x3A: ':', |
||||
0x3B: ';', |
||||
0x3C: '<', |
||||
0x3D: '=', |
||||
0x3E: '>', |
||||
0x3F: '?', |
||||
0x40: '@', |
||||
0x5B: 'Meta', |
||||
0x5C: 'Meta', |
||||
0x5D: 'ContextMenu', |
||||
0x6A: 'Numpad*', |
||||
0x6B: 'Numpad+', |
||||
0x6D: 'Numpad-', |
||||
0x6E: 'Numpad.', |
||||
0x6F: 'Numpad/', |
||||
0x90: 'NumLock', |
||||
0x91: 'ScrollLock', |
||||
0xA0: '^', |
||||
0xA1: '!', |
||||
0xA2: '"', |
||||
0xA3: '#', |
||||
0xA4: '$', |
||||
0xA5: '%', |
||||
0xA6: '&', |
||||
0xA7: '_', |
||||
0xA8: '(', |
||||
0xA9: ')', |
||||
0xAA: '*', |
||||
0xAB: '+', |
||||
0xAC: '|', |
||||
0xAD: '-', |
||||
0xAE: '{', |
||||
0xAF: '}', |
||||
0xB0: '~', |
||||
0xBA: ';', |
||||
0xBB: '=', |
||||
0xBC: 'Numpad,', |
||||
0xBD: '-', |
||||
0xBE: 'Numpad,', |
||||
0xC0: '`', |
||||
0xC2: 'Numpad,', |
||||
0xDB: '[', |
||||
0xDC: '\\', |
||||
0xDD: ']', |
||||
0xDE: "'", |
||||
0xE0: 'Meta' |
||||
} |
||||
// numbers 0-9
|
||||
for (let i = 0x30; i <= 0x39; i++) KEY_NAMES[i] = String.fromCharCode(i) |
||||
// characters A-Z
|
||||
for (let i = 0x41; i <= 0x5A; i++) KEY_NAMES[i] = String.fromCharCode(i) |
||||
// function F1-F20
|
||||
for (let i = 0x70; i <= 0x83; i++) KEY_NAMES[i] = `F${i - 0x70 + 1}` |
||||
// numpad 0-9
|
||||
for (let i = 0x60; i <= 0x69; i++) KEY_NAMES[i] = `Numpad${i - 0x60}` |
||||
|
||||
let cfg = { |
||||
np_alt: false, // Application Numpad Mode
|
||||
cu_alt: false, // Application Cursors Mode
|
||||
fn_alt: false, // SS3 function keys mode
|
||||
mt_click: false, // Mouse click tracking
|
||||
mt_move: false, // Mouse move tracking
|
||||
no_keys: false, // Suppress any key / clipboard event intercepting
|
||||
crlf_mode: false, // Enter sends CR LF
|
||||
all_fn: false // Capture also F5, F11 and F12
|
||||
} |
||||
|
||||
/** Fn alt choice for key message */ |
||||
const fa = (alt, normal) => cfg.fn_alt ? alt : normal |
||||
|
||||
/** Cursor alt choice for key message */ |
||||
const ca = (alt, normal) => cfg.cu_alt ? alt : normal |
||||
|
||||
/** Numpad alt choice for key message */ |
||||
const na = (alt, normal) => cfg.np_alt ? alt : normal |
||||
|
||||
const keymap = { |
||||
/* eslint-disable key-spacing */ |
||||
'Backspace': '\x08', |
||||
'Tab': '\x09', |
||||
'Enter': () => cfg.crlf_mode ? '\x0d\x0a' : '\x0d', |
||||
'Control+Enter': '\x0a', |
||||
'Escape': '\x1b', |
||||
'ArrowUp': () => ca('\x1bOA', '\x1b[A'), |
||||
'ArrowDown': () => ca('\x1bOB', '\x1b[B'), |
||||
'ArrowRight': () => ca('\x1bOC', '\x1b[C'), |
||||
'ArrowLeft': () => ca('\x1bOD', '\x1b[D'), |
||||
'Home': () => ca('\x1bOH', fa('\x1b[H', '\x1b[1~')), |
||||
'Insert': '\x1b[2~', |
||||
'Delete': '\x1b[3~', |
||||
'End': () => ca('\x1bOF', fa('\x1b[F', '\x1b[4~')), |
||||
'PageUp': '\x1b[5~', |
||||
'PageDown': '\x1b[6~', |
||||
'F1': () => fa('\x1bOP', '\x1b[11~'), |
||||
'F2': () => fa('\x1bOQ', '\x1b[12~'), |
||||
'F3': () => fa('\x1bOR', '\x1b[13~'), |
||||
'F4': () => fa('\x1bOS', '\x1b[14~'), |
||||
'F5': '\x1b[15~', // note the disconnect
|
||||
'F6': '\x1b[17~', |
||||
'F7': '\x1b[18~', |
||||
'F8': '\x1b[19~', |
||||
'F9': '\x1b[20~', |
||||
'F10': '\x1b[21~', // note the disconnect
|
||||
'F11': '\x1b[23~', |
||||
'F12': '\x1b[24~', |
||||
'Shift+F1': () => fa('\x1bO1;2P', '\x1b[25~'), |
||||
'Shift+F2': () => fa('\x1bO1;2Q', '\x1b[26~'), // note the disconnect
|
||||
'Shift+F3': () => fa('\x1bO1;2R', '\x1b[28~'), |
||||
'Shift+F4': () => fa('\x1bO1;2S', '\x1b[29~'), // note the disconnect
|
||||
'Shift+F5': () => fa('\x1b[15;2~', '\x1b[31~'), |
||||
'Shift+F6': () => fa('\x1b[17;2~', '\x1b[32~'), |
||||
'Shift+F7': () => fa('\x1b[18;2~', '\x1b[33~'), |
||||
'Shift+F8': () => fa('\x1b[19;2~', '\x1b[34~'), |
||||
'Shift+F9': () => fa('\x1b[20;2~', '\x1b[35~'), // 35-38 are not standard - but what is?
|
||||
'Shift+F10': () => fa('\x1b[21;2~', '\x1b[36~'), |
||||
'Shift+F11': () => fa('\x1b[22;2~', '\x1b[37~'), |
||||
'Shift+F12': () => fa('\x1b[23;2~', '\x1b[38~'), |
||||
'Numpad0': () => na('\x1bOp', '0'), |
||||
'Numpad1': () => na('\x1bOq', '1'), |
||||
'Numpad2': () => na('\x1bOr', '2'), |
||||
'Numpad3': () => na('\x1bOs', '3'), |
||||
'Numpad4': () => na('\x1bOt', '4'), |
||||
'Numpad5': () => na('\x1bOu', '5'), |
||||
'Numpad6': () => na('\x1bOv', '6'), |
||||
'Numpad7': () => na('\x1bOw', '7'), |
||||
'Numpad8': () => na('\x1bOx', '8'), |
||||
'Numpad9': () => na('\x1bOy', '9'), |
||||
'Numpad*': () => na('\x1bOR', '*'), |
||||
'Numpad+': () => na('\x1bOl', '+'), |
||||
'Numpad-': () => na('\x1bOS', '-'), |
||||
'Numpad.': () => na('\x1bOn', '.'), |
||||
'Numpad/': () => na('\x1bOQ', '/'), |
||||
// we don't implement numlock key (should change in numpad_alt mode,
|
||||
// but it's even more useless than the rest and also has the side
|
||||
// effect of changing the user's numlock state)
|
||||
|
||||
// shortcuts
|
||||
'Control+]': '\x1b', // alternate way to enter ESC
|
||||
'Control+\\': '\x1c', |
||||
'Control+[': '\x1d', |
||||
'Control+^': '\x1e', |
||||
'Control+_': '\x1f', |
||||
|
||||
// extra controls
|
||||
'Control+ArrowLeft': '\x1f[1;5D', |
||||
'Control+ArrowRight': '\x1f[1;5C', |
||||
'Control+ArrowUp': '\x1f[1;5A', |
||||
'Control+ArrowDown': '\x1f[1;5B', |
||||
'Control+Home': '\x1f[1;5H', |
||||
'Control+End': '\x1f[1;5F', |
||||
|
||||
// extra shift controls
|
||||
'Shift+ArrowLeft': '\x1f[1;2D', |
||||
'Shift+ArrowRight': '\x1f[1;2C', |
||||
'Shift+ArrowUp': '\x1f[1;2A', |
||||
'Shift+ArrowDown': '\x1f[1;2B', |
||||
'Shift+Home': '\x1f[1;2H', |
||||
'Shift+End': '\x1f[1;2F', |
||||
|
||||
// macOS text editing commands
|
||||
'Alt+ArrowLeft': '\x1bb', // ⌥← to go back a word (^[b)
|
||||
'Alt+ArrowRight': '\x1bf', // ⌥→ to go forward one word (^[f)
|
||||
'Meta+ArrowLeft': '\x01', // ⌘← to go to the beginning of a line (^A)
|
||||
'Meta+ArrowRight': '\x05', // ⌘→ to go to the end of a line (^E)
|
||||
'Alt+Backspace': '\x17', // ⌥⌫ to delete a word (^W)
|
||||
'Meta+Backspace': '\x15', // ⌘⌫ to delete to the beginning of a line (^U)
|
||||
|
||||
// copy to clipboard
|
||||
'Control+Shift+C' () { |
||||
screen.copySelectionToClipboard() |
||||
}, |
||||
'Control+Insert' () { |
||||
screen.copySelectionToClipboard() |
||||
}, |
||||
|
||||
// toggle debug mode
|
||||
'Control+F12' () { |
||||
screen.window.debug ^= 1 |
||||
} |
||||
/* eslint-enable key-spacing */ |
||||
} |
||||
|
||||
// ctrl+[A-Z] sent as simple low ASCII codes
|
||||
for (let i = 1; i <= 26; i++) { |
||||
keymap[`Control+${String.fromCharCode(0x40 + i)}`] = String.fromCharCode(i) |
||||
} |
||||
|
||||
/** Send a literal message */ |
||||
function sendString (str) { |
||||
return conn.send('s' + str) |
||||
} |
||||
|
||||
/** Send a button event */ |
||||
function sendButton (index) { |
||||
conn.send('b' + String.fromCharCode(index + 1)) |
||||
} |
||||
|
||||
const shouldAcceptEvent = function () { |
||||
if (cfg.no_keys) return false |
||||
if (document.activeElement instanceof window.HTMLTextAreaElement) return false |
||||
return true |
||||
} |
||||
|
||||
const keyBlacklist = [ |
||||
'F5', 'F11', 'F12', 'Shift+F5' |
||||
] |
||||
|
||||
let softModifiers = { |
||||
alt: false, |
||||
ctrl: false, |
||||
meta: false, |
||||
shift: false |
||||
} |
||||
|
||||
const handleKeyDown = function (e) { |
||||
if (!shouldAcceptEvent()) return |
||||
|
||||
let modifiers = [] |
||||
// sorted alphabetically
|
||||
if (e.altKey || softModifiers.alt) modifiers.push('Alt') |
||||
if (e.ctrlKey || softModifiers.ctrl) modifiers.push('Control') |
||||
if (e.metaKey || softModifiers.meta) modifiers.push('Meta') |
||||
if (e.shiftKey || softModifiers.shift) modifiers.push('Shift') |
||||
|
||||
let key = KEY_NAMES[e.which] || e.key |
||||
|
||||
// ignore clipboard events
|
||||
if ((e.ctrlKey || e.metaKey) && key === 'V') return |
||||
|
||||
let binding = null |
||||
|
||||
for (let name in keymap) { |
||||
let itemModifiers = name.split('+') |
||||
let itemKey = itemModifiers.pop() |
||||
|
||||
if (itemKey === key && itemModifiers.sort().join() === modifiers.join()) { |
||||
if (keyBlacklist.includes(name) && !cfg.all_fn) continue |
||||
binding = keymap[name] |
||||
break |
||||
} |
||||
} |
||||
|
||||
if (binding) { |
||||
if (binding instanceof Function) binding = binding() |
||||
e.preventDefault() |
||||
if (typeof binding === 'string') { |
||||
sendString(binding) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** Bind/rebind key messages */ |
||||
function initKeys ({ allFn }) { |
||||
// This takes care of text characters typed
|
||||
window.addEventListener('keypress', function (evt) { |
||||
if (!shouldAcceptEvent()) return |
||||
if (evt.ctrlKey || evt.metaKey) return |
||||
|
||||
let str = '' |
||||
if (evt.key && evt.key.length === 1) str = evt.key |
||||
else if (evt.which && evt.which !== 229) str = String.fromCodePoint(evt.which) |
||||
|
||||
if (str.length > 0 && str.charCodeAt(0) >= 32) { |
||||
// prevent space from scrolling
|
||||
if (evt.which === 32) evt.preventDefault() |
||||
sendString(str) |
||||
} |
||||
}) |
||||
|
||||
window.addEventListener('keydown', handleKeyDown) |
||||
window.addEventListener('copy', e => { |
||||
if (!shouldAcceptEvent()) return |
||||
let selectedText = screen.getSelectedText() |
||||
if (selectedText) { |
||||
e.preventDefault() |
||||
e.clipboardData.setData('text/plain', selectedText) |
||||
} |
||||
}) |
||||
window.addEventListener('paste', e => { |
||||
if (!shouldAcceptEvent()) return |
||||
e.preventDefault() |
||||
let string = e.clipboardData.getData('text/plain') |
||||
if (string.includes('\n') || string.length > 90) { |
||||
if (!input.termUpload) console.error('input.termUpload is undefined') |
||||
input.termUpload.setContent(string) |
||||
input.termUpload.open() |
||||
} else { |
||||
// simple string, just paste it
|
||||
if (screen.bracketedPaste) string = `\x1b[200~${string}\x1b[201~` |
||||
sendString(string) |
||||
} |
||||
}) |
||||
|
||||
cfg.all_fn = allFn |
||||
} |
||||
|
||||
// mouse button states
|
||||
let mb1 = 0 |
||||
let mb2 = 0 |
||||
let mb3 = 0 |
||||
|
||||
/** Init the Input module */ |
||||
function init (opts) { |
||||
initKeys(opts) |
||||
|
||||
// global mouse state tracking - for motion reporting
|
||||
window.addEventListener('mousedown', evt => { |
||||
if (evt.button === 0) mb1 = 1 |
||||
if (evt.button === 1) mb2 = 1 |
||||
if (evt.button === 2) mb3 = 1 |
||||
}) |
||||
|
||||
window.addEventListener('mouseup', evt => { |
||||
if (evt.button === 0) mb1 = 0 |
||||
if (evt.button === 1) mb2 = 0 |
||||
if (evt.button === 2) mb3 = 0 |
||||
}) |
||||
} |
||||
|
||||
// record modifier keys
|
||||
// bits: Meta, Alt, Shift, Ctrl
|
||||
let modifiers = 0b0000 |
||||
|
||||
window.addEventListener('keydown', e => { |
||||
if (e.ctrlKey) modifiers |= 1 |
||||
if (e.shiftKey) modifiers |= 2 |
||||
if (e.altKey) modifiers |= 4 |
||||
if (e.metaKey) modifiers |= 8 |
||||
}) |
||||
window.addEventListener('keyup', e => { |
||||
modifiers = 0 |
||||
if (e.ctrlKey) modifiers |= 1 |
||||
if (e.shiftKey) modifiers |= 2 |
||||
if (e.altKey) modifiers |= 4 |
||||
if (e.metaKey) modifiers |= 8 |
||||
}) |
||||
|
||||
/** Prepare modifiers byte for mouse message */ |
||||
function packModifiersForMouse () { |
||||
return modifiers |
||||
} |
||||
|
||||
input = { |
||||
/** Init the Input module */ |
||||
init, |
||||
|
||||
/** Send a literal string message */ |
||||
sendString, |
||||
sendButton, |
||||
|
||||
/** Enable alternate key modes (cursors, numpad, fn) */ |
||||
setAlts: function (cu, np, fn, crlf) { |
||||
if (cfg.cu_alt !== cu || cfg.np_alt !== np || cfg.fn_alt !== fn || cfg.crlf_mode !== crlf) { |
||||
cfg.cu_alt = cu |
||||
cfg.np_alt = np |
||||
cfg.fn_alt = fn |
||||
cfg.crlf_mode = crlf |
||||
} |
||||
}, |
||||
|
||||
setMouseMode (click, move) { |
||||
cfg.mt_click = click |
||||
cfg.mt_move = move |
||||
}, |
||||
|
||||
// Mouse events
|
||||
onMouseMove (x, y) { |
||||
if (!cfg.mt_move) return |
||||
const b = mb1 ? 1 : mb2 ? 2 : mb3 ? 3 : 0 |
||||
const m = packModifiersForMouse() |
||||
conn.send('m' + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)) |
||||
}, |
||||
|
||||
onMouseDown (x, y, b) { |
||||
if (!cfg.mt_click) return |
||||
if (b > 3 || b < 1) return |
||||
const m = packModifiersForMouse() |
||||
conn.send('p' + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)) |
||||
}, |
||||
|
||||
onMouseUp (x, y, b) { |
||||
if (!cfg.mt_click) return |
||||
if (b > 3 || b < 1) return |
||||
const m = packModifiersForMouse() |
||||
conn.send('r' + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)) |
||||
// console.log("B ",b," M ",m);
|
||||
}, |
||||
|
||||
onMouseWheel (x, y, dir) { |
||||
if (!cfg.mt_click) return |
||||
// -1 ... btn 4 (away from user)
|
||||
// +1 ... btn 5 (towards user)
|
||||
const m = packModifiersForMouse() |
||||
const b = (dir < 0 ? 4 : 5) |
||||
conn.send('p' + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)) |
||||
// console.log("B ",b," M ",m);
|
||||
}, |
||||
|
||||
/** |
||||
* Prevent capturing keys. This is used for text input |
||||
* modals on the terminal screen |
||||
*/ |
||||
blockKeys (yes) { |
||||
cfg.no_keys = yes |
||||
}, |
||||
|
||||
handleKeyDown, |
||||
softModifiers |
||||
} |
||||
return input |
||||
} |
@ -0,0 +1,590 @@ |
||||
const EventEmitter = require('events') |
||||
const { mk } = require('../utils') |
||||
const notify = require('../notif') |
||||
const ScreenParser = require('./screen_parser') |
||||
const ScreenLayout = require('./screen_layout') |
||||
const { ATTR_BLINK } = require('./screen_attr_bits') |
||||
|
||||
/** |
||||
* A terminal screen. |
||||
*/ |
||||
module.exports = class TermScreen extends EventEmitter { |
||||
constructor () { |
||||
super() |
||||
|
||||
this.parser = new ScreenParser() |
||||
this.layout = new ScreenLayout() |
||||
|
||||
// debug screen handle
|
||||
this._debug = null |
||||
|
||||
if ('AudioContext' in window || 'webkitAudioContext' in window) { |
||||
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)() |
||||
} else { |
||||
console.warn('No AudioContext!') |
||||
} |
||||
|
||||
this._window = { |
||||
width: 0, |
||||
height: 0, |
||||
// two bits. LSB: debug enabled by user, MSB: debug enabled by server
|
||||
debug: 0, |
||||
statusScreen: null |
||||
} |
||||
|
||||
// make writing to window update size and draw
|
||||
this.window = new Proxy(this._window, { |
||||
set (target, key, value) { |
||||
if (target[key] !== value) { |
||||
target[key] = value |
||||
self.updateLayout() |
||||
self.renderScreen(`window:${key}=${value}`) |
||||
self.emit(`update-window:${key}`, value) |
||||
} |
||||
return true |
||||
} |
||||
}) |
||||
|
||||
this.on('update-window:debug', debug => { this.layout.window.debug = !!debug }) |
||||
|
||||
this.cursor = { |
||||
x: 0, |
||||
y: 0, |
||||
blinking: true, |
||||
visible: true, |
||||
hanging: false, |
||||
style: 'block' |
||||
} |
||||
|
||||
const self = this |
||||
|
||||
// current selection
|
||||
this.selection = { |
||||
// when false, this will prevent selection in favor of mouse events,
|
||||
// though alt can be held to override it
|
||||
selectable: null, |
||||
|
||||
// selection start and end (x, y) tuples
|
||||
start: [0, 0], |
||||
end: [0, 0], |
||||
|
||||
setSelectable (value) { |
||||
if (value !== this.selectable) { |
||||
this.selectable = self.layout.selectable = value |
||||
} |
||||
} |
||||
} |
||||
|
||||
// mouse features
|
||||
this.mouseMode = { clicks: false, movement: false } |
||||
|
||||
this.showLinks = false |
||||
this.showButtons = false |
||||
this.title = '' |
||||
|
||||
this.bracketedPaste = false |
||||
this.blinkingCellCount = 0 |
||||
this.reverseVideo = false |
||||
|
||||
this.screen = [] |
||||
this.screenFG = [] |
||||
this.screenBG = [] |
||||
this.screenAttrs = [] |
||||
this.screenLines = [] |
||||
|
||||
// For testing TODO remove
|
||||
// this.screenLines[0] = 0b001
|
||||
// this.screenLines[1] = 0b010
|
||||
// this.screenLines[2] = 0b100
|
||||
// this.screenLines[3] = 0b011
|
||||
// this.screenLines[4] = 0b101
|
||||
|
||||
let selecting = false |
||||
|
||||
let selectStart = (x, y) => { |
||||
if (selecting) return |
||||
selecting = true |
||||
this.selection.start = this.selection.end = this.layout.screenToGrid(x, y, true) |
||||
this.renderScreen('select-start') |
||||
} |
||||
|
||||
let selectMove = (x, y) => { |
||||
if (!selecting) return |
||||
this.selection.end = this.layout.screenToGrid(x, y, true) |
||||
this.renderScreen('select-move') |
||||
} |
||||
|
||||
let selectEnd = (x, y) => { |
||||
if (!selecting) return |
||||
selecting = false |
||||
this.selection.end = this.layout.screenToGrid(x, y, true) |
||||
this.renderScreen('select-end') |
||||
Object.assign(this.selection, this.getNormalizedSelection()) |
||||
} |
||||
|
||||
// bind event listeners
|
||||
|
||||
this.layout.on('mousedown', e => { |
||||
this.emit('hide-touch-select-menu') |
||||
if ((this.selection.selectable || e.altKey) && e.button === 0) { |
||||
selectStart(e.offsetX, e.offsetY) |
||||
} else { |
||||
this.emit('mousedown', ...this.layout.screenToGrid(e.offsetX, e.offsetY), e.button + 1) |
||||
} |
||||
}) |
||||
|
||||
window.addEventListener('mousemove', e => { |
||||
selectMove(e.offsetX, e.offsetY) |
||||
}) |
||||
|
||||
window.addEventListener('mouseup', e => { |
||||
selectEnd(e.offsetX, e.offsetY) |
||||
}) |
||||
|
||||
// touch event listeners
|
||||
|
||||
let touchPosition = null |
||||
let touchDownTime = 0 |
||||
let touchSelectMinTime = 500 |
||||
let touchDidMove = false |
||||
|
||||
let getTouchPositionOffset = touch => { |
||||
let rect = this.layout.canvas.getBoundingClientRect() |
||||
return [touch.clientX - rect.left, touch.clientY - rect.top] |
||||
} |
||||
|
||||
this.layout.on('touchstart', e => { |
||||
touchPosition = getTouchPositionOffset(e.touches[0]) |
||||
touchDidMove = false |
||||
touchDownTime = Date.now() |
||||
|
||||
if (this.mouseMode.clicks) { |
||||
this.emit('mousedown', ...this.layout.screenToGrid(...touchPosition), 1) |
||||
e.preventDefault() |
||||
} |
||||
}) |
||||
|
||||
this.layout.on('touchmove', e => { |
||||
touchPosition = getTouchPositionOffset(e.touches[0]) |
||||
|
||||
if (!selecting && touchDidMove === false) { |
||||
if (touchDownTime < Date.now() - touchSelectMinTime) { |
||||
selectStart(...touchPosition) |
||||
} |
||||
} else if (selecting) { |
||||
e.preventDefault() |
||||
selectMove(...touchPosition) |
||||
} else if (this.mouseMode.movement && !selecting) { |
||||
this.emit('mousemove', ...this.layout.screenToGrid(...touchPosition)) |
||||
e.preventDefault() |
||||
} |
||||
|
||||
touchDidMove = true |
||||
}) |
||||
|
||||
this.layout.on('touchend', e => { |
||||
if (e.touches[0]) { |
||||
touchPosition = getTouchPositionOffset(e.touches[0]) |
||||
} |
||||
|
||||
if (selecting) { |
||||
e.preventDefault() |
||||
selectEnd(...touchPosition) |
||||
|
||||
// selection ended; show touch select menu
|
||||
// use middle position for x and one line above for y
|
||||
let selectionPos = this.layout.gridToScreen( |
||||
(this.selection.start[0] + this.selection.end[0]) / 2, |
||||
this.selection.start[1] - 1 |
||||
) |
||||
|
||||
this.emit('show-touch-select-menu', selectionPos[0], selectionPos[1]) |
||||
} else if (this.mouseMode.clicks) { |
||||
this.emit('mouseup', ...this.layout.screenToGrid(...touchPosition), 1) |
||||
e.preventDefault() |
||||
} |
||||
|
||||
if (!touchDidMove && !this.mouseMode.clicks) { |
||||
this.emit('tap', Object.assign(e, { |
||||
x: touchPosition[0], |
||||
y: touchPosition[1] |
||||
})) |
||||
} else if (!touchDidMove) this.resetSelection() |
||||
|
||||
touchPosition = null |
||||
}) |
||||
|
||||
this.on('tap', e => { |
||||
if (this.selection.start[0] !== this.selection.end[0] || |
||||
this.selection.start[1] !== this.selection.end[1]) { |
||||
// selection is not empty
|
||||
this.resetSelection() |
||||
} else { |
||||
e.preventDefault() |
||||
this.emit('open-soft-keyboard') |
||||
} |
||||
}) |
||||
|
||||
this.layout.on('mousemove', e => { |
||||
if (!selecting) { |
||||
this.emit('mousemove', ...this.layout.screenToGrid(e.offsetX, e.offsetY)) |
||||
} |
||||
}) |
||||
|
||||
this.layout.on('mouseup', e => { |
||||
if (!selecting) { |
||||
this.emit('mouseup', ...this.layout.screenToGrid(e.offsetX, e.offsetY), |
||||
e.button + 1) |
||||
} |
||||
}) |
||||
|
||||
let aggregateWheelDelta = 0 |
||||
this.layout.on('wheel', e => { |
||||
if (this.mouseMode.clicks) { |
||||
if (Math.abs(e.wheelDeltaY) === 120) { |
||||
// mouse wheel scrolling
|
||||
this.emit('mousewheel', ...this.layout.screenToGrid(e.offsetX, e.offsetY), e.deltaY > 0 ? 1 : -1) |
||||
} else { |
||||
// smooth scrolling
|
||||
aggregateWheelDelta -= e.wheelDeltaY |
||||
if (Math.abs(aggregateWheelDelta) >= 40) { |
||||
this.emit('mousewheel', ...this.layout.screenToGrid(e.offsetX, e.offsetY), aggregateWheelDelta > 0 ? 1 : -1) |
||||
aggregateWheelDelta = 0 |
||||
} |
||||
} |
||||
|
||||
// prevent page scrolling
|
||||
e.preventDefault() |
||||
} |
||||
}) |
||||
|
||||
this.layout.on('contextmenu', e => { |
||||
if (this.mouseMode.clicks) { |
||||
// prevent mouse keys getting stuck
|
||||
e.preventDefault() |
||||
} |
||||
selectEnd(e.offsetX, e.offsetY) |
||||
}) |
||||
} |
||||
|
||||
resetScreen () { |
||||
const { width, height } = this.window |
||||
this.blinkingCellCount = 0 |
||||
this.screen.screen = new Array(width * height).fill(' ') |
||||
this.screen.screenFG = new Array(width * height).fill(0) |
||||
this.screen.screenBG = new Array(width * height).fill(0) |
||||
this.screen.screenAttrs = new Array(width * height).fill(0) |
||||
this.screen.screenLines = new Array(height).fill(0) |
||||
} |
||||
|
||||
updateLayout () { |
||||
this.layout.window.width = this.window.width |
||||
this.layout.window.height = this.window.height |
||||
} |
||||
|
||||
renderScreen (reason) { |
||||
let selection = [] |
||||
|
||||
for (let cell = 0; cell < this.screen.length; cell++) { |
||||
selection.push(this.isInSelection(cell % this.window.width, Math.floor(cell / this.window.width))) |
||||
} |
||||
|
||||
this.layout.render(reason, { |
||||
width: this.window.width, |
||||
height: this.window.height, |
||||
screen: this.screen, |
||||
screenFG: this.screenFG, |
||||
screenBG: this.screenBG, |
||||
screenSelection: selection, |
||||
screenAttrs: this.screenAttrs, |
||||
screenLines: this.screenLines, |
||||
cursor: this.cursor, |
||||
statusScreen: this.window.statusScreen, |
||||
reverseVideo: this.reverseVideo, |
||||
hasBlinkingCells: !!this.blinkingCellCount |
||||
}) |
||||
} |
||||
|
||||
resetSelection () { |
||||
this.selection.start = this.selection.end = [0, 0] |
||||
this.emit('hide-touch-select-menu') |
||||
this.renderScreen('select-reset') |
||||
} |
||||
|
||||
/** |
||||
* Returns a normalized version of the current selection, such that `start` |
||||
* is always before `end`. |
||||
* @returns {Object} the normalized selection, with `start` and `end` |
||||
*/ |
||||
getNormalizedSelection () { |
||||
let { start, end } = this.selection |
||||
// if the start line is after the end line, or if they're both on the same
|
||||
// line but the start column comes after the end column, swap
|
||||
if (start[1] > end[1] || (start[1] === end[1] && start[0] > end[0])) { |
||||
[start, end] = [end, start] |
||||
} |
||||
return { start, end } |
||||
} |
||||
|
||||
/** |
||||
* Returns whether or not a given cell is in the current selection. |
||||
* @param {number} col - the column (x) |
||||
* @param {number} line - the line (y) |
||||
* @returns {boolean} |
||||
*/ |
||||
isInSelection (col, line) { |
||||
let { start, end } = this.getNormalizedSelection() |
||||
let colAfterStart = start[0] <= col |
||||
let colBeforeEnd = col < end[0] |
||||
let onStartLine = line === start[1] |
||||
let onEndLine = line === end[1] |
||||
|
||||
if (onStartLine && onEndLine) return colAfterStart && colBeforeEnd |
||||
else if (onStartLine) return colAfterStart |
||||
else if (onEndLine) return colBeforeEnd |
||||
else return start[1] < line && line < end[1] |
||||
} |
||||
|
||||
/** |
||||
* Sweeps for selected cells and joins them in a multiline string. |
||||
* @returns {string} the selection |
||||
*/ |
||||
getSelectedText () { |
||||
const screenLength = this.window.width * this.window.height |
||||
let lines = [] |
||||
let previousLineIndex = -1 |
||||
|
||||
for (let cell = 0; cell < screenLength; cell++) { |
||||
let x = cell % this.window.width |
||||
let y = Math.floor(cell / this.window.width) |
||||
|
||||
if (this.isInSelection(x, y)) { |
||||
if (previousLineIndex !== y) { |
||||
previousLineIndex = y |
||||
lines.push('') |
||||
} |
||||
lines[lines.length - 1] += this.screen[cell] |
||||
} |
||||
} |
||||
|
||||
return lines.join('\n') |
||||
} |
||||
|
||||
/** |
||||
* Copies the selection to clipboard and creates a notification balloon. |
||||
*/ |
||||
copySelectionToClipboard () { |
||||
let selectedText = this.getSelectedText() |
||||
// don't copy anything if nothing is selected
|
||||
if (!selectedText) return |
||||
let textarea = mk('textarea') |
||||
document.body.appendChild(textarea) |
||||
textarea.value = selectedText |
||||
textarea.select() |
||||
if (document.execCommand('copy')) { |
||||
notify.show('Copied to clipboard') |
||||
} else { |
||||
notify.show('Failed to copy') |
||||
} |
||||
document.body.removeChild(textarea) |
||||
} |
||||
|
||||
/** |
||||
* Shows an actual notification (if possible) or a notification balloon. |
||||
* @param {string} text - the notification content |
||||
*/ |
||||
showNotification (text) { |
||||
console.info(`Notification: ${text}`) |
||||
if (window.Notification && window.Notification.permission === 'granted') { |
||||
let notification = new window.Notification('ESPTerm', { |
||||
body: text |
||||
}) |
||||
notification.addEventListener('click', () => window.focus()) |
||||
} else { |
||||
if (window.Notification && window.Notification.permission !== 'denied') { |
||||
window.Notification.requestPermission() |
||||
} else { |
||||
// Fallback using the built-in notification balloon
|
||||
notify.show(text) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Creates a beep sound. |
||||
*/ |
||||
beep () { |
||||
const audioCtx = this.audioCtx |
||||
if (!audioCtx) return |
||||
|
||||
// prevent screeching
|
||||
if (this._lastBeep && this._lastBeep > Date.now() - 50) return |
||||
this._lastBeep = Date.now() |
||||
|
||||
if (!this._convolver) { |
||||
this._convolver = audioCtx.createConvolver() |
||||
let impulseLength = audioCtx.sampleRate * 0.8 |
||||
let impulse = audioCtx.createBuffer(2, impulseLength, audioCtx.sampleRate) |
||||
for (let i = 0; i < impulseLength; i++) { |
||||
impulse.getChannelData(0)[i] = (1 - i / impulseLength) ** (7 + Math.random()) |
||||
impulse.getChannelData(1)[i] = (1 - i / impulseLength) ** (7 + Math.random()) |
||||
} |
||||
this._convolver.buffer = impulse |
||||
this._convolver.connect(audioCtx.destination) |
||||
} |
||||
|
||||
// main beep
|
||||
const mainOsc = audioCtx.createOscillator() |
||||
const mainGain = audioCtx.createGain() |
||||
mainOsc.connect(mainGain) |
||||
mainGain.gain.value = 4 |
||||
mainOsc.frequency.value = 750 |
||||
mainOsc.type = 'sine' |
||||
|
||||
// surrogate beep (making it sound like 'oops')
|
||||
const surrOsc = audioCtx.createOscillator() |
||||
const surrGain = audioCtx.createGain() |
||||
surrOsc.connect(surrGain) |
||||
surrGain.gain.value = 2 |
||||
surrOsc.frequency.value = 400 |
||||
surrOsc.type = 'sine' |
||||
|
||||
mainGain.connect(this._convolver) |
||||
surrGain.connect(this._convolver) |
||||
|
||||
let startTime = audioCtx.currentTime |
||||
mainOsc.start() |
||||
mainOsc.stop(startTime + 0.5) |
||||
surrOsc.start(startTime + 0.05) |
||||
surrOsc.stop(startTime + 0.8) |
||||
|
||||
let loop = function () { |
||||
if (audioCtx.currentTime < startTime + 0.8) window.requestAnimationFrame(loop) |
||||
mainGain.gain.value *= 0.8 |
||||
surrGain.gain.value *= 0.8 |
||||
} |
||||
loop() |
||||
} |
||||
|
||||
load (...args) { |
||||
const updates = this.parser.parse(...args) |
||||
|
||||
for (let update of updates) { |
||||
switch (update.topic) { |
||||
case 'screen-opts': |
||||
if (update.width !== this.window.width || update.height !== this.window.height) { |
||||
this.window.width = update.width |
||||
this.window.height = update.height |
||||
this.resetScreen() |
||||
} |
||||
this.layout.renderer.loadTheme(update.theme) |
||||
this.layout.renderer.setDefaultColors(update.defFG, update.defBG) |
||||
this.cursor.visible = update.cursorVisible |
||||
this.emit('input-alts', ...update.inputAlts) |
||||
this.mouseMode.clicks = update.trackMouseClicks |
||||
this.mouseMode.movement = update.trackMouseMovement |
||||
this.emit('mouse-mode', update.trackMouseClicks, update.trackMouseMovement) |
||||
this.selection.setSelectable(!update.trackMouseClicks && !update.trackMouseMovement) |
||||
if (this.cursor.blinking !== update.cursorBlinking) { |
||||
this.cursor.blinking = update.cursorBlinking |
||||
this.layout.renderer.resetCursorBlink() |
||||
} |
||||
this.cursor.style = update.cursorStyle |
||||
this.bracketedPaste = update.bracketedPaste |
||||
this.reverseVideo = update.reverseVideo |
||||
this.window.debug &= 0b01 |
||||
this.window.debug |= (+update.debugEnabled << 1) |
||||
|
||||
this.showLinks = update.showConfigLinks |
||||
this.showButtons = update.showButtons |
||||
this.emit('opts-update') |
||||
break |
||||
|
||||
case 'double-lines': |
||||
this.screenLines = update.lines |
||||
this.renderScreen('double-lines') |
||||
break |
||||
|
||||
case 'static-opts': |
||||
this.layout.window.fontFamily = update.fontStack || null |
||||
this.layout.window.fontSize = update.fontSize |
||||
break |
||||
|
||||
case 'cursor': |
||||
if (this.cursor.x !== update.x || this.cursor.y !== update.y || this.cursor.hanging !== update.hanging) { |
||||
this.cursor.x = update.x |
||||
this.cursor.y = update.y |
||||
this.cursor.hanging = update.hanging |
||||
this.layout.renderer.resetCursorBlink() |
||||
this.emit('cursor-moved') |
||||
this.renderScreen('cursor-moved') |
||||
} |
||||
break |
||||
|
||||
case 'title': |
||||
this.emit('title-update', this.title = update.title) |
||||
break |
||||
|
||||
case 'buttons-update': |
||||
this.emit('buttons-update', update) |
||||
break |
||||
|
||||
case 'backdrop': |
||||
this.backgroundImage = update.image |
||||
break |
||||
|
||||
case 'bell': |
||||
this.beep() |
||||
break |
||||
|
||||
case 'internal': |
||||
this.emit('internal', update) |
||||
break |
||||
|
||||
case 'content': |
||||
const { frameX, frameY, frameWidth, frameHeight, cells } = update |
||||
|
||||
if (this._debug && this.window.debug) { |
||||
this._debug.pushFrame([frameX, frameY, frameWidth, frameHeight]) |
||||
} |
||||
|
||||
for (let cell = 0; cell < cells.length; cell++) { |
||||
let data = cells[cell] |
||||
|
||||
let cellXInFrame = cell % frameWidth |
||||
let cellYInFrame = Math.floor(cell / frameWidth) |
||||
let index = (frameY + cellYInFrame) * this.window.width + frameX + cellXInFrame |
||||
|
||||
if ((this.screenAttrs[index] & ATTR_BLINK) !== (data[3] & ATTR_BLINK)) { |
||||
if (data[3] & ATTR_BLINK) this.blinkingCellCount++ |
||||
else this.blinkingCellCount-- |
||||
} |
||||
|
||||
this.screen[index] = data[0] |
||||
this.screenFG[index] = data[1] |
||||
this.screenBG[index] = data[2] |
||||
this.screenAttrs[index] = data[3] |
||||
} |
||||
|
||||
if (this.window.debug) console.log(`Blinking cells: ${this.blinkingCellCount}`) |
||||
|
||||
this.renderScreen('load') |
||||
this.emit('load') |
||||
break |
||||
|
||||
case 'full-load-complete': |
||||
this.emit('full-load') |
||||
break |
||||
|
||||
case 'notification': |
||||
this.showNotification(update.content) |
||||
break |
||||
|
||||
default: |
||||
console.warn('Unhandled update', update) |
||||
} |
||||
} |
||||
|
||||
this.emit('update') |
||||
} |
||||
} |
@ -0,0 +1,15 @@ |
||||
// Bits in the cell attribs word
|
||||
|
||||
/* eslint-disable no-multi-spaces */ |
||||
exports.ATTR_FG = (1 << 0) // 1 if not using default background color (ignore cell bg) - color extension bit
|
||||
exports.ATTR_BG = (1 << 1) // 1 if not using default foreground color (ignore cell fg) - color extension bit
|
||||
exports.ATTR_BOLD = (1 << 2) // Bold font
|
||||
exports.ATTR_UNDERLINE = (1 << 3) // Underline decoration
|
||||
exports.ATTR_INVERSE = (1 << 4) // Invert colors - this is useful so we can clear then with SGR manipulation commands
|
||||
exports.ATTR_BLINK = (1 << 5) // Blinking
|
||||
exports.ATTR_ITALIC = (1 << 6) // Italic font
|
||||
exports.ATTR_STRIKE = (1 << 7) // Strike-through decoration
|
||||
exports.ATTR_OVERLINE = (1 << 8) // Over-line decoration
|
||||
exports.ATTR_FAINT = (1 << 9) // Faint foreground color (reduced alpha)
|
||||
exports.ATTR_FRAKTUR = (1 << 10) // Fraktur font (unicode substitution)
|
||||
/* eslint-enable no-multi-spaces */ |
@ -0,0 +1,285 @@ |
||||
const EventEmitter = require('events') |
||||
const CanvasRenderer = require('./screen_renderer') |
||||
|
||||
const DEFAULT_FONT = '"DejaVu Sans Mono", "Liberation Mono", "Inconsolata", "Menlo", monospace' |
||||
|
||||
/** |
||||
* Manages terminal screen layout and sizing |
||||
*/ |
||||
module.exports = class ScreenLayout extends EventEmitter { |
||||
constructor () { |
||||
super() |
||||
|
||||
this.canvas = document.createElement('canvas') |
||||
this.renderer = new CanvasRenderer(this.canvas) |
||||
|
||||
this._window = { |
||||
width: 0, |
||||
height: 0, |
||||
devicePixelRatio: 1, |
||||
fontFamily: DEFAULT_FONT, |
||||
fontSize: 20, |
||||
padding: 6, |
||||
gridScaleX: 1.0, |
||||
gridScaleY: 1.2, |
||||
fitIntoWidth: 0, |
||||
fitIntoHeight: 0, |
||||
debug: false |
||||
} |
||||
|
||||
// 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: '', |
||||
fontSize: 0, |
||||
fitIntoWidth: 0, |
||||
fitIntoHeight: 0 |
||||
} |
||||
|
||||
this.charSize = { width: 0, height: 0 } |
||||
|
||||
const self = this |
||||
|
||||
// make writing to window update size and draw
|
||||
this.window = new Proxy(this._window, { |
||||
set (target, 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 |
||||
} |
||||
}) |
||||
|
||||
this.on('update-window:debug', debug => { this.renderer.debug = debug }) |
||||
|
||||
this.canvas.addEventListener('mousedown', e => this.emit('mousedown', e)) |
||||
this.canvas.addEventListener('mousemove', e => this.emit('mousemove', e)) |
||||
this.canvas.addEventListener('mouseup', e => this.emit('mouseup', e)) |
||||
this.canvas.addEventListener('touchstart', e => this.emit('touchstart', e)) |
||||
this.canvas.addEventListener('touchmove', e => this.emit('touchmove', e)) |
||||
this.canvas.addEventListener('touchend', e => this.emit('touchend', e)) |
||||
this.canvas.addEventListener('wheel', e => this.emit('wheel', e)) |
||||
this.canvas.addEventListener('contextmenu', e => this.emit('contextmenu', e)) |
||||
} |
||||
|
||||
/** |
||||
* Schedule a size update in the next millisecond |
||||
*/ |
||||
scheduleSizeUpdate () { |
||||
clearTimeout(this._scheduledSizeUpdate) |
||||
this._scheduledSizeUpdate = setTimeout(() => this.updateSize(), 1) |
||||
} |
||||
|
||||
get backgroundImage () { |
||||
return this.canvas.style.backgroundImage |
||||
} |
||||
|
||||
set backgroundImage (value) { |
||||
this.canvas.style.backgroundImage = value ? `url(${value})` : '' |
||||
if (this.renderer.backgroundImage !== !!value) { |
||||
this.renderer.backgroundImage = !!value |
||||
this.renderer.resetDrawn() |
||||
this.renderer.scheduleDraw('background-image') |
||||
} |
||||
} |
||||
|
||||
get selectable () { |
||||
return this.canvas.classList.contains('selectable') |
||||
} |
||||
|
||||
set selectable (selectable) { |
||||
if (selectable) this.canvas.classList.add('selectable') |
||||
else this.canvas.classList.remove('selectable') |
||||
} |
||||
|
||||
/** |
||||
* Returns a CSS font string with the current font settings and the |
||||
* specified modifiers. |
||||
* @param {Object} modifiers |
||||
* @param {string} [modifiers.style] - the font style |
||||
* @param {string} [modifiers.weight] - the font weight |
||||
* @returns {string} a CSS font string |
||||
*/ |
||||
getFont (modifiers = {}) { |
||||
let fontStyle = modifiers.style || 'normal' |
||||
let fontWeight = modifiers.weight || 'normal' |
||||
let fontFamily = this.window.fontFamily || '' |
||||
if (fontFamily.length > 0) fontFamily += ',' |
||||
fontFamily += DEFAULT_FONT |
||||
return `${fontStyle} normal ${fontWeight} ${this.window.fontSize}px ${fontFamily}` |
||||
} |
||||
|
||||
/** |
||||
* Converts screen coordinates to grid coordinates. |
||||
* @param {number} x - x in pixels |
||||
* @param {number} y - y in pixels |
||||
* @param {boolean} rounded - whether to round the coord, used for select highlighting |
||||
* @returns {number[]} a tuple of (x, y) in cells |
||||
*/ |
||||
screenToGrid (x, y, rounded = false) { |
||||
let cellSize = this.getCellSize() |
||||
|
||||
x = x / this._windowScale - this._padding |
||||
y = y / this._windowScale - this._padding |
||||
y = Math.floor(y / cellSize.height) |
||||
if (this.renderer.drawnScreenLines[y]) x /= 2 // double size
|
||||
x = Math.floor((x + (rounded ? cellSize.width / 2 : 0)) / cellSize.width) |
||||
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] |
||||
} |
||||
|
||||
/** |
||||
* Converts grid coordinates to screen coordinates. |
||||
* @param {number} x - x in cells |
||||
* @param {number} y - y in cells |
||||
* @param {boolean} [withScale] - when true, will apply window scale |
||||
* @returns {number[]} a tuple of (x, y) in pixels |
||||
*/ |
||||
gridToScreen (x, y, withScale = false) { |
||||
let cellSize = this.getCellSize() |
||||
|
||||
if (this.renderer.drawnScreenLines[y]) x *= 2 // double size
|
||||
|
||||
return [x * cellSize.width, y * cellSize.height].map(v => this._padding + (withScale ? v * this._windowScale : v)) |
||||
} |
||||
|
||||
/** |
||||
* Update the character size, used for calculating the cell size. |
||||
* The space character is used for measuring. |
||||
* @returns {Object} the character size with `width` and `height` in pixels |
||||
*/ |
||||
updateCharSize () { |
||||
this.charSize = { |
||||
width: this.renderer.getCharWidthFor(this.getFont()), |
||||
height: this.window.fontSize |
||||
} |
||||
|
||||
return this.charSize |
||||
} |
||||
|
||||
/** |
||||
* The cell size, which is the character size multiplied by the grid scale. |
||||
* @returns {Object} the cell size with `width` and `height` in pixels |
||||
*/ |
||||
getCellSize () { |
||||
if (!this.charSize.height && this.window.fontSize) this.updateCharSize() |
||||
|
||||
return { |
||||
width: Math.ceil(this.charSize.width * this.window.gridScaleX), |
||||
height: Math.ceil(this.charSize.height * this.window.gridScaleY) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Updates the canvas size if it changed |
||||
*/ |
||||
updateSize () { |
||||
// see below (this is just updating it)
|
||||
this._window.devicePixelRatio = Math.ceil(this._windowScale * (window.devicePixelRatio || 1)) |
||||
|
||||
let didChange = false |
||||
for (let key in this.windowState) { |
||||
if (this.windowState.hasOwnProperty(key) && this.windowState[key] !== this.window[key]) { |
||||
didChange = true |
||||
this.windowState[key] = this.window[key] |
||||
} |
||||
} |
||||
|
||||
if (didChange) { |
||||
const { |
||||
width, |
||||
height, |
||||
fitIntoWidth, |
||||
fitIntoHeight, |
||||
padding |
||||
} = this.window |
||||
|
||||
this.updateCharSize() |
||||
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 |
||||
let fitAspect = fitIntoWidth / fitIntoHeight |
||||
|
||||
if (terminalAspect < fitAspect) { |
||||
// align heights
|
||||
realHeight = fitIntoHeight - 2 * padding |
||||
realWidth = realHeight * terminalAspect |
||||
} else { |
||||
// align widths
|
||||
realWidth = fitIntoWidth - 2 * padding |
||||
realHeight = realWidth / terminalAspect |
||||
} |
||||
} |
||||
|
||||
// store new window scale
|
||||
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.ceil(this._windowScale * (window.devicePixelRatio || 1)) |
||||
|
||||
this.canvas.width = (width * cellSize.width + 2 * Math.round(this._padding)) * devicePixelRatio |
||||
this.canvas.style.width = `${realWidth}px` |
||||
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)
|
||||
this.renderer.resetDrawn() |
||||
|
||||
this.renderer.render('update-size', this.serializeRenderData()) |
||||
|
||||
this.emit('size-update') |
||||
} |
||||
} |
||||
|
||||
serializeRenderData () { |
||||
return { |
||||
padding: Math.round(this._padding), |
||||
devicePixelRatio: this.window.devicePixelRatio, |
||||
charSize: this.charSize, |
||||
cellSize: this.getCellSize(), |
||||
fonts: [ |
||||
this.getFont(), |
||||
this.getFont({ weight: 'bold' }), |
||||
this.getFont({ style: 'italic' }), |
||||
this.getFont({ weight: 'bold', style: 'italic' }) |
||||
] |
||||
} |
||||
} |
||||
|
||||
render (reason, data) { |
||||
this.window.width = data.width |
||||
this.window.height = data.height |
||||
|
||||
Object.assign(data, this.serializeRenderData()) |
||||
|
||||
this.renderer.render(reason, data) |
||||
} |
||||
} |
@ -0,0 +1,416 @@ |
||||
const { |
||||
ATTR_FG, |
||||
ATTR_BG, |
||||
ATTR_BOLD, |
||||
ATTR_UNDERLINE, |
||||
ATTR_BLINK, |
||||
ATTR_STRIKE, |
||||
ATTR_OVERLINE, |
||||
ATTR_FAINT |
||||
} = require('./screen_attr_bits') |
||||
|
||||
// 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 |
||||
|
||||
// decode a number encoded as a unicode code point
|
||||
function du (str) { |
||||
if (!str) return NaN |
||||
let num = str.codePointAt(0) |
||||
if (num > 0xDFFF) num -= 0x800 |
||||
return num - 1 |
||||
} |
||||
|
||||
/* eslint-disable no-multi-spaces */ |
||||
// mnemonic
|
||||
const TOPIC_SCREEN_OPTS = 'O' // O-ptions
|
||||
const TOPIC_STATIC_OPTS = 'P' // P-arams
|
||||
const TOPIC_CONTENT = 'S' // S-creen
|
||||
const TOPIC_TITLE = 'T' // T-itle
|
||||
const TOPIC_BUTTONS = 'B' // B-uttons
|
||||
const TOPIC_CURSOR = 'C' // C-ursor
|
||||
const TOPIC_INTERNAL = 'D' // D-ebug
|
||||
const TOPIC_BELL = '!' // !!!
|
||||
const TOPIC_BACKDROP = 'W' // W-allpaper
|
||||
const TOPIC_DOUBLE_LINES = 'H' // H-uge
|
||||
|
||||
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) |
||||
|
||||
/* eslint-enable no-multi-spaces */ |
||||
|
||||
/** |
||||
* A parser for screen update messages |
||||
*/ |
||||
module.exports = class ScreenParser { |
||||
constructor () { |
||||
// true if full content was loaded
|
||||
this.contentLoaded = false |
||||
} |
||||
|
||||
parseUpdate (str) { |
||||
// console.log(`update ${str}`)
|
||||
|
||||
// current index
|
||||
let ci = 0 |
||||
let strArray = Array.from ? Array.from(str) : str.split('') |
||||
|
||||
let text |
||||
const topics = du(strArray[ci++]) |
||||
|
||||
let collectOneTerminatedString = () => { |
||||
// TODO optimize this
|
||||
text = '' |
||||
while (ci < strArray.length) { |
||||
let c = strArray[ci++] |
||||
if (c !== '\x01') { |
||||
text += c |
||||
} else { |
||||
break |
||||
} |
||||
} |
||||
return text |
||||
} |
||||
|
||||
let collectColor = () => { |
||||
let c = du(strArray[ci++]) |
||||
if (c & 0x10000) { // support for trueColor
|
||||
c &= 0xFFF |
||||
c |= (du(strArray[ci++]) & 0xFFF) << 12 |
||||
c += 256 |
||||
} |
||||
return c |
||||
} |
||||
|
||||
const updates = [] |
||||
|
||||
while (ci < strArray.length) { |
||||
const topic = strArray[ci++] |
||||
|
||||
if (topic === TOPIC_SCREEN_OPTS) { |
||||
const height = du(strArray[ci++]) |
||||
const width = du(strArray[ci++]) |
||||
const theme = du(strArray[ci++]) |
||||
const defFG = collectColor() |
||||
const defBG = collectColor() |
||||
|
||||
// process attributes
|
||||
const attributes = du(strArray[ci++]) |
||||
|
||||
const cursorVisible = !!(attributes & OPT_CURSOR_VISIBLE) |
||||
|
||||
// HACK: input alts are formatted as arguments for Input#setAlts
|
||||
const inputAlts = [ |
||||
!!(attributes & OPT_CURSORS_ALT_MODE), |
||||
!!(attributes & OPT_NUMPAD_ALT_MODE), |
||||
!!(attributes & OPT_FN_ALT_MODE), |
||||
!!(attributes & OPT_CRLF_MODE) |
||||
] |
||||
|
||||
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-- |
||||
let cursorStyle = cursorShape >> 1 |
||||
const cursorBlinking = !(cursorShape & 1) |
||||
if (cursorStyle === 0) cursorStyle = 'block' |
||||
else if (cursorStyle === 1) cursorStyle = 'line' |
||||
else cursorStyle = 'bar' |
||||
|
||||
const showButtons = !!(attributes & OPT_SHOW_BUTTONS) |
||||
const showConfigLinks = !!(attributes & OPT_SHOW_CONFIG_LINKS) |
||||
|
||||
const bracketedPaste = !!(attributes & OPT_BRACKETED_PASTE) |
||||
const reverseVideo = !!(attributes & OPT_REVERSE_VIDEO) |
||||
|
||||
const debugEnabled = !!(attributes & OPT_DEBUGBAR) |
||||
|
||||
updates.push({ |
||||
topic: 'screen-opts', |
||||
width, |
||||
height, |
||||
theme, |
||||
defFG, |
||||
defBG, |
||||
cursorVisible, |
||||
cursorBlinking, |
||||
cursorStyle, |
||||
inputAlts, |
||||
trackMouseClicks, |
||||
trackMouseMovement, |
||||
showButtons, |
||||
showConfigLinks, |
||||
bracketedPaste, |
||||
reverseVideo, |
||||
debugEnabled |
||||
}) |
||||
|
||||
} else if (topic === TOPIC_CURSOR) { |
||||
// cursor position
|
||||
const y = du(strArray[ci++]) |
||||
const x = du(strArray[ci++]) |
||||
const hanging = !!du(strArray[ci++]) |
||||
|
||||
updates.push({ |
||||
topic: 'cursor', |
||||
x, |
||||
y, |
||||
hanging |
||||
}) |
||||
|
||||
} else if (topic === TOPIC_STATIC_OPTS) { |
||||
const fontStack = collectOneTerminatedString() |
||||
const fontSize = du(strArray[ci++]) |
||||
|
||||
updates.push({ |
||||
topic: 'static-opts', |
||||
fontStack, |
||||
fontSize |
||||
}) |
||||
|
||||
} else if (topic === TOPIC_DOUBLE_LINES) { |
||||
let lines = [] |
||||
const count = du(strArray[ci++]) |
||||
for (let i = 0; i < count; i++) { |
||||
// format: INDEX<<3 | (dbl-h-bot : dbl-h-top : dbl-w)
|
||||
let n = du(strArray[ci++]) |
||||
lines[n >> 3] = n & 0b111 |
||||
} |
||||
updates.push({ topic: 'double-lines', lines: lines }) |
||||
|
||||
} else if (topic === TOPIC_TITLE) { |
||||
updates.push({ topic: 'title', title: collectOneTerminatedString() }) |
||||
|
||||
} else if (topic === TOPIC_BUTTONS) { |
||||
const count = du(strArray[ci++]) |
||||
|
||||
let labels = [] |
||||
let colors = [] |
||||
for (let j = 0; j < count; j++) { |
||||
colors.push(collectColor()) |
||||
labels.push(collectOneTerminatedString()) |
||||
} |
||||
|
||||
updates.push({ |
||||
topic: 'buttons-update', |
||||
labels, |
||||
colors |
||||
}) |
||||
|
||||
} else if (topic === TOPIC_BACKDROP) { |
||||
updates.push({ topic: 'backdrop', image: collectOneTerminatedString() }) |
||||
|
||||
} else if (topic === TOPIC_BELL) { |
||||
updates.push({ topic: 'bell' }) |
||||
|
||||
} 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++] |
||||
|
||||
let cursorFg = collectColor() |
||||
let cursorBg = collectColor() |
||||
|
||||
const freeHeap = du(strArray[ci++]) |
||||
const clientCount = du(strArray[ci++]) |
||||
|
||||
updates.push({ |
||||
topic: 'internal', |
||||
flags, |
||||
cursorAttrs, |
||||
regionStart, |
||||
regionEnd, |
||||
charsetGx, |
||||
charsetG0, |
||||
charsetG1, |
||||
cursorFg, |
||||
cursorBg, |
||||
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++]) |
||||
|
||||
// content
|
||||
let fg = 7 |
||||
let bg = 0 |
||||
let attrs = 0 |
||||
let cell = 0 // cell index
|
||||
let lastChar = ' ' |
||||
let frameLength = frameWidth * frameHeight |
||||
|
||||
const MASK_LINE_ATTR = ATTR_UNDERLINE | ATTR_OVERLINE | ATTR_STRIKE |
||||
const MASK_BLINK = ATTR_BLINK |
||||
|
||||
const cells = [] |
||||
|
||||
let pushCell = () => { |
||||
let hasFG = attrs & ATTR_FG |
||||
let hasBG = attrs & ATTR_BG |
||||
let cellFG = fg |
||||
let cellBG = bg |
||||
let cellAttrs = attrs |
||||
|
||||
// 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 |
||||
|
||||
// Remove blink attribute if it wouldn't have any effect
|
||||
if ((cellAttrs & MASK_BLINK) && |
||||
((lastChar === ' ' && ((cellAttrs & MASK_LINE_ATTR) === 0)) || // no line styles
|
||||
(fg === bg && hasFG && hasBG) // invisible text
|
||||
) |
||||
) { |
||||
cellAttrs ^= MASK_BLINK |
||||
} |
||||
|
||||
// 8 dark system colors turn bright when bold
|
||||
if ((cellAttrs & ATTR_BOLD) && !(cellAttrs & ATTR_FAINT) && hasFG && cellFG < 8) { |
||||
cellFG += 8 |
||||
} |
||||
|
||||
cells.push([lastChar, cellFG, cellBG, cellAttrs]) |
||||
} |
||||
|
||||
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++ |
||||
} |
||||
} |
||||
|
||||
updates.push({ |
||||
topic: 'content', |
||||
frameX, |
||||
frameY, |
||||
frameWidth, |
||||
frameHeight, |
||||
cells |
||||
}) |
||||
} |
||||
|
||||
if (topics & 0x3B && !this.contentLoaded) { |
||||
updates.push({ topic: 'full-load-complete' }) |
||||
this.contentLoaded = true |
||||
} |
||||
} |
||||
|
||||
return updates |
||||
} |
||||
|
||||
/** |
||||
* Parses a message from the server |
||||
* @param {string} message - the message |
||||
*/ |
||||
parse (message) { |
||||
const content = message.substr(1) |
||||
const updates = [] |
||||
|
||||
// This is a good place for debugging the message
|
||||
// console.log(message)
|
||||
|
||||
switch (message[0]) { |
||||
case 'U': |
||||
updates.push(...this.parseUpdate(content)) |
||||
break |
||||
|
||||
case 'G': |
||||
return [{ |
||||
topic: 'notification', |
||||
content |
||||
}] |
||||
|
||||
default: |
||||
console.warn(`Bad data message type; ignoring.\n${JSON.stringify(message)}`) |
||||
} |
||||
|
||||
return updates |
||||
} |
||||
} |
@ -0,0 +1,936 @@ |
||||
const EventEmitter = require('events') |
||||
const { |
||||
themes, |
||||
getColor |
||||
} = require('./themes') |
||||
|
||||
const { |
||||
ATTR_FG, |
||||
ATTR_BG, |
||||
ATTR_BOLD, |
||||
ATTR_UNDERLINE, |
||||
ATTR_INVERSE, |
||||
ATTR_BLINK, |
||||
ATTR_ITALIC, |
||||
ATTR_STRIKE, |
||||
ATTR_OVERLINE, |
||||
ATTR_FAINT, |
||||
ATTR_FRAKTUR |
||||
} = require('./screen_attr_bits') |
||||
|
||||
// Some non-bold Fraktur symbols are outside the contiguous block
|
||||
const frakturExceptions = { |
||||
'C': '\u212d', |
||||
'H': '\u210c', |
||||
'I': '\u2111', |
||||
'R': '\u211c', |
||||
'Z': '\u2128' |
||||
} |
||||
|
||||
/** |
||||
* A terminal screen renderer, using canvas 2D |
||||
*/ |
||||
module.exports = class CanvasRenderer extends EventEmitter { |
||||
constructor (canvas) { |
||||
super() |
||||
|
||||
this.canvas = canvas |
||||
this.ctx = this.canvas.getContext('2d') |
||||
|
||||
this._palette = null // colors 0-15
|
||||
this.defaultBG = 0 |
||||
this.defaultFG = 7 |
||||
|
||||
this.debug = false |
||||
this._debug = null |
||||
|
||||
this.graphics = 0 |
||||
|
||||
this.statusFont = "-apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif" |
||||
|
||||
// screen data, considered immutable
|
||||
this.width = 0 |
||||
this.height = 0 |
||||
this.padding = 0 |
||||
this.charSize = { width: 0, height: 0 } |
||||
this.cellSize = { width: 0, height: 0 } |
||||
this.fonts = ['', '', '', ''] // normal, bold, italic, bold-italic
|
||||
this.screen = [] |
||||
this.screenFG = [] |
||||
this.screenBG = [] |
||||
this.screenAttrs = [] |
||||
this.screenSelection = [] |
||||
this.screenLines = [] |
||||
this.cursor = {} |
||||
this.reverseVideo = false |
||||
this.hasBlinkingCells = false |
||||
this.statusScreen = null |
||||
|
||||
this.resetDrawn() |
||||
|
||||
this.blinkStyleOn = false |
||||
this.blinkInterval = null |
||||
this.cursorBlinkOn = false |
||||
this.cursorBlinkInterval = null |
||||
|
||||
// start blink timers
|
||||
this.resetBlink() |
||||
this.resetCursorBlink() |
||||
} |
||||
|
||||
render (reason, data) { |
||||
if ('hasBlinkingCells' in data && data.hasBlinkingCells !== this.hasBlinkingCells) { |
||||
if (data.hasBlinkingCells) this.resetBlink() |
||||
else clearInterval(this.blinkInterval) |
||||
} |
||||
|
||||
Object.assign(this, data) |
||||
this.scheduleDraw(reason) |
||||
} |
||||
|
||||
resetDrawn () { |
||||
// used to determine if a cell should be redrawn; storing the current state
|
||||
// as it is on screen
|
||||
if (this.debug) console.log('Resetting drawn screen') |
||||
|
||||
this.drawnScreen = [] |
||||
this.drawnScreenFG = [] |
||||
this.drawnScreenBG = [] |
||||
this.drawnScreenAttrs = [] |
||||
this.drawnScreenLines = [] |
||||
this.drawnCursor = [-1, -1, '', false] |
||||
} |
||||
|
||||
/** |
||||
* The color palette. Should define 16 colors in an array. |
||||
* @type {string[]} |
||||
*/ |
||||
get palette () { |
||||
return this._palette || themes[0] |
||||
} |
||||
|
||||
/** @type {string[]} */ |
||||
set palette (palette) { |
||||
if (this._palette !== palette) { |
||||
this._palette = palette |
||||
this.resetDrawn() |
||||
this.emit('palette-update', palette) |
||||
this.scheduleDraw('palette') |
||||
} |
||||
} |
||||
|
||||
getCharWidthFor (font) { |
||||
this.ctx.font = font |
||||
return Math.floor(this.ctx.measureText(' ').width) |
||||
} |
||||
|
||||
loadTheme (i) { |
||||
if (i in themes) this.palette = themes[i] |
||||
} |
||||
|
||||
setDefaultColors (fg, bg) { |
||||
if (fg !== this.defaultFG || bg !== this.defaultBG) { |
||||
this.resetDrawn() |
||||
this.defaultFG = fg |
||||
this.defaultBG = bg |
||||
this.scheduleDraw('default-colors') |
||||
|
||||
// full bg with default color (goes behind the image)
|
||||
this.canvas.style.backgroundColor = this.getColor(bg) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Schedule a draw in the next millisecond |
||||
* @param {string} why - the reason why the draw occured (for debugging) |
||||
* @param {number} [aggregateTime] - time to wait for more scheduleDraw calls |
||||
* to occur. 1 ms by default. |
||||
*/ |
||||
scheduleDraw (why, aggregateTime = 1) { |
||||
clearTimeout(this._scheduledDraw) |
||||
this._scheduledDraw = setTimeout(() => this.draw(why), aggregateTime) |
||||
} |
||||
|
||||
/** |
||||
* Returns the specified color. If `i` is in the palette, it will return the |
||||
* palette color. If `i` is between 16 and 255, it will return the 256color |
||||
* value. If `i` is larger than 255, it will return an RGB color value. If `i` |
||||
* is -1 (foreground) or -2 (background), it will return the selection colors. |
||||
* @param {number} i - the color |
||||
* @returns {string} the CSS color |
||||
*/ |
||||
getColor (i) { |
||||
return getColor(i, this.palette) |
||||
} |
||||
|
||||
/** |
||||
* Resets the cursor blink to on and restarts the timer |
||||
*/ |
||||
resetCursorBlink () { |
||||
this.cursorBlinkOn = true |
||||
clearInterval(this.cursorBlinkInterval) |
||||
this.cursorBlinkInterval = setInterval(() => { |
||||
this.cursorBlinkOn = this.cursor.blinking ? !this.cursorBlinkOn : true |
||||
if (this.cursor.blinking) this.scheduleDraw('cursor-blink') |
||||
}, 500) |
||||
} |
||||
|
||||
/** |
||||
* Resets the blink style to on and restarts the timer |
||||
*/ |
||||
resetBlink () { |
||||
this.blinkStyleOn = true |
||||
clearInterval(this.blinkInterval) |
||||
let intervals = 0 |
||||
this.blinkInterval = setInterval(() => { |
||||
if (this.blinkingCellCount <= 0) return |
||||
|
||||
intervals++ |
||||
if (intervals >= 4 && this.blinkStyleOn) { |
||||
this.blinkStyleOn = false |
||||
intervals = 0 |
||||
this.scheduleDraw('blink-style') |
||||
} else if (intervals >= 1 && !this.blinkStyleOn) { |
||||
this.blinkStyleOn = true |
||||
intervals = 0 |
||||
this.scheduleDraw('blink-style') |
||||
} |
||||
}, 200) |
||||
} |
||||
|
||||
/** |
||||
* Draws a cell's background with the given parameters. |
||||
* @param {Object} options |
||||
* @param {number} options.x - x in cells |
||||
* @param {number} options.y - y in cells |
||||
* @param {number} options.cellWidth - cell width in pixels |
||||
* @param {number} options.cellHeight - cell height in pixels |
||||
* @param {number} options.bg - the background color |
||||
* @param {number} options.isDefaultBG - if true, will draw image background if available |
||||
*/ |
||||
drawBackground ({ x, y, cellWidth, cellHeight, bg, isDefaultBG }) { |
||||
const { ctx, width, height, padding } = this |
||||
|
||||
// is a double-width/double-height line
|
||||
if (this.screenLines[y] & 0b001) cellWidth *= 2 |
||||
|
||||
ctx.fillStyle = this.getColor(bg) |
||||
let screenX = x * cellWidth + padding |
||||
let screenY = y * cellHeight + padding |
||||
let isBorderCell = x === 0 || y === 0 || x === width - 1 || y === height - 1 |
||||
|
||||
let fillX, fillY, fillWidth, fillHeight |
||||
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 |
||||
|
||||
fillX = left |
||||
fillY = top |
||||
fillWidth = right - left |
||||
fillHeight = bottom - top |
||||
} else { |
||||
fillX = screenX |
||||
fillY = screenY |
||||
fillWidth = cellWidth |
||||
fillHeight = cellHeight |
||||
} |
||||
|
||||
ctx.clearRect(fillX, fillY, fillWidth, fillHeight) |
||||
|
||||
if (!isDefaultBG || bg < 0 || !this.backgroundImage) { |
||||
ctx.fillRect(fillX, fillY, fillWidth, fillHeight) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Draws a cell's character with the given parameters. Won't do anything if |
||||
* text is an empty string. |
||||
* @param {Object} options |
||||
* @param {number} options.x - x in cells |
||||
* @param {number} options.y - y in cells |
||||
* @param {Object} options.charSize - the character size, an object with |
||||
* `width` and `height` in pixels |
||||
* @param {number} options.cellWidth - cell width in pixels |
||||
* @param {number} options.cellHeight - cell height in pixels |
||||
* @param {string} options.text - the cell content |
||||
* @param {number} options.fg - the foreground color |
||||
* @param {number} options.attrs - the cell's attributes |
||||
*/ |
||||
drawCharacter ({ x, y, charSize, cellWidth, cellHeight, text, fg, attrs }) { |
||||
if (!text) return |
||||
|
||||
const { ctx, padding } = this |
||||
|
||||
let underline = false |
||||
let strike = false |
||||
let overline = false |
||||
if (attrs & ATTR_FAINT) ctx.globalAlpha = 0.5 |
||||
if (attrs & ATTR_UNDERLINE) underline = true |
||||
if (attrs & ATTR_FRAKTUR) text = CanvasRenderer.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 |
||||
|
||||
const dblWidth = this.screenLines[y] & 0b001 |
||||
const dblHeightTop = this.screenLines[y] & 0b010 |
||||
const dblHeightBot = this.screenLines[y] & 0b100 |
||||
|
||||
if (this.screenLines[y]) { |
||||
// is a double-width/double-height line
|
||||
if (dblWidth) cellWidth *= 2 |
||||
|
||||
ctx.save() |
||||
ctx.translate(padding, screenY + 0.5 * cellHeight) |
||||
if (dblWidth) ctx.scale(2, 1) |
||||
if (dblHeightTop) { |
||||
// top half
|
||||
ctx.scale(1, 2) |
||||
ctx.translate(0, cellHeight / 4) |
||||
} else if (dblHeightBot) { |
||||
// bottom half
|
||||
ctx.scale(1, 2) |
||||
ctx.translate(0, -cellHeight / 4) |
||||
} |
||||
ctx.translate(-padding, -screenY - 0.5 * cellHeight) |
||||
if (dblWidth) ctx.translate(-cellWidth / 4, 0) |
||||
|
||||
if (dblHeightBot || dblHeightTop) { |
||||
// characters overflow -- needs clipping
|
||||
// TODO: clipping is really expensive
|
||||
ctx.beginPath() |
||||
if (dblHeightTop) ctx.rect(screenX, screenY, cellWidth, cellHeight / 2) |
||||
else ctx.rect(screenX, screenY + cellHeight / 2, cellWidth, cellHeight / 2) |
||||
ctx.clip() |
||||
} |
||||
} |
||||
|
||||
let codePoint = text.codePointAt(0) |
||||
if (codePoint >= 0x2580 && codePoint <= 0x259F) { |
||||
// block elements
|
||||
ctx.beginPath() |
||||
const left = screenX |
||||
const top = screenY |
||||
const cw = cellWidth |
||||
const ch = cellHeight |
||||
const c2w = cellWidth / 2 |
||||
const c2h = cellHeight / 2 |
||||
|
||||
// http://www.fileformat.info/info/unicode/block/block_elements/utf8test.htm
|
||||
// 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F
|
||||
// 0x2580 ▀ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▉ ▊ ▋ ▌ ▍ ▎ ▏
|
||||
// 0x2590 ▐ ░ ▒ ▓ ▔ ▕ ▖ ▗ ▘ ▙ ▚ ▛ ▜ ▝ ▞ ▟
|
||||
|
||||
if (codePoint === 0x2580) { |
||||
// upper half block >▀<
|
||||
ctx.rect(left, top, cw, c2h) |
||||
} else if (codePoint <= 0x2588) { |
||||
// lower n eighth block (increasing) >▁< to >█<
|
||||
let offset = (1 - (codePoint - 0x2580) / 8) * ch |
||||
ctx.rect(left, top + offset, cw, ch - offset) |
||||
} else if (codePoint <= 0x258F) { |
||||
// left n eighth block (decreasing) >▉< to >▏<
|
||||
let offset = (codePoint - 0x2588) / 8 * cw |
||||
ctx.rect(left, top, cw - offset, ch) |
||||
} else if (codePoint === 0x2590) { |
||||
// right half block >▐<
|
||||
ctx.rect(left + c2w, top, c2w, ch) |
||||
} else if (codePoint <= 0x2593) { |
||||
// shading >░< >▒< >▓<
|
||||
|
||||
// dot spacing by dividing cell size by a constant. This could be
|
||||
// reworked to always return a whole number, but that would require
|
||||
// prime factorization, and doing that without a loop would let you
|
||||
// take over the world, which is not within the scope of this project.
|
||||
let dotSpacingX, dotSpacingY, dotSize |
||||
if (codePoint === 0x2591) { |
||||
dotSpacingX = cw / 4 |
||||
dotSpacingY = ch / 10 |
||||
dotSize = 1 |
||||
} else if (codePoint === 0x2592) { |
||||
dotSpacingX = cw / 6 |
||||
dotSpacingY = cw / 10 |
||||
dotSize = 1 |
||||
} else if (codePoint === 0x2593) { |
||||
dotSpacingX = cw / 4 |
||||
dotSpacingY = cw / 7 |
||||
dotSize = 2 |
||||
} |
||||
|
||||
let alignRight = false |
||||
for (let dy = 0; dy < ch; dy += dotSpacingY) { |
||||
for (let dx = 0; dx < cw; dx += dotSpacingX) { |
||||
// prevent overflow
|
||||
let dotSizeY = Math.min(dotSize, ch - dy) |
||||
ctx.rect(left + (alignRight ? cw - dx - dotSize : dx), top + dy, dotSize, dotSizeY) |
||||
} |
||||
alignRight = !alignRight |
||||
} |
||||
} else if (codePoint === 0x2594) { |
||||
// upper one eighth block >▔<
|
||||
ctx.rect(left, top, cw, ch / 8) |
||||
} else if (codePoint === 0x2595) { |
||||
// right one eighth block >▕<
|
||||
ctx.rect(left + (7 / 8) * cw, top, cw / 8, ch) |
||||
} else if (codePoint === 0x2596) { |
||||
// left bottom quadrant >▖<
|
||||
ctx.rect(left, top + c2h, c2w, c2h) |
||||
} else if (codePoint === 0x2597) { |
||||
// right bottom quadrant >▗<
|
||||
ctx.rect(left + c2w, top + c2h, c2w, c2h) |
||||
} else if (codePoint === 0x2598) { |
||||
// left top quadrant >▘<
|
||||
ctx.rect(left, top, c2w, c2h) |
||||
} else if (codePoint === 0x2599) { |
||||
// left chair >▙<
|
||||
ctx.rect(left, top, c2w, ch) |
||||
ctx.rect(left + c2w, top + c2h, c2w, c2h) |
||||
} else if (codePoint === 0x259A) { |
||||
// quadrants lt rb >▚<
|
||||
ctx.rect(left, top, c2w, c2h) |
||||
ctx.rect(left + c2w, top + c2h, c2w, c2h) |
||||
} else if (codePoint === 0x259B) { |
||||
// left chair upside down >▛<
|
||||
ctx.rect(left, top, c2w, ch) |
||||
ctx.rect(left + c2w, top, c2w, c2h) |
||||
} else if (codePoint === 0x259C) { |
||||
// right chair upside down >▜<
|
||||
ctx.rect(left, top, cw, c2h) |
||||
ctx.rect(left + c2w, top + c2h, c2w, c2h) |
||||
} else if (codePoint === 0x259D) { |
||||
// right top quadrant >▝<
|
||||
ctx.rect(left + c2w, top, c2w, c2h) |
||||
} else if (codePoint === 0x259E) { |
||||
// quadrants lb rt >▞<
|
||||
ctx.rect(left, top + c2h, c2w, c2h) |
||||
ctx.rect(left + c2w, top, c2w, c2h) |
||||
} else if (codePoint === 0x259F) { |
||||
// right chair upside down >▟<
|
||||
ctx.rect(left, top + c2h, c2w, c2h) |
||||
ctx.rect(left + c2w, top, c2w, ch) |
||||
} |
||||
|
||||
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, screenX + 0.5 * cellWidth, screenY + 0.5 * cellHeight) |
||||
} |
||||
|
||||
// -- line drawing - a reference for a possible future rect/line implementation ---
|
||||
// http://www.fileformat.info/info/unicode/block/box_drawing/utf8test.htm
|
||||
// 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F
|
||||
// 0x2500 ─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏
|
||||
// 0x2510 ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟
|
||||
// 0x2520 ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯
|
||||
// 0x2530 ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿
|
||||
// 0x2540 ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏
|
||||
// 0x2550 ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟
|
||||
// 0x2560 ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯
|
||||
// 0x2570 ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿
|
||||
|
||||
if (underline || strike || overline) { |
||||
ctx.strokeStyle = this.getColor(fg) |
||||
ctx.lineWidth = 1 |
||||
ctx.lineCap = 'round' |
||||
ctx.beginPath() |
||||
|
||||
if (underline) { |
||||
let lineY = Math.round(screenY + charSize.height) + 0.5 |
||||
ctx.moveTo(screenX, lineY) |
||||
ctx.lineTo(screenX + cellWidth, lineY) |
||||
} |
||||
|
||||
if (strike) { |
||||
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(screenY) + 0.5 |
||||
ctx.moveTo(screenX, lineY) |
||||
ctx.lineTo(screenX + cellWidth, lineY) |
||||
} |
||||
|
||||
ctx.stroke() |
||||
} |
||||
|
||||
if (this.screenLines[y]) ctx.restore() |
||||
|
||||
ctx.globalAlpha = 1 |
||||
} |
||||
|
||||
/** |
||||
* Returns all adjacent cell indices given a radius. |
||||
* @param {number} cell - the center cell index |
||||
* @param {number} [radius] - the radius. 1 by default |
||||
* @returns {number[]} an array of cell indices |
||||
*/ |
||||
getAdjacentCells (cell, radius = 1) { |
||||
const { width, height } = this |
||||
const screenLength = width * height |
||||
|
||||
let cells = [] |
||||
|
||||
for (let x = -radius; x <= radius; x++) { |
||||
for (let y = -radius; y <= radius; y++) { |
||||
if (x === 0 && y === 0) continue |
||||
cells.push(cell + x + y * width) |
||||
} |
||||
} |
||||
|
||||
return cells.filter(cell => cell >= 0 && cell < screenLength) |
||||
} |
||||
|
||||
/** |
||||
* Updates the screen. |
||||
* @param {string} why - the draw reason (for debugging) |
||||
*/ |
||||
draw (why) { |
||||
const ctx = this.ctx |
||||
const { |
||||
width, |
||||
height, |
||||
devicePixelRatio, |
||||
statusScreen |
||||
} = this |
||||
|
||||
if (statusScreen) { |
||||
// draw status screen instead
|
||||
this.drawStatus(statusScreen) |
||||
this.startDrawLoop() |
||||
return |
||||
} else this.stopDrawLoop() |
||||
|
||||
const charSize = this.charSize |
||||
const { width: cellWidth, height: cellHeight } = this.cellSize |
||||
const screenLength = width * height |
||||
|
||||
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0) |
||||
|
||||
if (this.debug && this._debug) this._debug.drawStart(why) |
||||
|
||||
ctx.font = this.fonts[0] |
||||
ctx.textAlign = 'center' |
||||
ctx.textBaseline = 'middle' |
||||
|
||||
// bits in the attr value that affect the font
|
||||
const FONT_MASK = ATTR_BOLD | ATTR_ITALIC |
||||
|
||||
// Map of (attrs & FONT_MASK) -> Array of cell indices
|
||||
let fontGroups = new Map() |
||||
|
||||
// Map of (cell index) -> boolean, whether or not a cell has updated
|
||||
let updateMap = new Map() |
||||
|
||||
for (let cell = 0; cell < screenLength; cell++) { |
||||
let x = cell % width |
||||
let y = Math.floor(cell / width) |
||||
let isCursor = this.cursorBlinkOn && |
||||
this.cursor.x === x && |
||||
this.cursor.y === y && |
||||
this.cursor.visible |
||||
|
||||
let wasCursor = x === this.drawnCursor[0] && y === this.drawnCursor[1] |
||||
|
||||
let text = this.screen[cell] |
||||
let fg = this.screenFG[cell] | 0 |
||||
let bg = this.screenBG[cell] | 0 |
||||
let attrs = this.screenAttrs[cell] | 0 |
||||
let inSelection = this.screenSelection[cell] |
||||
|
||||
let isDefaultBG = false |
||||
|
||||
if (!(attrs & ATTR_FG)) fg = this.defaultFG |
||||
if (!(attrs & ATTR_BG)) { |
||||
bg = this.defaultBG |
||||
isDefaultBG = true |
||||
} |
||||
|
||||
if (attrs & ATTR_INVERSE) [fg, bg] = [bg, fg] // swap - reversed character colors
|
||||
if (this.reverseVideo) [fg, bg] = [bg, fg] // swap - reversed all screen
|
||||
|
||||
if (attrs & ATTR_BLINK && !this.blinkStyleOn) { |
||||
// blinking is enabled and blink style is off
|
||||
// set text to nothing so drawCharacter only draws decoration
|
||||
text = ' ' |
||||
} |
||||
|
||||
if (inSelection) { |
||||
fg = -1 |
||||
bg = -2 |
||||
} |
||||
|
||||
let didUpdate = text !== this.drawnScreen[cell] || // text updated
|
||||
fg !== this.drawnScreenFG[cell] || // foreground updated, and this cell has text
|
||||
bg !== this.drawnScreenBG[cell] || // background updated
|
||||
attrs !== this.drawnScreenAttrs[cell] || // attributes updated
|
||||
this.screenLines[y] !== this.drawnScreenLines[y] || // line updated
|
||||
// TODO: fix artifacts or keep this hack:
|
||||
isCursor || wasCursor || // cursor blink/position updated
|
||||
(isCursor && this.cursor.style !== this.drawnCursor[2]) || // cursor style updated
|
||||
(isCursor && this.cursor.hanging !== this.drawnCursor[3]) // cursor hanging updated
|
||||
|
||||
let font = attrs & FONT_MASK |
||||
if (!fontGroups.has(font)) fontGroups.set(font, []) |
||||
|
||||
fontGroups.get(font).push({ cell, x, y, text, fg, bg, attrs, isCursor, inSelection, isDefaultBG }) |
||||
updateMap.set(cell, didUpdate) |
||||
} |
||||
|
||||
// set drawn screen lines
|
||||
this.drawnScreenLines = this.screenLines.slice() |
||||
|
||||
let debugFilledUpdates = [] |
||||
|
||||
if (this.graphics >= 1) { |
||||
// fancy graphics gets really slow when there's a lot of masks
|
||||
// so here's an algorithm that fills in holes in the update map
|
||||
|
||||
for (let cell of updateMap.keys()) { |
||||
if (updateMap.get(cell)) continue |
||||
let previous = updateMap.get(cell - 1) || false |
||||
let next = updateMap.get(cell + 1) || false |
||||
|
||||
if (previous && next) { |
||||
// set cell to true of horizontally adjacent updated
|
||||
updateMap.set(cell, true) |
||||
if (this.debug && this._debug) debugFilledUpdates.push(cell) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Map of (cell index) -> boolean, whether or not a cell should be redrawn
|
||||
const redrawMap = new Map() |
||||
const maskedCells = new Map() |
||||
|
||||
let isTextWide = text => |
||||
text !== ' ' && ctx.measureText(text).width >= (cellWidth + 0.05) |
||||
|
||||
// decide for each cell if it should be redrawn
|
||||
for (let cell of updateMap.keys()) { |
||||
let shouldUpdate = updateMap.get(cell) || redrawMap.get(cell) || false |
||||
|
||||
// TODO: fonts (necessary?)
|
||||
let text = this.screen[cell] |
||||
let isWideCell = isTextWide(text) |
||||
let checkRadius = isWideCell ? 2 : 1 |
||||
|
||||
if (!shouldUpdate) { |
||||
// check adjacent cells
|
||||
let adjacentDidUpdate = false |
||||
|
||||
for (let adjacentCell of this.getAdjacentCells(cell, checkRadius)) { |
||||
// update this cell if:
|
||||
// - the adjacent cell updated (For now, this'll always be true because characters can be slightly larger than they say they are)
|
||||
// - the adjacent cell updated and this cell or the adjacent cell is wide
|
||||
// - this or the adjacent cell is not double-sized
|
||||
if (updateMap.get(adjacentCell) && |
||||
(this.graphics < 2 || isWideCell || isTextWide(this.screen[adjacentCell])) && |
||||
(!this.screenLines[Math.floor(cell / this.width)] && !this.screenLines[Math.floor(adjacentCell / this.width)])) { |
||||
adjacentDidUpdate = true |
||||
|
||||
if (this.getAdjacentCells(cell, 1).includes(adjacentCell)) { |
||||
// this is within a radius of 1, therefore this cell should be included in the mask as well
|
||||
maskedCells.set(cell, true) |
||||
} |
||||
break |
||||
} |
||||
} |
||||
|
||||
if (adjacentDidUpdate) shouldUpdate = true |
||||
} |
||||
|
||||
if (updateMap.get(cell)) { |
||||
// this was updated, it should definitely be included in the mask
|
||||
maskedCells.set(cell, true) |
||||
} |
||||
|
||||
redrawMap.set(cell, shouldUpdate) |
||||
} |
||||
|
||||
// mask to masked regions only
|
||||
if (this.graphics >= 1) { |
||||
// TODO: include padding in border cells
|
||||
const padding = this.padding |
||||
|
||||
let regions = [] |
||||
|
||||
for (let y = 0; y < height; y++) { |
||||
let regionStart = null |
||||
for (let x = 0; x < width; x++) { |
||||
let cell = y * width + x |
||||
let masked = maskedCells.get(cell) |
||||
if (masked && regionStart === null) regionStart = x |
||||
if (!masked && regionStart !== null) { |
||||
regions.push([regionStart, y, x, y + 1]) |
||||
regionStart = null |
||||
} |
||||
} |
||||
if (regionStart !== null) { |
||||
regions.push([regionStart, y, width, y + 1]) |
||||
} |
||||
} |
||||
|
||||
// join regions if possible (O(n^2-1), sorry)
|
||||
let i = 0 |
||||
while (i < regions.length) { |
||||
let region = regions[i] |
||||
let j = 0 |
||||
while (j < regions.length) { |
||||
let other = regions[j] |
||||
if (other === region) { |
||||
j++ |
||||
continue |
||||
} |
||||
if (other[0] === region[0] && other[2] === region[2] && other[3] === region[1]) { |
||||
region[1] = other[1] |
||||
regions.splice(j, 1) |
||||
if (i > j) i-- |
||||
j-- |
||||
} |
||||
j++ |
||||
} |
||||
i++ |
||||
} |
||||
|
||||
ctx.save() |
||||
ctx.beginPath() |
||||
for (let region of regions) { |
||||
let [regionStart, y, endX, endY] = region |
||||
let rectX = padding + regionStart * cellWidth |
||||
let rectY = padding + y * cellHeight |
||||
let rectWidth = (endX - regionStart) * cellWidth |
||||
let rectHeight = (endY - y) * cellHeight |
||||
|
||||
// compensate for padding
|
||||
if (regionStart === 0) { |
||||
rectX -= padding |
||||
rectWidth += padding |
||||
} |
||||
if (y === 0) { |
||||
rectY -= padding |
||||
rectHeight += padding |
||||
} |
||||
if (endX === width - 1) rectWidth += padding |
||||
if (y === height - 1) rectHeight += padding |
||||
|
||||
ctx.rect(rectX, rectY, rectWidth, rectHeight) |
||||
} |
||||
ctx.clip() |
||||
} |
||||
|
||||
// pass 1: backgrounds
|
||||
for (let font of fontGroups.keys()) { |
||||
for (let data of fontGroups.get(font)) { |
||||
let { cell, x, y, text, bg, isDefaultBG } = data |
||||
|
||||
if (redrawMap.get(cell)) { |
||||
this.drawBackground({ x, y, cellWidth, cellHeight, bg, isDefaultBG }) |
||||
|
||||
if (this.debug) { |
||||
// set cell flags
|
||||
let flags = (+redrawMap.get(cell)) |
||||
flags |= (+updateMap.get(cell)) << 1 |
||||
flags |= (+maskedCells.get(cell)) << 2 |
||||
flags |= (+isTextWide(text)) << 3 |
||||
flags |= (+debugFilledUpdates.includes(cell)) << 4 |
||||
this._debug.setCell(cell, flags) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// reset drawn cursor
|
||||
this.drawnCursor = [-1, -1, '', false] |
||||
|
||||
// pass 2: characters
|
||||
for (let font of fontGroups.keys()) { |
||||
// set font once because in Firefox, this is a really slow action for some
|
||||
// reason
|
||||
let fontIndex = 0 |
||||
if (font & ATTR_BOLD) fontIndex |= 1 |
||||
if (font & ATTR_ITALIC) fontIndex |= 2 |
||||
ctx.font = this.fonts[fontIndex] |
||||
|
||||
for (let data of fontGroups.get(font)) { |
||||
let { cell, x, y, text, fg, bg, attrs, isCursor, inSelection } = data |
||||
|
||||
if (redrawMap.get(cell)) { |
||||
this.drawCharacter({ |
||||
x, y, charSize, cellWidth, cellHeight, text, fg, attrs |
||||
}) |
||||
|
||||
this.drawnScreen[cell] = text |
||||
this.drawnScreenFG[cell] = fg |
||||
this.drawnScreenBG[cell] = bg |
||||
this.drawnScreenAttrs[cell] = attrs |
||||
|
||||
if (isCursor) this.drawnCursor = [x, y, this.cursor.style, this.cursor.hanging] |
||||
|
||||
// draw cursor
|
||||
if (isCursor && !inSelection) { |
||||
ctx.save() |
||||
ctx.beginPath() |
||||
|
||||
let cursorX = x |
||||
let cursorY = y |
||||
let cursorWidth = cellWidth // JS doesn't allow same-name assignment
|
||||
|
||||
if (this.cursor.hanging) { |
||||
// draw hanging cursor in the margin
|
||||
cursorX += 1 |
||||
} |
||||
|
||||
// double-width lines
|
||||
if (this.screenLines[cursorY] & 0b001) cursorWidth *= 2 |
||||
|
||||
let screenX = cursorX * cursorWidth + this.padding |
||||
let screenY = cursorY * cellHeight + this.padding |
||||
|
||||
if (this.cursor.style === 'block') { |
||||
// block
|
||||
ctx.rect(screenX, screenY, cursorWidth, cellHeight) |
||||
} else if (this.cursor.style === 'bar') { |
||||
// vertical bar
|
||||
let barWidth = 2 |
||||
ctx.rect(screenX, screenY, barWidth, cellHeight) |
||||
} else if (this.cursor.style === 'line') { |
||||
// underline
|
||||
let lineHeight = 2 |
||||
ctx.rect(screenX, screenY + charSize.height, cursorWidth, lineHeight) |
||||
} |
||||
ctx.clip() |
||||
|
||||
// swap foreground/background
|
||||
;[fg, bg] = [bg, fg] |
||||
|
||||
// HACK: ensure cursor is visible
|
||||
if (fg === bg) bg = fg === 0 ? 7 : 0 |
||||
|
||||
this.drawBackground({ x: cursorX, y: cursorY, cellWidth, cellHeight, bg }) |
||||
this.drawCharacter({ |
||||
x: cursorX, y: cursorY, charSize, cellWidth, cellHeight, text, fg, attrs |
||||
}) |
||||
ctx.restore() |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (this.graphics >= 1) ctx.restore() |
||||
|
||||
if (this.debug && this._debug) this._debug.drawEnd() |
||||
|
||||
this.emit('draw', why) |
||||
} |
||||
|
||||
drawStatus (statusScreen) { |
||||
const { ctx, width, height, devicePixelRatio } = this |
||||
|
||||
// reset drawnScreen to force redraw when statusScreen is disabled
|
||||
this.drawnScreen = [] |
||||
|
||||
const cellSize = this.cellSize |
||||
const screenWidth = width * cellSize.width + 2 * this.padding |
||||
const screenHeight = height * cellSize.height + 2 * this.padding |
||||
|
||||
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0) |
||||
ctx.fillStyle = this.getColor(this.defaultBG) |
||||
ctx.fillRect(0, 0, screenWidth, screenHeight) |
||||
|
||||
ctx.font = `24px ${this.statusFont}` |
||||
ctx.fillStyle = this.getColor(this.defaultFG) |
||||
ctx.textAlign = 'center' |
||||
ctx.textBaseline = 'middle' |
||||
ctx.fillText(statusScreen.title || '', screenWidth / 2, screenHeight / 2 - 50) |
||||
|
||||
if (statusScreen.loading) { |
||||
// show loading spinner
|
||||
ctx.save() |
||||
ctx.translate(screenWidth / 2, screenHeight / 2 + 20) |
||||
|
||||
ctx.strokeStyle = this.getColor(this.defaultFG) |
||||
ctx.lineWidth = 5 |
||||
ctx.lineCap = 'round' |
||||
|
||||
let t = Date.now() / 1000 |
||||
|
||||
for (let i = 0; i < 12; i++) { |
||||
ctx.rotate(Math.PI / 6) |
||||
let offset = ((t * 12) - i) % 12 |
||||
ctx.globalAlpha = Math.max(0.2, 1 - offset / 3) |
||||
ctx.beginPath() |
||||
ctx.moveTo(0, 15) |
||||
ctx.lineTo(0, 30) |
||||
ctx.stroke() |
||||
} |
||||
|
||||
ctx.restore() |
||||
} |
||||
} |
||||
|
||||
startDrawLoop () { |
||||
if (this._drawTimerThread) return |
||||
let threadID = Math.random().toString(36) |
||||
this._drawTimerThread = threadID |
||||
this.drawTimerLoop(threadID) |
||||
} |
||||
|
||||
stopDrawLoop () { |
||||
this._drawTimerThread = null |
||||
} |
||||
|
||||
drawTimerLoop (threadID) { |
||||
if (!threadID || threadID !== this._drawTimerThread) return |
||||
window.requestAnimationFrame(() => this.drawTimerLoop(threadID)) |
||||
this.draw('draw-loop') |
||||
} |
||||
|
||||
/** |
||||
* Converts an alphabetic character to its fraktur variant. |
||||
* @param {string} character - the character |
||||
* @returns {string} the converted character |
||||
*/ |
||||
static alphaToFraktur (character) { |
||||
if (character >= 'a' && character <= 'z') { |
||||
character = String.fromCodePoint(0x1d51e - 0x61 + character.charCodeAt(0)) |
||||
} else if (character >= 'A' && character <= 'Z') { |
||||
character = frakturExceptions[character] || String.fromCodePoint(0x1d504 - 0x41 + character.charCodeAt(0)) |
||||
} |
||||
return character |
||||
} |
||||
} |
@ -0,0 +1,167 @@ |
||||
const { qs } = require('../utils') |
||||
|
||||
module.exports = function (screen, input) { |
||||
const keyInput = qs('#softkb-input') |
||||
if (!keyInput) return // abort, we're not on the terminal page
|
||||
|
||||
const shortcutBar = document.createElement('div') |
||||
shortcutBar.id = 'keyboard-shortcut-bar' |
||||
if (navigator.userAgent.match(/iPad|iPhone|iPod/)) { |
||||
qs('#screen').appendChild(shortcutBar) |
||||
} |
||||
|
||||
let keyboardOpen = false |
||||
|
||||
// moves the input to where the cursor is on the canvas.
|
||||
// this is because most browsers will always scroll to wherever the focused
|
||||
// input is
|
||||
let updateInputPosition = function () { |
||||
if (!keyboardOpen) return |
||||
|
||||
let [x, y] = screen.layout.gridToScreen(screen.cursor.x, screen.cursor.y, true) |
||||
keyInput.style.transform = `translate(${x}px, ${y}px)` |
||||
} |
||||
|
||||
keyInput.addEventListener('focus', () => { |
||||
keyboardOpen = true |
||||
updateInputPosition() |
||||
shortcutBar.classList.add('open') |
||||
}) |
||||
|
||||
keyInput.addEventListener('blur', () => { |
||||
keyboardOpen = false |
||||
shortcutBar.classList.remove('open') |
||||
}) |
||||
|
||||
screen.on('cursor-moved', updateInputPosition) |
||||
|
||||
qs('#term-kb-open').addEventListener('click', e => { |
||||
e.preventDefault() |
||||
keyInput.focus() |
||||
}) |
||||
|
||||
// Chrome for Android doesn't send proper keydown/keypress events with
|
||||
// real key values instead of 229 “Unidentified,” so here's a workaround
|
||||
// that deals with the input composition events.
|
||||
|
||||
let lastCompositionString = '' |
||||
|
||||
// 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)) { |
||||
// characters have been added at the end
|
||||
input.sendString(newValue.substr(lastCompositionString.length)) |
||||
} else resend = true |
||||
} else if (newValue.length < lastCompositionString.length) { |
||||
if (lastCompositionString.startsWith(newValue)) { |
||||
// characters have been removed at the end
|
||||
input.sendString('\b'.repeat(lastCompositionString.length - |
||||
newValue.length)) |
||||
} else resend = true |
||||
} else if (newValue !== lastCompositionString) resend = true |
||||
|
||||
if (resend) { |
||||
// the entire string changed; resend everything
|
||||
input.sendString('\b'.repeat(lastCompositionString.length) + |
||||
newValue) |
||||
} |
||||
lastCompositionString = newValue |
||||
} |
||||
|
||||
keyInput.addEventListener('keydown', e => { |
||||
if (e.key === 'Unidentified') return |
||||
|
||||
keyInput.value = '' |
||||
|
||||
e.stopPropagation() |
||||
input.handleKeyDown(e) |
||||
}) |
||||
|
||||
keyInput.addEventListener('keypress', e => { |
||||
// prevent key duplication on iOS (because Safari *does* send proper events)
|
||||
e.stopPropagation() |
||||
}) |
||||
|
||||
keyInput.addEventListener('input', e => { |
||||
e.stopPropagation() |
||||
|
||||
if (e.isComposing && 'data' in e) { |
||||
sendInputDelta(e.data) |
||||
} else if (e.isComposing) { |
||||
// Firefox Mobile doesn't support InputEvent#data, so here's a hack
|
||||
// that just takes the input value and uses that
|
||||
sendInputDelta(keyInput.value) |
||||
} else { |
||||
if (e.inputType === 'insertCompositionText') input.sendString(e.data) |
||||
else if (e.inputType === 'deleteContentBackward') { |
||||
lastCompositionString = '' |
||||
sendInputDelta('') |
||||
} else if (e.inputType === 'insertText') { |
||||
input.sendString(e.data) |
||||
} |
||||
} |
||||
}) |
||||
|
||||
keyInput.addEventListener('compositionstart', e => { |
||||
lastCompositionString = '' |
||||
}) |
||||
|
||||
keyInput.addEventListener('compositionend', e => { |
||||
lastCompositionString = '' |
||||
keyInput.value = '' |
||||
}) |
||||
|
||||
screen.on('open-soft-keyboard', () => keyInput.focus()) |
||||
|
||||
// shortcut bar
|
||||
const shortcuts = { |
||||
Control: 'ctrl', |
||||
Esc: 0x1b, |
||||
Tab: 0x09, |
||||
'←': 0x25, |
||||
'↓': 0x28, |
||||
'↑': 0x26, |
||||
'→': 0x27 |
||||
} |
||||
|
||||
let touchMoved = false |
||||
|
||||
for (const shortcut in shortcuts) { |
||||
const button = document.createElement('button') |
||||
button.classList.add('shortcut-button') |
||||
button.textContent = shortcut |
||||
shortcutBar.appendChild(button) |
||||
|
||||
const key = shortcuts[shortcut] |
||||
if (typeof key === 'string') button.classList.add('modifier') |
||||
button.addEventListener('touchstart', e => { |
||||
touchMoved = false |
||||
if (typeof key === 'string') { |
||||
// modifier button
|
||||
input.softModifiers[key] = true |
||||
button.classList.add('enabled') |
||||
|
||||
// prevent default. This prevents scrolling, but also prevents the
|
||||
// selection popup
|
||||
e.preventDefault() |
||||
} |
||||
}) |
||||
window.addEventListener('touchmove', e => { |
||||
touchMoved = true |
||||
}) |
||||
button.addEventListener('touchend', e => { |
||||
e.preventDefault() |
||||
if (typeof key === 'number') { |
||||
if (touchMoved) return |
||||
let fakeEvent = { which: key, preventDefault: () => {} } |
||||
input.handleKeyDown(fakeEvent) |
||||
} else if (typeof key === 'string') { |
||||
button.classList.remove('enabled') |
||||
input.softModifiers[key] = false |
||||
} |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,151 @@ |
||||
const $ = require('../lib/chibi') |
||||
const { rgb255ToHex } = require('../lib/color_utils') |
||||
|
||||
const themes = exports.themes = [ |
||||
[ // 0 - Tango - terminator
|
||||
'#111213', '#CC0000', '#4E9A06', '#C4A000', '#3465A4', '#75507B', '#06989A', '#D3D7CF', |
||||
'#555753', '#EF2929', '#8AE234', '#FCE94F', '#729FCF', '#AD7FA8', '#34E2E2', '#EEEEEC' |
||||
], |
||||
[ // 1 - Linux (CGA) - terminator
|
||||
'#000000', '#aa0000', '#00aa00', '#aa5500', '#0000aa', '#aa00aa', '#00aaaa', '#aaaaaa', |
||||
'#555555', '#ff5555', '#55ff55', '#ffff55', '#5555ff', '#ff55ff', '#55ffff', '#ffffff' |
||||
], |
||||
[ // 2 - xterm - terminator
|
||||
'#000000', '#cd0000', '#00cd00', '#cdcd00', '#0000ee', '#cd00cd', '#00cdcd', '#e5e5e5', |
||||
'#7f7f7f', '#ff0000', '#00ff00', '#ffff00', '#5c5cff', '#ff00ff', '#00ffff', '#ffffff' |
||||
], |
||||
[ // 3 - rxvt - terminator
|
||||
'#000000', '#cd0000', '#00cd00', '#cdcd00', '#0000cd', '#cd00cd', '#00cdcd', '#faebd7', |
||||
'#404040', '#ff0000', '#00ff00', '#ffff00', '#0000ff', '#ff00ff', '#00ffff', '#ffffff' |
||||
], |
||||
[ // 4 - Ambience - terminator
|
||||
'#2e3436', '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', '#d3d7cf', |
||||
'#555753', '#ef2929', '#8ae234', '#fce94f', '#729fcf', '#ad7fa8', '#34e2e2', '#eeeeec' |
||||
], |
||||
[ // 5 - Solarized Dark - terminator
|
||||
'#073642', '#dc322f', '#859900', '#b58900', '#268bd2', '#d33682', '#2aa198', '#eee8d5', |
||||
'#002b36', '#cb4b16', '#586e75', '#657b83', '#839496', '#6c71c4', '#93a1a1', '#fdf6e3' |
||||
], |
||||
[ // 6 - CGA NTSC - wikipedia
|
||||
'#000000', '#69001A', '#117800', '#769100', '#1A00A6', '#8019AB', '#289E76', '#A4A4A4', |
||||
'#484848', '#C54E76', '#6DD441', '#D2ED46', '#765BFF', '#DC75FF', '#84FAD2', '#FFFFFF' |
||||
], |
||||
[ // 7 - ZX Spectrum - wikipedia
|
||||
'#000000', '#aa0000', '#00aa00', '#aaaa00', '#0000aa', '#aa00aa', '#00aaaa', '#aaaaaa', |
||||
'#000000', '#ff0000', '#00FF00', '#ffff00', '#0000ff', '#ff00ff', '#00ffff', '#ffffff' |
||||
], |
||||
[ // 8 - Apple II - wikipedia
|
||||
'#000000', '#722640', '#0E5940', '#808080', '#40337F', '#E434FE', '#1B9AFE', '#BFB3FF', |
||||
'#404C00', '#E46501', '#1BCB01', '#BFCC80', '#808080', '#F1A6BF', '#8DD9BF', '#ffffff' |
||||
], |
||||
[ // 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', '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 |
||||
|
||||
exports.buildColorTable = function () { |
||||
if (colorTable256 !== null) return colorTable256 |
||||
|
||||
// 256color lookup table
|
||||
// should not be used to look up 0-15
|
||||
colorTable256 = new Array(16).fill('#000000') |
||||
|
||||
// fill color table
|
||||
// colors 16-231 are a 6x6x6 color cube
|
||||
for (let red = 0; red < 6; red++) { |
||||
for (let green = 0; green < 6; green++) { |
||||
for (let blue = 0; blue < 6; blue++) { |
||||
let redValue = red * 40 + (red ? 55 : 0) |
||||
let greenValue = green * 40 + (green ? 55 : 0) |
||||
let blueValue = blue * 40 + (blue ? 55 : 0) |
||||
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(rgb255ToHex(value, value, value)) |
||||
} |
||||
|
||||
return colorTable256 |
||||
} |
||||
|
||||
exports.SELECTION_FG = '#333' |
||||
exports.SELECTION_BG = '#b2d7fe' |
||||
|
||||
exports.themePreview = function (themeN) { |
||||
$('[data-fg]').forEach((elem) => { |
||||
let shade = elem.dataset.fg |
||||
if (/^\d+$/.test(shade)) shade = exports.toHex(shade, themeN) |
||||
elem.style.color = shade |
||||
}) |
||||
$('[data-bg]').forEach((elem) => { |
||||
let shade = elem.dataset.bg |
||||
if (/^\d+$/.test(shade)) shade = exports.toHex(shade, themeN) |
||||
elem.style.backgroundColor = shade |
||||
}) |
||||
} |
||||
|
||||
exports.colorTable256 = null |
||||
exports.ensureColorTable256 = function () { |
||||
if (!exports.colorTable256) exports.colorTable256 = exports.buildColorTable() |
||||
} |
||||
|
||||
exports.getColor = function (i, palette = []) { |
||||
// return palette color if it exists
|
||||
if (i < 16 && i in palette) return palette[i] |
||||
|
||||
// -1 for selection foreground, -2 for selection background
|
||||
if (i === -1) return exports.SELECTION_FG |
||||
if (i === -2) return exports.SELECTION_BG |
||||
|
||||
// 256 color
|
||||
if (i > 15 && i < 256) { |
||||
exports.ensureColorTable256() |
||||
return exports.colorTable256[i] |
||||
} |
||||
|
||||
// 24-bit color, encoded as (hex) + 256 (such that #000000 == 256)
|
||||
if (i > 255) { |
||||
i -= 256 |
||||
return '#' + `000000${i.toString(16)}`.substr(-6) |
||||
} |
||||
|
||||
// return error color
|
||||
return Math.floor(Date.now() / 1000) % 2 === 0 ? '#ff0ff' : '#00ff00' |
||||
} |
||||
|
||||
exports.toHex = function (shade, themeN) { |
||||
if (/^\d+$/.test(shade)) { |
||||
shade = +shade |
||||
return exports.getColor(shade, themes[themeN]) |
||||
} |
||||
return shade |
||||
} |
@ -0,0 +1,182 @@ |
||||
const $ = require('../lib/chibi') |
||||
const { qs } = require('../utils') |
||||
const modal = require('../modal') |
||||
|
||||
/** File upload utility */ |
||||
module.exports = function (conn, input, screen) { |
||||
let lines, // array of lines without newlines
|
||||
line_i, // current line index
|
||||
fuTout, // timeout handle for line sending
|
||||
send_delay_ms, // delay between lines (ms)
|
||||
nl_str, // newline string to use
|
||||
curLine, // current line (when using fuOil)
|
||||
inline_pos // Offset in line (for long lines)
|
||||
|
||||
// lines longer than this are split to chunks
|
||||
// sending a super-ling string through the socket is not a good idea
|
||||
const MAX_LINE_LEN = 128 |
||||
|
||||
function openUploadDialog () { |
||||
updateStatus('Ready...') |
||||
modal.show('#fu_modal', onDialogClose) |
||||
$('#fu_form').toggleClass('busy', false) |
||||
input.blockKeys(true) |
||||
} |
||||
|
||||
function onDialogClose () { |
||||
console.log('Upload modal closed.') |
||||
clearTimeout(fuTout) |
||||
line_i = 0 |
||||
input.blockKeys(false) |
||||
} |
||||
|
||||
function updateStatus (msg) { |
||||
qs('#fu_prog').textContent = msg |
||||
} |
||||
|
||||
function startUpload () { |
||||
let v = qs('#fu_text').value |
||||
if (!v.length) { |
||||
fuClose() |
||||
return |
||||
} |
||||
|
||||
lines = v.split('\n') |
||||
line_i = 0 |
||||
inline_pos = 0 // offset in line
|
||||
send_delay_ms = +qs('#fu_delay').value |
||||
|
||||
// sanitize - 0 causes overflows
|
||||
if (send_delay_ms < 0) { |
||||
send_delay_ms = 0 |
||||
qs('#fu_delay').value = send_delay_ms |
||||
} |
||||
|
||||
nl_str = { |
||||
'CR': '\r', |
||||
'LF': '\n', |
||||
'CRLF': '\r\n' |
||||
}[qs('#fu_crlf').value] |
||||
|
||||
$('#fu_form').toggleClass('busy', true) |
||||
updateStatus('Starting...') |
||||
uploadLine() |
||||
} |
||||
|
||||
function uploadLine () { |
||||
if (!$('#fu_modal').hasClass('visible')) { |
||||
// Modal is closed, cancel
|
||||
return |
||||
} |
||||
|
||||
if (!conn.canSend()) { |
||||
// postpone
|
||||
fuTout = setTimeout(uploadLine, 1) |
||||
return |
||||
} |
||||
|
||||
if (inline_pos === 0) { |
||||
curLine = '' |
||||
if (line_i === 0) { |
||||
if (screen.bracketedPaste) { |
||||
curLine = '\x1b[200~' |
||||
} |
||||
} |
||||
|
||||
curLine += lines[line_i++] + nl_str |
||||
|
||||
if (line_i === lines.length) { |
||||
if (screen.bracketedPaste) { |
||||
curLine += '\x1b[201~' |
||||
} |
||||
} |
||||
} |
||||
|
||||
let maxChunk = +qs('#fu_chunk').value |
||||
if (maxChunk === 0 || maxChunk > MAX_LINE_LEN) { |
||||
maxChunk = MAX_LINE_LEN |
||||
} |
||||
|
||||
let chunk |
||||
if ((curLine.length - inline_pos) <= maxChunk) { |
||||
chunk = curLine.substr(inline_pos, maxChunk) |
||||
inline_pos = 0 |
||||
} else { |
||||
chunk = curLine.substr(inline_pos, maxChunk) |
||||
inline_pos += maxChunk |
||||
} |
||||
|
||||
if (!input.sendString(chunk)) { |
||||
updateStatus('FAILED!') |
||||
return |
||||
} |
||||
|
||||
let pt = Math.round((line_i / lines.length) * 1000) / 10 |
||||
updateStatus(`${line_i} / ${lines.length} (${pt}%)`) |
||||
|
||||
if (lines.length > line_i || inline_pos > 0) { |
||||
fuTout = setTimeout(uploadLine, send_delay_ms) |
||||
} else { |
||||
closeWhenReady() |
||||
} |
||||
} |
||||
|
||||
function closeWhenReady () { |
||||
if (!conn.canSend()) { |
||||
// stuck in XOFF still, wait to process...
|
||||
updateStatus('Waiting for Tx buffer...') |
||||
setTimeout(closeWhenReady, 100) |
||||
} else { |
||||
updateStatus('Done.') |
||||
// delay to show it
|
||||
fuClose() |
||||
} |
||||
} |
||||
|
||||
function fuClose () { |
||||
modal.hide('#fu_modal') |
||||
} |
||||
|
||||
return { |
||||
init: function () { |
||||
qs('#fu_file').addEventListener('change', function (evt) { |
||||
let reader = new window.FileReader() |
||||
let file = evt.target.files[0] |
||||
let ftype = file.type || 'application/octet-stream' |
||||
console.log('Selected file type: ' + ftype) |
||||
if (!ftype.match(/text\/.*|application\/(json|csv|.*xml.*|.*script.*|x-php)/)) { |
||||
// Deny load of blobs like img - can crash browser and will get corrupted anyway
|
||||
if (!window.confirm(`This does not look like a text file: ${ftype}\nReally load?`)) { |
||||
qs('#fu_file').value = '' |
||||
return |
||||
} |
||||
} |
||||
reader.onload = function (e) { |
||||
const txt = e.target.result.replace(/[\r\n]+/, '\n') |
||||
qs('#fu_text').value = txt |
||||
} |
||||
console.log('Loading file...') |
||||
reader.readAsText(file) |
||||
}, false) |
||||
|
||||
qs('#term-fu-open').addEventListener('click', e => { |
||||
e.preventDefault() |
||||
openUploadDialog() |
||||
}) |
||||
|
||||
qs('#term-fu-start').addEventListener('click', e => { |
||||
e.preventDefault() |
||||
startUpload() |
||||
}) |
||||
|
||||
qs('#term-fu-close').addEventListener('click', e => { |
||||
e.preventDefault() |
||||
fuClose() |
||||
}) |
||||
}, |
||||
open: openUploadDialog, |
||||
setContent (content) { |
||||
qs('#fu_text').value = content |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,106 @@ |
||||
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,#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) |
||||
for (let i = 1; i <= 5; i++) { |
||||
ColorTriangle.initInput(qs(`#bc${i}`), 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 + '"> ' + lbl + ' </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() |
||||
} |
@ -0,0 +1,60 @@ |
||||
/** Make a node */ |
||||
exports.mk = function mk (e) { |
||||
return document.createElement(e) |
||||
} |
||||
|
||||
/** Find one by query */ |
||||
exports.qs = function qs (s) { |
||||
return document.querySelector(s) |
||||
} |
||||
|
||||
/** Find all by query */ |
||||
exports.qsa = function qsa (s) { |
||||
return document.querySelectorAll(s) |
||||
} |
||||
|
||||
/** |
||||
* Filter 'spacebar' and 'return' from keypress handler, |
||||
* and when they're pressed, fire the callback. |
||||
* use $(...).on('keypress', cr(handler)) |
||||
*/ |
||||
exports.cr = function cr (hdl) { |
||||
return function (e) { |
||||
if (e.which === 10 || e.which === 13 || e.which === 32) { |
||||
hdl() |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** Decode number from 2B encoding */ |
||||
exports.parse2B = function parse2B (s, i = 0) { |
||||
return (s.charCodeAt(i++) - 1) + (s.charCodeAt(i) - 1) * 127 |
||||
} |
||||
|
||||
/** Decode number from 3B encoding */ |
||||
exports.parse3B = function parse3B (s, i = 0) { |
||||
return (s.charCodeAt(i) - 1) + (s.charCodeAt(i + 1) - 1) * 127 + (s.charCodeAt(i + 2) - 1) * 127 * 127 |
||||
} |
||||
|
||||
/** Encode using 2B encoding, returns string. */ |
||||
exports.encode2B = function encode2B (n) { |
||||
let lsb, msb |
||||
lsb = (n % 127) |
||||
n = ((n - lsb) / 127) |
||||
lsb += 1 |
||||
msb = (n + 1) |
||||
return String.fromCharCode(lsb) + String.fromCharCode(msb) |
||||
} |
||||
|
||||
/** Encode using 3B encoding, returns string. */ |
||||
exports.encode3B = function encode3B (n) { |
||||
let lsb, msb, xsb |
||||
lsb = (n % 127) |
||||
n = (n - lsb) / 127 |
||||
lsb += 1 |
||||
msb = (n % 127) |
||||
n = (n - msb) / 127 |
||||
msb += 1 |
||||
xsb = (n + 1) |
||||
return String.fromCharCode(lsb) + String.fromCharCode(msb) + String.fromCharCode(xsb) |
||||
} |
@ -0,0 +1,173 @@ |
||||
const $ = require('./lib/chibi') |
||||
const { mk } = require('./utils') |
||||
const tr = require('./lang') |
||||
|
||||
const HTTPS = window.location.protocol.match(/s:/) |
||||
|
||||
{ |
||||
const w = window.WiFi = {} |
||||
|
||||
const authTypes = ['Open', 'WEP', 'WPA', 'WPA2', 'WPA/WPA2'] |
||||
let curSSID |
||||
|
||||
// Get XX % for a slider input
|
||||
function calc_dBm (inp) { |
||||
return `+${(inp.value * 0.25).toFixed(2)} dBm` |
||||
} |
||||
|
||||
// Display selected STA SSID etc
|
||||
function selectSta (name, password, ip) { |
||||
$('#sta_ssid').val(name) |
||||
$('#sta_password').val(password) |
||||
|
||||
$('#sta-nw').toggleClass('hidden', name.length === 0) |
||||
$('#sta-nw-nil').toggleClass('hidden', name.length > 0) |
||||
|
||||
$('#sta-nw .essid').html($.htmlEscape(name)) |
||||
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')) |
||||
} |
||||
|
||||
/** Update display for received response */ |
||||
function onScan (resp, status) { |
||||
// var ap_json = {
|
||||
// "result": {
|
||||
// "inProgress": "0",
|
||||
// "APs": [
|
||||
// {"essid": "Chlivek", "bssid": "88:f7:c7:52:b3:99", "rssi": "204", "enc": "4", "channel": "1"},
|
||||
// {"essid": "TyNikdy", "bssid": "5c:f4:ab:0d:f1:1b", "rssi": "164", "enc": "3", "channel": "1"},
|
||||
// ]
|
||||
// }
|
||||
// };
|
||||
|
||||
if (status !== 200) { |
||||
// bad response
|
||||
rescan(5000) // wait 5s then retry
|
||||
return |
||||
} |
||||
|
||||
try { |
||||
resp = JSON.parse(resp) |
||||
} catch (e) { |
||||
console.log(e) |
||||
rescan(5000) |
||||
return |
||||
} |
||||
|
||||
const done = !resp.result.inProgress && resp.result.APs.length > 0 |
||||
rescan(done ? 15000 : 1000) |
||||
if (!done) return // no redraw yet
|
||||
|
||||
// clear the AP list
|
||||
let $list = $('#ap-list') |
||||
// remove old APs
|
||||
$('#ap-list .AP').remove() |
||||
|
||||
$list.toggleClass('hidden', !done) |
||||
$('#ap-loader').toggleClass('hidden', done) |
||||
|
||||
// scan done
|
||||
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
|
||||
|
||||
let item = mk('div') |
||||
|
||||
let $item = $(item) |
||||
.data('ssid', ap.essid) |
||||
.data('pwd', ap.enc) |
||||
.attr('tabindex', 0) |
||||
.addClass('AP') |
||||
|
||||
// mark current SSID
|
||||
if (ap.essid === curSSID) { |
||||
$item.addClass('selected') |
||||
} |
||||
|
||||
let inner = mk('div') |
||||
let escapedSSID = $.htmlEscape(ap.essid) |
||||
$(inner).addClass('inner') |
||||
.htmlAppend(`<div class="rssi">${ap.rssi_perc}</div>`) |
||||
.htmlAppend(`<div class="essid" title="${escapedSSID}">${escapedSSID}</div>`) |
||||
.htmlAppend(`<div class="auth">${authTypes[ap.enc]}</div>`) |
||||
|
||||
$item.on('click', function () { |
||||
let $th = $(this) |
||||
|
||||
const conn_ssid = $th.data('ssid') |
||||
let conn_pass = '' |
||||
|
||||
if (+$th.data('pwd')) { |
||||
// this AP needs a password
|
||||
conn_pass = window.prompt(tr('wifi.enter_passwd').replace(':ssid:', conn_ssid)) |
||||
if (!conn_pass) return |
||||
} |
||||
|
||||
$('#sta_password').val(conn_pass) |
||||
$('#sta_ssid').val(conn_ssid) |
||||
selectSta(conn_ssid, conn_pass, '') |
||||
}) |
||||
|
||||
item.appendChild(inner) |
||||
$list[0].appendChild(item) |
||||
}) |
||||
} |
||||
|
||||
function startScanning () { |
||||
$('#ap-loader').removeClass('hidden') |
||||
$('#ap-scan').addClass('hidden') |
||||
$('#ap-loader .anim-dots').html('.') |
||||
|
||||
scanAPs() |
||||
} |
||||
|
||||
/** Ask the CGI what APs are visible (async) */ |
||||
function scanAPs () { |
||||
if (window._demo) { |
||||
onScan(window._demo_aps, 200) |
||||
} else { |
||||
$.get(`${HTTPS ? 'https' : 'http'}://${window._root}/cfg/wifi/scan`, onScan) |
||||
} |
||||
} |
||||
|
||||
function rescan (time) { |
||||
setTimeout(scanAPs, time) |
||||
} |
||||
|
||||
/** Set up the WiFi page */ |
||||
function wifiInit (cfg) { |
||||
// Update slider value displays
|
||||
$('.Row.range').forEach(function (x) { |
||||
let inp = x.querySelector('input') |
||||
let disp1 = x.querySelector('.x-disp1') |
||||
let disp2 = x.querySelector('.x-disp2') |
||||
let t = calc_dBm(inp) |
||||
$(disp1).html(t) |
||||
$(disp2).html(t) |
||||
$(inp).on('input', function () { |
||||
t = calc_dBm(inp) |
||||
$(disp1).html(t) |
||||
$(disp2).html(t) |
||||
}) |
||||
}) |
||||
|
||||
// Forget STA credentials
|
||||
$('#forget-sta').on('click', () => { |
||||
selectSta('', '', '') |
||||
return false |
||||
}) |
||||
|
||||
selectSta(cfg.sta_ssid, cfg.sta_password, cfg.sta_active_ip) |
||||
curSSID = cfg.sta_active_ssid |
||||
} |
||||
|
||||
w.init = wifiInit |
||||
w.startScanning = startScanning |
||||
} |
@ -1,189 +0,0 @@ |
||||
/** Global generic init */ |
||||
$.ready(function () { |
||||
// Checkbox UI (checkbox CSS and hidden input with int value)
|
||||
$('.Row.checkbox').forEach(function(x) { |
||||
var inp = x.querySelector('input'); |
||||
var box = x.querySelector('.box'); |
||||
|
||||
$(box).toggleClass('checked', inp.value); |
||||
|
||||
var hdl = function() { |
||||
inp.value = 1 - inp.value; |
||||
$(box).toggleClass('checked', inp.value) |
||||
}; |
||||
|
||||
$(x).on('click', hdl).on('keypress', cr(hdl)); |
||||
}); |
||||
|
||||
// Expanding boxes on mobile
|
||||
$('.Box.mobcol,.Box.fold').forEach(function(x) { |
||||
var h = x.querySelector('h2'); |
||||
|
||||
var hdl = function() { |
||||
$(x).toggleClass('expanded'); |
||||
}; |
||||
$(h).on('click', hdl).on('keypress', cr(hdl)); |
||||
}); |
||||
|
||||
$('form').forEach(function(x) { |
||||
$(x).on('keypress', function(e) { |
||||
if ((e.keyCode == 10 || e.keyCode == 13) && e.ctrlKey) { |
||||
x.submit(); |
||||
} |
||||
}) |
||||
}); |
||||
|
||||
// loader dots...
|
||||
setInterval(function () { |
||||
$('.anim-dots').each(function (x) { |
||||
var $x = $(x); |
||||
var dots = $x.html() + '.'; |
||||
if (dots.length == 5) dots = '.'; |
||||
$x.html(dots); |
||||
}); |
||||
}, 1000); |
||||
|
||||
// flipping number boxes with the mouse wheel
|
||||
$('input[type=number]').on('mousewheel', function(e) { |
||||
var $this = $(this); |
||||
var val = +$this.val(); |
||||
if (isNaN(val)) val = 1; |
||||
|
||||
var step = +($this.attr('step') || 1); |
||||
var min = +$this.attr('min'); |
||||
var max = +$this.attr('max'); |
||||
if(e.wheelDelta > 0) { |
||||
val += step; |
||||
} else { |
||||
val -= step; |
||||
} |
||||
|
||||
if (typeof min != 'undefined') val = Math.max(val, +min); |
||||
if (typeof max != 'undefined') val = Math.min(val, +max); |
||||
$this.val(val); |
||||
|
||||
if ("createEvent" in document) { |
||||
var evt = document.createEvent("HTMLEvents"); |
||||
evt.initEvent("change", false, true); |
||||
$this[0].dispatchEvent(evt); |
||||
} else { |
||||
$this[0].fireEvent("onchange"); |
||||
} |
||||
|
||||
e.preventDefault(); |
||||
}); |
||||
|
||||
var errAt = location.search.indexOf('err='); |
||||
if (errAt !== -1 && qs('.Box.errors')) { |
||||
var errs = location.search.substr(errAt+4).split(','); |
||||
var hres = []; |
||||
errs.forEach(function(er) { |
||||
var lbl = qs('label[for="'+er+'"]'); |
||||
if (lbl) { |
||||
lbl.classList.add('error'); |
||||
hres.push(lbl.childNodes[0].textContent.trim().replace(/: ?$/, '')); |
||||
} else { |
||||
hres.push(er); |
||||
} |
||||
}); |
||||
|
||||
qs('.Box.errors .list').innerHTML = hres.join(', '); |
||||
qs('.Box.errors').classList.remove('hidden'); |
||||
} |
||||
|
||||
Modal.init(); |
||||
Notify.init(); |
||||
|
||||
// remove tabindixes from h2 if wide
|
||||
if (window.innerWidth > 550) { |
||||
$('.Box h2').forEach(function (x) { |
||||
x.removeAttribute('tabindex'); |
||||
}); |
||||
|
||||
// brand works as a link back to term in widescreen mode
|
||||
var br = qs('#brand'); |
||||
br && br.addEventListener('click', function() { |
||||
location.href='/'; // go to terminal
|
||||
}); |
||||
} |
||||
}); |
||||
|
||||
$._loader = function(vis) { |
||||
$('#loader').toggleClass('show', vis); |
||||
}; |
||||
|
||||
function showPage() { |
||||
$('#content').addClass('load'); |
||||
} |
||||
|
||||
$.ready(function() { |
||||
if (window.noAutoShow !== true) { |
||||
setTimeout(function () { |
||||
showPage(); |
||||
}, 1); |
||||
} |
||||
}); |
||||
|
||||
|
||||
/*! http://mths.be/fromcodepoint v0.1.0 by @mathias */ |
||||
if (!String.fromCodePoint) { |
||||
(function() { |
||||
var defineProperty = (function() { |
||||
// IE 8 only supports `Object.defineProperty` on DOM elements
|
||||
try { |
||||
var object = {}; |
||||
var $defineProperty = Object.defineProperty; |
||||
var result = $defineProperty(object, object, object) && $defineProperty; |
||||
} catch(error) {} |
||||
return result; |
||||
}()); |
||||
var stringFromCharCode = String.fromCharCode; |
||||
var floor = Math.floor; |
||||
var fromCodePoint = function() { |
||||
var MAX_SIZE = 0x4000; |
||||
var codeUnits = []; |
||||
var highSurrogate; |
||||
var lowSurrogate; |
||||
var index = -1; |
||||
var length = arguments.length; |
||||
if (!length) { |
||||
return ''; |
||||
} |
||||
var result = ''; |
||||
while (++index < length) { |
||||
var codePoint = Number(arguments[index]); |
||||
if ( |
||||
!isFinite(codePoint) || // `NaN`, `+Infinity`, or `-Infinity`
|
||||
codePoint < 0 || // not a valid Unicode code point
|
||||
codePoint > 0x10FFFF || // not a valid Unicode code point
|
||||
floor(codePoint) != codePoint // not an integer
|
||||
) { |
||||
throw RangeError('Invalid code point: ' + codePoint); |
||||
} |
||||
if (codePoint <= 0xFFFF) { // BMP code point
|
||||
codeUnits.push(codePoint); |
||||
} else { // Astral code point; split in surrogate halves
|
||||
// http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
|
||||
codePoint -= 0x10000; |
||||
highSurrogate = (codePoint >> 10) + 0xD800; |
||||
lowSurrogate = (codePoint % 0x400) + 0xDC00; |
||||
codeUnits.push(highSurrogate, lowSurrogate); |
||||
} |
||||
if (index + 1 == length || codeUnits.length > MAX_SIZE) { |
||||
result += stringFromCharCode.apply(null, codeUnits); |
||||
codeUnits.length = 0; |
||||
} |
||||
} |
||||
return result; |
||||
}; |
||||
if (defineProperty) { |
||||
defineProperty(String, 'fromCodePoint', { |
||||
'value': fromCodePoint, |
||||
'configurable': true, |
||||
'writable': true |
||||
}); |
||||
} else { |
||||
String.fromCodePoint = fromCodePoint; |
||||
} |
||||
}()); |
||||
} |
@ -1,310 +0,0 @@ |
||||
// keymaster.js
|
||||
// (c) 2011-2013 Thomas Fuchs
|
||||
// keymaster.js may be freely distributed under the MIT license.
|
||||
|
||||
;(function(global){ |
||||
var k, |
||||
_handlers = {}, |
||||
_mods = { 16: false, 18: false, 17: false, 91: false }, |
||||
_scope = 'all', |
||||
// modifier keys
|
||||
_MODIFIERS = { |
||||
'⇧': 16, shift: 16, |
||||
'⌥': 18, alt: 18, option: 18, |
||||
'⌃': 17, ctrl: 17, control: 17, |
||||
'⌘': 91, command: 91 |
||||
}, |
||||
// special keys
|
||||
_MAP = { |
||||
backspace: 8, tab: 9, clear: 12, |
||||
enter: 13, 'return': 13, |
||||
esc: 27, escape: 27, space: 32, |
||||
left: 37, up: 38, |
||||
right: 39, down: 40, |
||||
del: 46, 'delete': 46, |
||||
home: 36, end: 35, |
||||
pageup: 33, pagedown: 34, |
||||
',': 188, '.': 190, '/': 191, |
||||
'`': 192, '-': 189, '=': 187, |
||||
';': 186, '\'': 222, |
||||
'[': 219, ']': 221, '\\': 220, |
||||
// added:
|
||||
insert: 45, |
||||
np_0: 96, np_1: 97, np_2: 98, np_3: 99, np_4: 100, np_5: 101, |
||||
np_6: 102, np_7: 103, np_8: 104, np_9: 105, np_mul: 106, |
||||
np_add: 107, np_sub: 109, np_point: 110, np_div: 111, numlock: 144, |
||||
}, |
||||
code = function(x){ |
||||
return _MAP[x] || x.toUpperCase().charCodeAt(0); |
||||
}, |
||||
_downKeys = []; |
||||
|
||||
for(k=1;k<20;k++) _MAP['f'+k] = 111+k; |
||||
|
||||
// IE doesn't support Array#indexOf, so have a simple replacement
|
||||
function index(array, item){ |
||||
var i = array.length; |
||||
while(i--) if(array[i]===item) return i; |
||||
return -1; |
||||
} |
||||
|
||||
// for comparing mods before unassignment
|
||||
function compareArray(a1, a2) { |
||||
if (a1.length != a2.length) return false; |
||||
for (var i = 0; i < a1.length; i++) { |
||||
if (a1[i] !== a2[i]) return false; |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
var modifierMap = { |
||||
16:'shiftKey', |
||||
18:'altKey', |
||||
17:'ctrlKey', |
||||
91:'metaKey' |
||||
}; |
||||
function updateModifierKey(event) { |
||||
for(k in _mods) _mods[k] = event[modifierMap[k]]; |
||||
}; |
||||
|
||||
function isModifierPressed(mod) { |
||||
if (mod=='control'||mod=='ctrl') return _mods[17]; |
||||
if (mod=='shift') return _mods[16]; |
||||
if (mod=='meta') return _mods[91]; |
||||
if (mod=='alt') return _mods[18]; |
||||
return false; |
||||
} |
||||
|
||||
// handle keydown event
|
||||
function dispatch(event) { |
||||
var key, handler, k, i, modifiersMatch, scope; |
||||
key = event.keyCode; |
||||
|
||||
if (index(_downKeys, key) == -1) { |
||||
_downKeys.push(key); |
||||
} |
||||
|
||||
// if a modifier key, set the key.<modifierkeyname> property to true and return
|
||||
if(key == 93 || key == 224) key = 91; // right command on webkit, command on Gecko
|
||||
if(key in _mods) { |
||||
_mods[key] = true; |
||||
// 'assignKey' from inside this closure is exported to window.key
|
||||
for(k in _MODIFIERS) if(_MODIFIERS[k] == key) assignKey[k] = true; |
||||
return; |
||||
} |
||||
updateModifierKey(event); |
||||
|
||||
// see if we need to ignore the keypress (filter() can can be overridden)
|
||||
// by default ignore key presses if a select, textarea, or input is focused
|
||||
if(!assignKey.filter.call(this, event)) return; |
||||
|
||||
// abort if no potentially matching shortcuts found
|
||||
if (!(key in _handlers)) return; |
||||
|
||||
scope = getScope(); |
||||
|
||||
// for each potential shortcut
|
||||
for (i = 0; i < _handlers[key].length; i++) { |
||||
handler = _handlers[key][i]; |
||||
|
||||
// see if it's in the current scope
|
||||
if(handler.scope == scope || handler.scope == 'all'){ |
||||
// check if modifiers match if any
|
||||
modifiersMatch = handler.mods.length > 0; |
||||
for(k in _mods) |
||||
if((!_mods[k] && index(handler.mods, +k) > -1) || |
||||
(_mods[k] && index(handler.mods, +k) == -1)) modifiersMatch = false; |
||||
// call the handler and stop the event if neccessary
|
||||
if((handler.mods.length == 0 && !_mods[16] && !_mods[18] && !_mods[17] && !_mods[91]) || modifiersMatch){ |
||||
if(handler.method(event, handler)===false){ |
||||
if(event.preventDefault) event.preventDefault(); |
||||
else event.returnValue = false; |
||||
if(event.stopPropagation) event.stopPropagation(); |
||||
if(event.cancelBubble) event.cancelBubble = true; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
}; |
||||
|
||||
// unset modifier keys on keyup
|
||||
function clearModifier(event){ |
||||
var key = event.keyCode, k, |
||||
i = index(_downKeys, key); |
||||
|
||||
// remove key from _downKeys
|
||||
if (i >= 0) { |
||||
_downKeys.splice(i, 1); |
||||
} |
||||
|
||||
if(key == 93 || key == 224) key = 91; |
||||
if(key in _mods) { |
||||
_mods[key] = false; |
||||
for(k in _MODIFIERS) if(_MODIFIERS[k] == key) assignKey[k] = false; |
||||
} |
||||
}; |
||||
|
||||
function resetModifiers() { |
||||
for(k in _mods) _mods[k] = false; |
||||
for(k in _MODIFIERS) assignKey[k] = false; |
||||
}; |
||||
|
||||
// parse and assign shortcut
|
||||
function assignKey(key, scope, method){ |
||||
var keys, mods; |
||||
keys = getKeys(key); |
||||
if (method === undefined) { |
||||
method = scope; |
||||
scope = 'all'; |
||||
} |
||||
|
||||
// for each shortcut
|
||||
for (var i = 0; i < keys.length; i++) { |
||||
// set modifier keys if any
|
||||
mods = []; |
||||
key = keys[i].split('+'); |
||||
if (key.length > 1){ |
||||
mods = getMods(key); |
||||
key = [key[key.length-1]]; |
||||
} |
||||
// convert to keycode and...
|
||||
key = key[0] |
||||
key = code(key); |
||||
// ...store handler
|
||||
if (!(key in _handlers)) _handlers[key] = []; |
||||
_handlers[key].push({ shortcut: keys[i], scope: scope, method: method, key: keys[i], mods: mods }); |
||||
} |
||||
}; |
||||
|
||||
// unbind all handlers for given key in current scope
|
||||
function unbindKey(key, scope) { |
||||
var multipleKeys, keys, |
||||
mods = [], |
||||
i, j, obj; |
||||
|
||||
multipleKeys = getKeys(key); |
||||
|
||||
for (j = 0; j < multipleKeys.length; j++) { |
||||
keys = multipleKeys[j].split('+'); |
||||
|
||||
if (keys.length > 1) { |
||||
mods = getMods(keys); |
||||
} |
||||
|
||||
key = keys[keys.length - 1]; |
||||
key = code(key); |
||||
|
||||
if (scope === undefined) { |
||||
scope = getScope(); |
||||
} |
||||
if (!_handlers[key]) { |
||||
return; |
||||
} |
||||
for (i = 0; i < _handlers[key].length; i++) { |
||||
obj = _handlers[key][i]; |
||||
// only clear handlers if correct scope and mods match
|
||||
if (obj.scope === scope && compareArray(obj.mods, mods)) { |
||||
_handlers[key][i] = {}; |
||||
} |
||||
} |
||||
} |
||||
}; |
||||
|
||||
// Returns true if the key with code 'keyCode' is currently down
|
||||
// Converts strings into key codes.
|
||||
function isPressed(keyCode) { |
||||
if (typeof(keyCode)=='string') { |
||||
keyCode = code(keyCode); |
||||
} |
||||
return index(_downKeys, keyCode) != -1; |
||||
} |
||||
|
||||
function getPressedKeyCodes() { |
||||
return _downKeys.slice(0); |
||||
} |
||||
|
||||
function filter(event){ |
||||
var tagName = (event.target || event.srcElement).tagName; |
||||
// ignore keypressed in any elements that support keyboard data input
|
||||
return !(tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA'); |
||||
} |
||||
|
||||
// initialize key.<modifier> to false
|
||||
for(k in _MODIFIERS) assignKey[k] = false; |
||||
|
||||
// set current scope (default 'all')
|
||||
function setScope(scope){ _scope = scope || 'all' }; |
||||
function getScope(){ return _scope || 'all' }; |
||||
|
||||
// delete all handlers for a given scope
|
||||
function deleteScope(scope){ |
||||
var key, handlers, i; |
||||
|
||||
for (key in _handlers) { |
||||
handlers = _handlers[key]; |
||||
for (i = 0; i < handlers.length; ) { |
||||
if (handlers[i].scope === scope) handlers.splice(i, 1); |
||||
else i++; |
||||
} |
||||
} |
||||
}; |
||||
|
||||
// abstract key logic for assign and unassign
|
||||
function getKeys(key) { |
||||
var keys; |
||||
key = key.replace(/\s/g, ''); |
||||
keys = key.split(','); |
||||
if ((keys[keys.length - 1]) == '') { |
||||
keys[keys.length - 2] += ','; |
||||
} |
||||
return keys; |
||||
} |
||||
|
||||
// abstract mods logic for assign and unassign
|
||||
function getMods(key) { |
||||
var mods = key.slice(0, key.length - 1); |
||||
for (var mi = 0; mi < mods.length; mi++) |
||||
mods[mi] = _MODIFIERS[mods[mi]]; |
||||
return mods; |
||||
} |
||||
|
||||
// cross-browser events
|
||||
function addEvent(object, event, method) { |
||||
if (object.addEventListener) |
||||
object.addEventListener(event, method, false); |
||||
else if(object.attachEvent) |
||||
object.attachEvent('on'+event, function(){ method(window.event) }); |
||||
}; |
||||
|
||||
// set the handlers globally on document
|
||||
addEvent(document, 'keydown', function(event) { dispatch(event) }); // Passing _scope to a callback to ensure it remains the same by execution. Fixes #48
|
||||
addEvent(document, 'keyup', clearModifier); |
||||
|
||||
// reset modifiers to false whenever the window is (re)focused.
|
||||
addEvent(window, 'focus', resetModifiers); |
||||
|
||||
// store previously defined key
|
||||
var previousKey = global.key; |
||||
|
||||
// restore previously defined key and return reference to our key object
|
||||
function noConflict() { |
||||
var k = global.key; |
||||
global.key = previousKey; |
||||
return k; |
||||
} |
||||
|
||||
// set window.key and window.key.set/get/deleteScope, and the default filter
|
||||
global.key = assignKey; |
||||
global.key.setScope = setScope; |
||||
global.key.getScope = getScope; |
||||
global.key.deleteScope = deleteScope; |
||||
global.key.filter = filter; |
||||
global.key.isPressed = isPressed; |
||||
global.key.isModifier = isModifierPressed; |
||||
global.key.getPressedKeyCodes = getPressedKeyCodes; |
||||
global.key.noConflict = noConflict; |
||||
global.key.unbind = unbindKey; |
||||
|
||||
if(typeof module !== 'undefined') module.exports = assignKey; |
||||
|
||||
})(this); |
@ -1,8 +0,0 @@ |
||||
// Generated from PHP locale file
|
||||
var _tr = { |
||||
"wifi.connected_ip_is": "Connected, IP is ", |
||||
"wifi.not_conn": "Not connected.", |
||||
"wifi.enter_passwd": "Enter password for \":ssid:\"" |
||||
}; |
||||
|
||||
function tr(key) { return _tr[key] || '?'+key+'?'; } |
@ -1,44 +0,0 @@ |
||||
/** Module for toggling a modal overlay */ |
||||
(function () { |
||||
var modal = {}; |
||||
var curCloseCb = null; |
||||
|
||||
modal.show = function (sel, closeCb) { |
||||
var $m = $(sel); |
||||
$m.removeClass('hidden visible'); |
||||
setTimeout(function () { |
||||
$m.addClass('visible'); |
||||
}, 1); |
||||
curCloseCb = closeCb; |
||||
}; |
||||
|
||||
modal.hide = function (sel) { |
||||
var $m = $(sel); |
||||
$m.removeClass('visible'); |
||||
setTimeout(function () { |
||||
$m.addClass('hidden'); |
||||
if (curCloseCb) curCloseCb(); |
||||
}, 500); // transition time
|
||||
}; |
||||
|
||||
modal.init = function () { |
||||
// close modal by click outside the dialog
|
||||
$('.Modal').on('click', function () { |
||||
if ($(this).hasClass('no-close')) return; // this is a no-close modal
|
||||
modal.hide(this); |
||||
}); |
||||
|
||||
$('.Dialog').on('click', function (e) { |
||||
e.stopImmediatePropagation(); |
||||
}); |
||||
|
||||
// Hide all modals on esc
|
||||
$(window).on('keydown', function (e) { |
||||
if (e.which == 27) { |
||||
modal.hide('.Modal'); |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
window.Modal = modal; |
||||
})(); |
@ -1,32 +0,0 @@ |
||||
(function (nt) { |
||||
var sel = '#notif'; |
||||
|
||||
var hideTmeo1; // timeout to start hiding (transition)
|
||||
var hideTmeo2; // timeout to add the hidden class
|
||||
|
||||
nt.show = function (message, timeout) { |
||||
$(sel).html(message); |
||||
Modal.show(sel); |
||||
|
||||
clearTimeout(hideTmeo1); |
||||
clearTimeout(hideTmeo2); |
||||
|
||||
if (undef(timeout)) timeout = 2500; |
||||
|
||||
hideTmeo1 = setTimeout(nt.hide, timeout); |
||||
}; |
||||
|
||||
nt.hide = function () { |
||||
var $m = $(sel); |
||||
$m.removeClass('visible'); |
||||
hideTmeo2 = setTimeout(function () { |
||||
$m.addClass('hidden'); |
||||
}, 250); // transition time
|
||||
}; |
||||
|
||||
nt.init = function() { |
||||
$(sel).on('click', function() { |
||||
nt.hide(this); |
||||
}); |
||||
}; |
||||
})(window.Notify = {}); |
@ -1,6 +0,0 @@ |
||||
/** Init the terminal sub-module - called from HTML */ |
||||
window.termInit = function () { |
||||
Conn.init(); |
||||
Input.init(); |
||||
TermUpl.init(); |
||||
}; |
@ -1,134 +0,0 @@ |
||||
/** Handle connections */ |
||||
var Conn = (function () { |
||||
var ws; |
||||
var heartbeatTout; |
||||
var pingIv; |
||||
var xoff = false; |
||||
var autoXoffTout; |
||||
var reconTout; |
||||
|
||||
var pageShown = false; |
||||
|
||||
function onOpen(evt) { |
||||
console.log("CONNECTED"); |
||||
doSend("i"); |
||||
} |
||||
|
||||
function onClose(evt) { |
||||
console.warn("SOCKET CLOSED, code " + evt.code + ". Reconnecting..."); |
||||
clearTimeout(reconTout); |
||||
reconTout = setTimeout(function () { |
||||
init(); |
||||
}, 2000); |
||||
// this happens when the buffer gets fucked up via invalid unicode.
|
||||
// we basically use polling instead of socket then
|
||||
} |
||||
|
||||
function onMessage(evt) { |
||||
try { |
||||
// . = heartbeat
|
||||
switch (evt.data.charAt(0)) { |
||||
case 'B': |
||||
case 'T': |
||||
case 'S': |
||||
Screen.load(evt.data); |
||||
if(!pageShown) { |
||||
showPage(); |
||||
pageShown = true; |
||||
} |
||||
break; |
||||
|
||||
case '-': |
||||
//console.log('xoff');
|
||||
xoff = true; |
||||
autoXoffTout = setTimeout(function () { |
||||
xoff = false; |
||||
}, 250); |
||||
break; |
||||
|
||||
case '+': |
||||
//console.log('xon');
|
||||
xoff = false; |
||||
clearTimeout(autoXoffTout); |
||||
break; |
||||
} |
||||
heartbeat(); |
||||
} catch (e) { |
||||
console.error(e); |
||||
} |
||||
} |
||||
|
||||
function canSend() { |
||||
return !xoff; |
||||
} |
||||
|
||||
function doSend(message) { |
||||
if (_demo) { |
||||
console.log("TX: ", message); |
||||
return true; // Simulate success
|
||||
} |
||||
if (xoff) { |
||||
// TODO queue
|
||||
console.log("Can't send, flood control."); |
||||
return false; |
||||
} |
||||
|
||||
if (!ws) return false; // for dry testing
|
||||
if (ws.readyState != 1) { |
||||
console.error("Socket not ready"); |
||||
return false; |
||||
} |
||||
if (typeof message != "string") { |
||||
message = JSON.stringify(message); |
||||
} |
||||
ws.send(message); |
||||
return true; |
||||
} |
||||
|
||||
function init() { |
||||
if (_demo) { |
||||
console.log("Demo mode!"); |
||||
Screen.load(_demo_screen); |
||||
showPage(); |
||||
return; |
||||
} |
||||
|
||||
clearTimeout(reconTout); |
||||
clearTimeout(heartbeatTout); |
||||
|
||||
ws = new WebSocket("ws://" + _root + "/term/update.ws"); |
||||
ws.onopen = onOpen; |
||||
ws.onclose = onClose; |
||||
ws.onmessage = onMessage; |
||||
console.log("Opening socket."); |
||||
heartbeat(); |
||||
} |
||||
|
||||
function heartbeat() { |
||||
clearTimeout(heartbeatTout); |
||||
heartbeatTout = setTimeout(heartbeatFail, 2000); |
||||
} |
||||
|
||||
function heartbeatFail() { |
||||
console.error("Heartbeat lost, probing server..."); |
||||
pingIv = setInterval(function () { |
||||
console.log("> ping"); |
||||
$.get('http://' + _root + '/system/ping', function (resp, status) { |
||||
if (status == 200) { |
||||
clearInterval(pingIv); |
||||
console.info("Server ready, reloading page..."); |
||||
location.reload(); |
||||
} |
||||
}, { |
||||
timeout: 100, |
||||
}); |
||||
}, 1000); |
||||
} |
||||
|
||||
return { |
||||
ws: null, |
||||
init: init, |
||||
send: doSend, |
||||
canSend: canSend, // check flood control
|
||||
}; |
||||
})(); |
@ -1,264 +0,0 @@ |
||||
/** |
||||
* User input |
||||
* |
||||
* --- Rx messages: --- |
||||
* S - screen content (binary encoding of the entire screen with simple compression) |
||||
* T - text labels - Title and buttons, \0x01-separated |
||||
* B - beep |
||||
* . - heartbeat |
||||
* |
||||
* --- Tx messages --- |
||||
* s - string |
||||
* b - action button |
||||
* p - mb press |
||||
* r - mb release |
||||
* m - mouse move |
||||
*/ |
||||
var Input = (function() { |
||||
var opts = { |
||||
np_alt: false, |
||||
cu_alt: false, |
||||
fn_alt: false, |
||||
mt_click: false, |
||||
mt_move: false, |
||||
no_keys: false, |
||||
crlf_mode: false, |
||||
}; |
||||
|
||||
/** Send a literal message */ |
||||
function sendStrMsg(str) { |
||||
return Conn.send("s"+str); |
||||
} |
||||
|
||||
/** Send a button event */ |
||||
function sendBtnMsg(n) { |
||||
Conn.send("b"+Chr(n)); |
||||
} |
||||
|
||||
/** Fn alt choice for key message */ |
||||
function fa(alt, normal) { |
||||
return opts.fn_alt ? alt : normal; |
||||
} |
||||
|
||||
/** Cursor alt choice for key message */ |
||||
function ca(alt, normal) { |
||||
return opts.cu_alt ? alt : normal; |
||||
} |
||||
|
||||
/** Numpad alt choice for key message */ |
||||
function na(alt, normal) { |
||||
return opts.np_alt ? alt : normal; |
||||
} |
||||
|
||||
function _bindFnKeys() { |
||||
var keymap = { |
||||
'tab': '\x09', |
||||
'backspace': '\x08', |
||||
'enter': opts.crlf_mode ? '\x0d\x0a' : '\x0d', |
||||
'ctrl+enter': '\x0a', |
||||
'esc': '\x1b', |
||||
'up': ca('\x1bOA', '\x1b[A'), |
||||
'down': ca('\x1bOB', '\x1b[B'), |
||||
'right': ca('\x1bOC', '\x1b[C'), |
||||
'left': ca('\x1bOD', '\x1b[D'), |
||||
'home': ca('\x1bOH', fa('\x1b[H', '\x1b[1~')), |
||||
'insert': '\x1b[2~', |
||||
'delete': '\x1b[3~', |
||||
'end': ca('\x1bOF', fa('\x1b[F', '\x1b[4~')), |
||||
'pageup': '\x1b[5~', |
||||
'pagedown': '\x1b[6~', |
||||
'f1': fa('\x1bOP', '\x1b[11~'), |
||||
'f2': fa('\x1bOQ', '\x1b[12~'), |
||||
'f3': fa('\x1bOR', '\x1b[13~'), |
||||
'f4': fa('\x1bOS', '\x1b[14~'), |
||||
'f5': '\x1b[15~', // note the disconnect
|
||||
'f6': '\x1b[17~', |
||||
'f7': '\x1b[18~', |
||||
'f8': '\x1b[19~', |
||||
'f9': '\x1b[20~', |
||||
'f10': '\x1b[21~', // note the disconnect
|
||||
'f11': '\x1b[23~', |
||||
'f12': '\x1b[24~', |
||||
'shift+f1': fa('\x1bO1;2P', '\x1b[25~'), |
||||
'shift+f2': fa('\x1bO1;2Q', '\x1b[26~'), // note the disconnect
|
||||
'shift+f3': fa('\x1bO1;2R', '\x1b[28~'), |
||||
'shift+f4': fa('\x1bO1;2S', '\x1b[29~'), // note the disconnect
|
||||
'shift+f5': fa('\x1b[15;2~', '\x1b[31~'), |
||||
'shift+f6': fa('\x1b[17;2~', '\x1b[32~'), |
||||
'shift+f7': fa('\x1b[18;2~', '\x1b[33~'), |
||||
'shift+f8': fa('\x1b[19;2~', '\x1b[34~'), |
||||
'shift+f9': fa('\x1b[20;2~', '\x1b[35~'), // 35-38 are not standard - but what is?
|
||||
'shift+f10': fa('\x1b[21;2~', '\x1b[36~'), |
||||
'shift+f11': fa('\x1b[22;2~', '\x1b[37~'), |
||||
'shift+f12': fa('\x1b[23;2~', '\x1b[38~'), |
||||
'np_0': na('\x1bOp', '0'), |
||||
'np_1': na('\x1bOq', '1'), |
||||
'np_2': na('\x1bOr', '2'), |
||||
'np_3': na('\x1bOs', '3'), |
||||
'np_4': na('\x1bOt', '4'), |
||||
'np_5': na('\x1bOu', '5'), |
||||
'np_6': na('\x1bOv', '6'), |
||||
'np_7': na('\x1bOw', '7'), |
||||
'np_8': na('\x1bOx', '8'), |
||||
'np_9': na('\x1bOy', '9'), |
||||
'np_mul': na('\x1bOR', '*'), |
||||
'np_add': na('\x1bOl', '+'), |
||||
'np_sub': na('\x1bOS', '-'), |
||||
'np_point': na('\x1bOn', '.'), |
||||
'np_div': na('\x1bOQ', '/'), |
||||
// we don't implement numlock key (should change in numpad_alt mode, but it's even more useless than the rest)
|
||||
}; |
||||
|
||||
for (var k in keymap) { |
||||
if (keymap.hasOwnProperty(k)) { |
||||
bind(k, keymap[k]); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** Bind a keystroke to message */ |
||||
function bind(combo, str) { |
||||
// mac fix - allow also cmd
|
||||
if (combo.indexOf('ctrl+') !== -1) { |
||||
combo += ',' + combo.replace('ctrl', 'command'); |
||||
} |
||||
|
||||
// unbind possible old binding
|
||||
key.unbind(combo); |
||||
|
||||
key(combo, function (e) { |
||||
if (opts.no_keys) return; |
||||
e.preventDefault(); |
||||
sendStrMsg(str) |
||||
}); |
||||
} |
||||
|
||||
/** Bind/rebind key messages */ |
||||
function _initKeys() { |
||||
// This takes care of text characters typed
|
||||
window.addEventListener('keypress', function(evt) { |
||||
if (opts.no_keys) return; |
||||
var str = ''; |
||||
if (evt.key) str = evt.key; |
||||
else if (evt.which) str = String.fromCodePoint(evt.which); |
||||
if (str.length>0 && str.charCodeAt(0) >= 32) { |
||||
// console.log("Typed ", str);
|
||||
sendStrMsg(str); |
||||
} |
||||
}); |
||||
|
||||
// ctrl-letter codes are sent as simple low ASCII codes
|
||||
for (var i = 1; i<=26;i++) { |
||||
bind('ctrl+' + String.fromCharCode(96+i), String.fromCharCode(i)); |
||||
} |
||||
bind('ctrl+]', '\x1b'); // alternate way to enter ESC
|
||||
bind('ctrl+\\', '\x1c'); |
||||
bind('ctrl+[', '\x1d'); |
||||
bind('ctrl+^', '\x1e'); |
||||
bind('ctrl+_', '\x1f'); |
||||
|
||||
_bindFnKeys(); |
||||
} |
||||
|
||||
// mouse button states
|
||||
var mb1 = 0; |
||||
var mb2 = 0; |
||||
var mb3 = 0; |
||||
|
||||
/** Init the Input module */ |
||||
function init() { |
||||
_initKeys(); |
||||
|
||||
// Button presses
|
||||
$('#action-buttons button').forEach(function(s) { |
||||
s.addEventListener('click', function() { |
||||
sendBtnMsg(+this.dataset['n']); |
||||
}); |
||||
}); |
||||
|
||||
// global mouse state tracking - for motion reporting
|
||||
window.addEventListener('mousedown', function(evt) { |
||||
if (evt.button == 0) mb1 = 1; |
||||
if (evt.button == 1) mb2 = 1; |
||||
if (evt.button == 2) mb3 = 1; |
||||
}); |
||||
|
||||
window.addEventListener('mouseup', function(evt) { |
||||
if (evt.button == 0) mb1 = 0; |
||||
if (evt.button == 1) mb2 = 0; |
||||
if (evt.button == 2) mb3 = 0; |
||||
}); |
||||
} |
||||
|
||||
/** Prepare modifiers byte for mouse message */ |
||||
function packModifiersForMouse() { |
||||
return (key.isModifier('ctrl')?1:0) | |
||||
(key.isModifier('shift')?2:0) | |
||||
(key.isModifier('alt')?4:0) | |
||||
(key.isModifier('meta')?8:0); |
||||
} |
||||
|
||||
return { |
||||
/** Init the Input module */ |
||||
init: init, |
||||
|
||||
/** Send a literal string message */ |
||||
sendString: sendStrMsg, |
||||
|
||||
/** Enable alternate key modes (cursors, numpad, fn) */ |
||||
setAlts: function(cu, np, fn, crlf) { |
||||
if (opts.cu_alt != cu || opts.np_alt != np || opts.fn_alt != fn || opts.crlf_mode != crlf) { |
||||
opts.cu_alt = cu; |
||||
opts.np_alt = np; |
||||
opts.fn_alt = fn; |
||||
opts.crlf_mode = crlf; |
||||
|
||||
// rebind keys - codes have changed
|
||||
_bindFnKeys(); |
||||
} |
||||
}, |
||||
|
||||
setMouseMode: function(click, move) { |
||||
opts.mt_click = click; |
||||
opts.mt_move = move; |
||||
}, |
||||
|
||||
// Mouse events
|
||||
onMouseMove: function (x, y) { |
||||
if (!opts.mt_move) return; |
||||
var b = mb1 ? 1 : mb2 ? 2 : mb3 ? 3 : 0; |
||||
var m = packModifiersForMouse(); |
||||
Conn.send("m" + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)); |
||||
}, |
||||
onMouseDown: function (x, y, b) { |
||||
if (!opts.mt_click) return; |
||||
if (b > 3 || b < 1) return; |
||||
var m = packModifiersForMouse(); |
||||
Conn.send("p" + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)); |
||||
// console.log("B ",b," M ",m);
|
||||
}, |
||||
onMouseUp: function (x, y, b) { |
||||
if (!opts.mt_click) return; |
||||
if (b > 3 || b < 1) return; |
||||
var m = packModifiersForMouse(); |
||||
Conn.send("r" + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)); |
||||
// console.log("B ",b," M ",m);
|
||||
}, |
||||
onMouseWheel: function (x, y, dir) { |
||||
if (!opts.mt_click) return; |
||||
// -1 ... btn 4 (away from user)
|
||||
// +1 ... btn 5 (towards user)
|
||||
var m = packModifiersForMouse(); |
||||
var b = (dir < 0 ? 4 : 5); |
||||
Conn.send("p" + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)); |
||||
// console.log("B ",b," M ",m);
|
||||
}, |
||||
mouseTracksClicks: function() { |
||||
return opts.mt_click; |
||||
}, |
||||
blockKeys: function(yes) { |
||||
opts.no_keys = yes; |
||||
} |
||||
}; |
||||
})(); |
||||
|
@ -1,380 +0,0 @@ |
||||
var Screen = (function () { |
||||
var W = 0, H = 0; // dimensions
|
||||
var inited = false; |
||||
|
||||
var cursor = { |
||||
a: false, // active (blink state)
|
||||
x: 0, // 0-based coordinates
|
||||
y: 0, |
||||
fg: 7, // colors 0-15
|
||||
bg: 0, |
||||
attrs: 0, |
||||
suppress: false, // do not turn on in blink interval (for safe moving)
|
||||
forceOn: false, // force on unless hanging: used to keep cursor visible during move
|
||||
hidden: false, // do not show (DEC opt)
|
||||
hanging: false, // cursor at column "W+1" - not visible
|
||||
}; |
||||
|
||||
var screen = []; |
||||
var blinkIval; |
||||
var cursorFlashStartIval; |
||||
|
||||
// Some non-bold Fraktur symbols are outside the contiguous block
|
||||
var frakturExceptions = { |
||||
'C': '\u212d', |
||||
'H': '\u210c', |
||||
'I': '\u2111', |
||||
'R': '\u211c', |
||||
'Z': '\u2128', |
||||
}; |
||||
|
||||
// for BEL
|
||||
var audioCtx = null; |
||||
try { |
||||
audioCtx = new (window.AudioContext || window.audioContext || window.webkitAudioContext)(); |
||||
} catch (er) { |
||||
console.error("No AudioContext!", er); |
||||
} |
||||
|
||||
/** Get cell under cursor */ |
||||
function _curCell() { |
||||
return screen[cursor.y*W + cursor.x]; |
||||
} |
||||
|
||||
/** Safely move cursor */ |
||||
function cursorSet(y, x) { |
||||
// Hide and prevent from showing up during the move
|
||||
cursor.suppress = true; |
||||
_draw(_curCell(), false); |
||||
cursor.x = x; |
||||
cursor.y = y; |
||||
// Show again
|
||||
cursor.suppress = false; |
||||
_draw(_curCell()); |
||||
} |
||||
|
||||
function alpha2fraktur(t) { |
||||
// perform substitution
|
||||
if (t >= 'a' && t <= 'z') { |
||||
t = String.fromCodePoint(0x1d51e - 97 + t.charCodeAt(0)); |
||||
} |
||||
else if (t >= 'A' && t <= 'Z') { |
||||
// this set is incomplete, some exceptions are needed
|
||||
if (frakturExceptions.hasOwnProperty(t)) { |
||||
t = frakturExceptions[t]; |
||||
} else { |
||||
t = String.fromCodePoint(0x1d504 - 65 + t.charCodeAt(0)); |
||||
} |
||||
} |
||||
return t; |
||||
} |
||||
|
||||
/** Update cell on display. inv = invert (for cursor) */ |
||||
function _draw(cell, inv) { |
||||
if (!cell) return; |
||||
if (typeof inv == 'undefined') { |
||||
inv = cursor.a && cursor.x == cell.x && cursor.y == cell.y; |
||||
} |
||||
|
||||
var fg, bg, cn, t; |
||||
|
||||
fg = inv ? cell.bg : cell.fg; |
||||
bg = inv ? cell.fg : cell.bg; |
||||
|
||||
t = cell.t; |
||||
if (!t.length) t = ' '; |
||||
|
||||
cn = 'fg' + fg + ' bg' + bg; |
||||
if (cell.attrs & (1<<0)) cn += ' bold'; |
||||
if (cell.attrs & (1<<1)) cn += ' faint'; |
||||
if (cell.attrs & (1<<2)) cn += ' italic'; |
||||
if (cell.attrs & (1<<3)) cn += ' under'; |
||||
if (cell.attrs & (1<<4)) cn += ' blink'; |
||||
if (cell.attrs & (1<<5)) { |
||||
cn += ' fraktur'; |
||||
t = alpha2fraktur(t); |
||||
} |
||||
if (cell.attrs & (1<<6)) cn += ' strike'; |
||||
|
||||
cell.slot.textContent = t; |
||||
cell.elem.className = cn; |
||||
} |
||||
|
||||
/** Show entire screen */ |
||||
function _drawAll() { |
||||
for (var i = W*H-1; i>=0; i--) { |
||||
_draw(screen[i]); |
||||
} |
||||
} |
||||
|
||||
function _rebuild(rows, cols) { |
||||
W = cols; |
||||
H = rows; |
||||
|
||||
/* Build screen & show */ |
||||
var cOuter, cInner, cell, screenDiv = qs('#screen'); |
||||
|
||||
// Empty the screen node
|
||||
while (screenDiv.firstChild) screenDiv.removeChild(screenDiv.firstChild); |
||||
|
||||
screen = []; |
||||
|
||||
for(var i = 0; i < W*H; i++) { |
||||
cOuter = mk('span'); |
||||
cInner = mk('span'); |
||||
|
||||
/* Mouse tracking */ |
||||
(function() { |
||||
var x = i % W; |
||||
var y = Math.floor(i / W); |
||||
cOuter.addEventListener('mouseenter', function (evt) { |
||||
Input.onMouseMove(x, y); |
||||
}); |
||||
cOuter.addEventListener('mousedown', function (evt) { |
||||
Input.onMouseDown(x, y, evt.button+1); |
||||
}); |
||||
cOuter.addEventListener('mouseup', function (evt) { |
||||
Input.onMouseUp(x, y, evt.button+1); |
||||
}); |
||||
cOuter.addEventListener('contextmenu', function (evt) { |
||||
if (Input.mouseTracksClicks()) { |
||||
evt.preventDefault(); |
||||
} |
||||
}); |
||||
cOuter.addEventListener('mousewheel', function (evt) { |
||||
Input.onMouseWheel(x, y, evt.deltaY>0?1:-1); |
||||
return false; |
||||
}); |
||||
})(); |
||||
|
||||
/* End of line */ |
||||
if ((i > 0) && (i % W == 0)) { |
||||
screenDiv.appendChild(mk('br')); |
||||
} |
||||
/* The cell */ |
||||
cOuter.appendChild(cInner); |
||||
screenDiv.appendChild(cOuter); |
||||
|
||||
cell = { |
||||
t: ' ', |
||||
fg: 7, |
||||
bg: 0, // the colors will be replaced immediately as we receive data (user won't see this)
|
||||
attrs: 0, |
||||
elem: cOuter, |
||||
slot: cInner, |
||||
x: i % W, |
||||
y: Math.floor(i / W), |
||||
}; |
||||
screen.push(cell); |
||||
_draw(cell); |
||||
} |
||||
} |
||||
|
||||
/** Init the terminal */ |
||||
function _init() { |
||||
/* Cursor blinking */ |
||||
clearInterval(blinkIval); |
||||
blinkIval = setInterval(function () { |
||||
cursor.a = !cursor.a; |
||||
if (cursor.hidden || cursor.hanging) { |
||||
cursor.a = false; |
||||
} |
||||
|
||||
if (!cursor.suppress) { |
||||
_draw(_curCell(), cursor.forceOn || cursor.a); |
||||
} |
||||
}, 500); |
||||
|
||||
/* blink attribute animation */ |
||||
setInterval(function () { |
||||
$('#screen').removeClass('blink-hide'); |
||||
setTimeout(function () { |
||||
$('#screen').addClass('blink-hide'); |
||||
}, 800); // 200 ms ON
|
||||
}, 1000); |
||||
|
||||
inited = true; |
||||
} |
||||
|
||||
// constants for decoding the update blob
|
||||
var SEQ_SET_COLOR_ATTR = 1; |
||||
var SEQ_REPEAT = 2; |
||||
var SEQ_SET_COLOR = 3; |
||||
var SEQ_SET_ATTR = 4; |
||||
|
||||
/** Parse received screen update object (leading S removed already) */ |
||||
function _load_content(str) { |
||||
var i = 0, ci = 0, j, jc, num, num2, t = ' ', fg, bg, attrs, cell; |
||||
|
||||
if (!inited) _init(); |
||||
|
||||
var cursorMoved; |
||||
|
||||
// Set size
|
||||
num = parse2B(str, i); i += 2; // height
|
||||
num2 = parse2B(str, i); i += 2; // width
|
||||
if (num != H || num2 != W) { |
||||
_rebuild(num, num2); |
||||
} |
||||
// console.log("Size ",num, num2);
|
||||
|
||||
// Cursor position
|
||||
num = parse2B(str, i); i += 2; // row
|
||||
num2 = parse2B(str, i); i += 2; // col
|
||||
cursorMoved = (cursor.x != num2 || cursor.y != num); |
||||
cursorSet(num, num2); |
||||
// console.log("Cursor at ",num, num2);
|
||||
|
||||
// Attributes
|
||||
num = parse3B(str, i); i += 3; |
||||
cursor.hidden = !(num & (1<<0)); // DEC opt "visible"
|
||||
cursor.hanging = !!(num & (1<<1)); |
||||
|
||||
Input.setAlts( |
||||
!!(num & (1<<2)), // cursors alt
|
||||
!!(num & (1<<3)), // numpad alt
|
||||
!!(num & (1<<4)), // fn keys alt
|
||||
!!(num & (1<<12)) // crlf mode
|
||||
); |
||||
|
||||
var mt_click = !!(num & (1<<5)); |
||||
var mt_move = !!(num & (1<<6)); |
||||
Input.setMouseMode( |
||||
mt_click, |
||||
mt_move |
||||
); |
||||
$('#screen').toggleClass('noselect', mt_move); |
||||
|
||||
var show_buttons = !!(num & (1<<7)); |
||||
var show_config_links = !!(num & (1<<8)); |
||||
$('.x-term-conf-btn').toggleClass('hidden', !show_config_links); |
||||
$('#action-buttons').toggleClass('hidden', !show_buttons); |
||||
|
||||
// bits 9-11 are cursor shape (not implemented)
|
||||
|
||||
fg = 7; |
||||
bg = 0; |
||||
attrs = 0; |
||||
|
||||
// Here come the content
|
||||
while(i < str.length && ci<W*H) { |
||||
|
||||
j = str[i++]; |
||||
jc = j.charCodeAt(0); |
||||
if (jc == SEQ_SET_COLOR_ATTR) { |
||||
num = parse3B(str, i); i += 3; |
||||
fg = num & 0x0F; |
||||
bg = (num & 0xF0) >> 4; |
||||
attrs = (num & 0xFF00)>>8; |
||||
} |
||||
else if (jc == SEQ_SET_COLOR) { |
||||
num = parse2B(str, i); i += 2; |
||||
fg = num & 0x0F; |
||||
bg = (num & 0xF0) >> 4; |
||||
} |
||||
else if (jc == SEQ_SET_ATTR) { |
||||
num = parse2B(str, i); i += 2; |
||||
attrs = num & 0xFF; |
||||
} |
||||
else if (jc == SEQ_REPEAT) { |
||||
num = parse2B(str, i); i += 2; |
||||
// console.log("Repeat x ",num);
|
||||
for (; num>0 && ci<W*H; num--) { |
||||
cell = screen[ci++]; |
||||
cell.fg = fg; |
||||
cell.bg = bg; |
||||
cell.t = t; |
||||
cell.attrs = attrs; |
||||
} |
||||
} |
||||
else { |
||||
cell = screen[ci++]; |
||||
// Unique cell character
|
||||
t = cell.t = j; |
||||
cell.fg = fg; |
||||
cell.bg = bg; |
||||
cell.attrs = attrs; |
||||
// console.log("Symbol ", j);
|
||||
} |
||||
} |
||||
|
||||
_drawAll(); |
||||
|
||||
// if (!cursor.hidden || cursor.hanging || !cursor.suppress) {
|
||||
// // hide cursor asap
|
||||
// _draw(_curCell(), false);
|
||||
// }
|
||||
|
||||
if (cursorMoved) { |
||||
cursor.forceOn = true; |
||||
cursorFlashStartIval = setTimeout(function() { |
||||
cursor.forceOn = false; |
||||
}, 1200); |
||||
_draw(_curCell(), true); |
||||
} |
||||
} |
||||
|
||||
/** Apply labels to buttons and screen title (leading T removed already) */ |
||||
function _load_labels(str) { |
||||
var pieces = str.split('\x01'); |
||||
qs('h1').textContent = pieces[0]; |
||||
$('#action-buttons button').forEach(function(x, i) { |
||||
var s = pieces[i+1].trim(); |
||||
// if empty string, use the "dim" effect and put nbsp instead to stretch the btn vertically
|
||||
x.innerHTML = s.length > 0 ? e(s) : " "; |
||||
x.style.opacity = s.length > 0 ? 1 : 0.2; |
||||
}) |
||||
} |
||||
|
||||
/** Audible beep for ASCII 7 */ |
||||
function _beep() { |
||||
var osc, gain; |
||||
if (!audioCtx) return; |
||||
|
||||
// Main beep
|
||||
osc = audioCtx.createOscillator(); |
||||
gain = audioCtx.createGain(); |
||||
osc.connect(gain); |
||||
gain.connect(audioCtx.destination); |
||||
gain.gain.value = 0.5; |
||||
osc.frequency.value = 750; |
||||
osc.type = 'sine'; |
||||
osc.start(); |
||||
osc.stop(audioCtx.currentTime+0.05); |
||||
|
||||
// Surrogate beep (making it sound like 'oops')
|
||||
osc = audioCtx.createOscillator(); |
||||
gain = audioCtx.createGain(); |
||||
osc.connect(gain); |
||||
gain.connect(audioCtx.destination); |
||||
gain.gain.value = 0.2; |
||||
osc.frequency.value = 400; |
||||
osc.type = 'sine'; |
||||
osc.start(audioCtx.currentTime+0.05); |
||||
osc.stop(audioCtx.currentTime+0.08); |
||||
} |
||||
|
||||
/** Load screen content from a binary sequence (new) */ |
||||
function load(str) { |
||||
//console.log(JSON.stringify(str));
|
||||
var content = str.substr(1); |
||||
switch(str.charAt(0)) { |
||||
case 'S': |
||||
_load_content(content); |
||||
break; |
||||
case 'T': |
||||
_load_labels(content); |
||||
break; |
||||
case 'B': |
||||
_beep(); |
||||
break; |
||||
default: |
||||
console.warn("Bad data message type, ignoring."); |
||||
console.log(str); |
||||
} |
||||
} |
||||
|
||||
return { |
||||
load: load, // full load (string)
|
||||
}; |
||||
})(); |
@ -1,146 +0,0 @@ |
||||
/** File upload utility */ |
||||
var TermUpl = (function() { |
||||
var lines, // array of lines without newlines
|
||||
line_i, // current line index
|
||||
fuTout, // timeout handle for line sending
|
||||
send_delay_ms, // delay between lines (ms)
|
||||
nl_str, // newline string to use
|
||||
curLine, // current line (when using fuOil)
|
||||
inline_pos; // Offset in line (for long lines)
|
||||
|
||||
// lines longer than this are split to chunks
|
||||
// sending a super-ling string through the socket is not a good idea
|
||||
var MAX_LINE_LEN = 128; |
||||
|
||||
function fuOpen() { |
||||
fuStatus("Ready..."); |
||||
Modal.show('#fu_modal', onClose); |
||||
$('#fu_form').toggleClass('busy', false); |
||||
Input.blockKeys(true); |
||||
} |
||||
|
||||
function onClose() { |
||||
console.log("Upload modal closed."); |
||||
clearTimeout(fuTout); |
||||
line_i = 0; |
||||
Input.blockKeys(false); |
||||
} |
||||
|
||||
function fuStatus(msg) { |
||||
qs('#fu_prog').textContent = msg; |
||||
} |
||||
|
||||
function fuSend() { |
||||
var v = qs('#fu_text').value; |
||||
if (!v.length) { |
||||
fuClose(); |
||||
return; |
||||
} |
||||
|
||||
lines = v.split('\n'); |
||||
line_i = 0; |
||||
inline_pos = 0; // offset in line
|
||||
send_delay_ms = qs('#fu_delay').value; |
||||
|
||||
// sanitize - 0 causes overflows
|
||||
if (send_delay_ms < 0) { |
||||
send_delay_ms = 0; |
||||
qs('#fu_delay').value = send_delay_ms; |
||||
} |
||||
|
||||
nl_str = { |
||||
'CR': '\r', |
||||
'LF': '\n', |
||||
'CRLF': '\r\n', |
||||
}[qs('#fu_crlf').value]; |
||||
|
||||
$('#fu_form').toggleClass('busy', true); |
||||
fuStatus("Starting..."); |
||||
fuSendLine(); |
||||
} |
||||
|
||||
function fuSendLine() { |
||||
if (!$('#fu_modal').hasClass('visible')) { |
||||
// Modal is closed, cancel
|
||||
return; |
||||
} |
||||
|
||||
if (!Conn.canSend()) { |
||||
// postpone
|
||||
fuTout = setTimeout(fuSendLine, 1); |
||||
return; |
||||
} |
||||
|
||||
if (inline_pos == 0) { |
||||
curLine = lines[line_i++] + nl_str; |
||||
} |
||||
|
||||
var chunk; |
||||
if ((curLine.length - inline_pos) <= MAX_LINE_LEN) { |
||||
chunk = curLine.substr(inline_pos, MAX_LINE_LEN); |
||||
inline_pos = 0; |
||||
} else { |
||||
chunk = curLine.substr(inline_pos, MAX_LINE_LEN); |
||||
inline_pos += MAX_LINE_LEN; |
||||
} |
||||
|
||||
if (!Input.sendString(chunk)) { |
||||
fuStatus("FAILED!"); |
||||
return; |
||||
} |
||||
|
||||
var all = lines.length; |
||||
|
||||
fuStatus(line_i+" / "+all+ " ("+(Math.round((line_i/all)*1000)/10)+"%)"); |
||||
|
||||
if (lines.length > line_i || inline_pos > 0) { |
||||
fuTout = setTimeout(fuSendLine, send_delay_ms); |
||||
} else { |
||||
closeWhenReady(); |
||||
} |
||||
} |
||||
|
||||
function closeWhenReady() { |
||||
if (!Conn.canSend()) { |
||||
// stuck in XOFF still, wait to process...
|
||||
fuStatus("Waiting for Tx buffer..."); |
||||
setTimeout(closeWhenReady, 100); |
||||
} else { |
||||
fuStatus("Done."); |
||||
// delay to show it
|
||||
setTimeout(function() { |
||||
fuClose(); |
||||
}, 100); |
||||
} |
||||
} |
||||
|
||||
function fuClose() { |
||||
Modal.hide('#fu_modal'); |
||||
} |
||||
|
||||
return { |
||||
init: function() { |
||||
qs('#fu_file').addEventListener('change', function (evt) { |
||||
var reader = new FileReader(); |
||||
var file = evt.target.files[0]; |
||||
console.log("Selected file type: "+file.type); |
||||
if (!file.type.match(/text\/.*|application\/(json|csv|.*xml.*|.*script.*)/)) { |
||||
// Deny load of blobs like img - can crash browser and will get corrupted anyway
|
||||
if (!confirm("This does not look like a text file: "+file.type+"\nReally load?")) { |
||||
qs('#fu_file').value = ''; |
||||
return; |
||||
} |
||||
} |
||||
reader.onload = function(e) { |
||||
var txt = e.target.result.replace(/[\r\n]+/,'\n'); |
||||
qs('#fu_text').value = txt; |
||||
}; |
||||
console.log("Loading file..."); |
||||
reader.readAsText(file); |
||||
}, false); |
||||
}, |
||||
close: fuClose, |
||||
start: fuSend, |
||||
open: fuOpen, |
||||
} |
||||
})(); |
@ -1,161 +0,0 @@ |
||||
/** Make a node */ |
||||
function mk(e) {return document.createElement(e)} |
||||
|
||||
/** Find one by query */ |
||||
function qs(s) {return document.querySelector(s)} |
||||
|
||||
/** Find all by query */ |
||||
function qsa(s) {return document.querySelectorAll(s)} |
||||
|
||||
/** Convert any to bool safely */ |
||||
function bool(x) { |
||||
return (x === 1 || x === '1' || x === true || x === 'true'); |
||||
} |
||||
|
||||
/** |
||||
* Filter 'spacebar' and 'return' from keypress handler, |
||||
* and when they're pressed, fire the callback. |
||||
* use $(...).on('keypress', cr(handler)) |
||||
*/ |
||||
function cr(hdl) { |
||||
return function(e) { |
||||
if (e.which == 10 || e.which == 13 || e.which == 32) { |
||||
hdl(); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
/** Extend an objects with options */ |
||||
function extend(defaults, options) { |
||||
var target = {}; |
||||
|
||||
Object.keys(defaults).forEach(function(k){ |
||||
target[k] = defaults[k]; |
||||
}); |
||||
|
||||
Object.keys(options).forEach(function(k){ |
||||
target[k] = options[k]; |
||||
}); |
||||
|
||||
return target; |
||||
} |
||||
|
||||
/** Escape string for use as literal in RegExp */ |
||||
function rgxe(str) { |
||||
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); |
||||
} |
||||
|
||||
/** Format number to N decimal places, output as string */ |
||||
function numfmt(x, places) { |
||||
var pow = Math.pow(10, places); |
||||
return Math.round(x*pow) / pow; |
||||
} |
||||
|
||||
/** Get millisecond timestamp */ |
||||
function msNow() { |
||||
return +(new Date); |
||||
} |
||||
|
||||
/** Get ms elapsed since msNow() */ |
||||
function msElapsed(start) { |
||||
return msNow() - start; |
||||
} |
||||
|
||||
/** Shim for log base 10 */ |
||||
Math.log10 = Math.log10 || function(x) { |
||||
return Math.log(x) / Math.LN10; |
||||
}; |
||||
|
||||
/** |
||||
* Perform a substitution in the given string. |
||||
* |
||||
* Arguments - array or list of replacements. |
||||
* Arguments numeric keys will replace {0}, {1} etc. |
||||
* Named keys also work, ie. {foo: "bar"} -> replaces {foo} with bar. |
||||
* |
||||
* Braces are added to keys if missing. |
||||
* |
||||
* @returns {String} result |
||||
*/ |
||||
String.prototype.format = function () { |
||||
var out = this; |
||||
var repl = arguments; |
||||
|
||||
if (arguments.length == 1 && (Array.isArray(arguments[0]) || typeof arguments[0] == 'object')) { |
||||
repl = arguments[0]; |
||||
} |
||||
|
||||
for (var ph in repl) { |
||||
if (repl.hasOwnProperty(ph)) { |
||||
var ph_orig = ph; |
||||
|
||||
if (!ph.match(/^\{.*\}$/)) { |
||||
ph = '{' + ph + '}'; |
||||
} |
||||
|
||||
// replace all occurrences
|
||||
var pattern = new RegExp(rgxe(ph), "g"); |
||||
out = out.replace(pattern, repl[ph_orig]); |
||||
} |
||||
} |
||||
|
||||
return out; |
||||
}; |
||||
|
||||
/** HTML escape */ |
||||
function e(str) { |
||||
return $.htmlEscape(str); |
||||
} |
||||
|
||||
/** Check for undefined */ |
||||
function undef(x) { |
||||
return typeof x == 'undefined'; |
||||
} |
||||
|
||||
/** Safe json parse */ |
||||
function jsp(str) { |
||||
try { |
||||
return JSON.parse(str); |
||||
} catch(e) { |
||||
console.error(e); |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
/** Create a character from ASCII code */ |
||||
function Chr(n) { |
||||
return String.fromCharCode(n); |
||||
} |
||||
|
||||
/** Decode number from 2B encoding */ |
||||
function parse2B(s, i) { |
||||
return (s.charCodeAt(i++) - 1) + (s.charCodeAt(i) - 1) * 127; |
||||
} |
||||
|
||||
/** Decode number from 3B encoding */ |
||||
function parse3B(s, i) { |
||||
return (s.charCodeAt(i) - 1) + (s.charCodeAt(i+1) - 1) * 127 + (s.charCodeAt(i+2) - 1) * 127 * 127; |
||||
} |
||||
|
||||
/** Encode using 2B encoding, returns string. */ |
||||
function encode2B(n) { |
||||
var lsb, msb; |
||||
lsb = (n % 127); |
||||
n = ((n - lsb) / 127); |
||||
lsb += 1; |
||||
msb = (n + 1); |
||||
return Chr(lsb) + Chr(msb); |
||||
} |
||||
|
||||
/** Encode using 3B encoding, returns string. */ |
||||
function encode3B(n) { |
||||
var lsb, msb, xsb; |
||||
lsb = (n % 127); |
||||
n = (n - lsb) / 127; |
||||
lsb += 1; |
||||
msb = (n % 127); |
||||
n = (n - msb) / 127; |
||||
msb += 1; |
||||
xsb = (n + 1); |
||||
return Chr(lsb) + Chr(msb) + Chr(xsb); |
||||
} |
@ -1,163 +0,0 @@ |
||||
(function(w) { |
||||
var authStr = ['Open', 'WEP', 'WPA', 'WPA2', 'WPA/WPA2']; |
||||
var curSSID; |
||||
|
||||
// Get XX % for a slider input
|
||||
function rangePt(inp) { |
||||
return Math.round(((inp.value / inp.max)*100)) + '%'; |
||||
} |
||||
|
||||
// Display selected STA SSID etc
|
||||
function selectSta(name, password, ip) { |
||||
$('#sta_ssid').val(name); |
||||
$('#sta_password').val(password); |
||||
|
||||
$('#sta-nw').toggleClass('hidden', name.length == 0); |
||||
$('#sta-nw-nil').toggleClass('hidden', name.length > 0); |
||||
|
||||
$('#sta-nw .essid').html(e(name)); |
||||
var nopw = undef(password) || password.length == 0; |
||||
$('#sta-nw .passwd').toggleClass('hidden', nopw); |
||||
$('#sta-nw .nopasswd').toggleClass('hidden', !nopw); |
||||
$('#sta-nw .ip').html(ip.length>0 ? tr('wifi.connected_ip_is')+ip : tr('wifi.not_conn')); |
||||
} |
||||
|
||||
/** Update display for received response */ |
||||
function onScan(resp, status) { |
||||
//var ap_json = {
|
||||
// "result": {
|
||||
// "inProgress": "0",
|
||||
// "APs": [
|
||||
// {"essid": "Chlivek", "bssid": "88:f7:c7:52:b3:99", "rssi": "204", "enc": "4", "channel": "1"},
|
||||
// {"essid": "TyNikdy", "bssid": "5c:f4:ab:0d:f1:1b", "rssi": "164", "enc": "3", "channel": "1"},
|
||||
// ]
|
||||
// }
|
||||
//};
|
||||
|
||||
if (status != 200) { |
||||
// bad response
|
||||
rescan(5000); // wait 5sm then retry
|
||||
return; |
||||
} |
||||
|
||||
try { |
||||
resp = JSON.parse(resp); |
||||
} catch (e) { |
||||
console.log(e); |
||||
rescan(5000); |
||||
return; |
||||
} |
||||
|
||||
var done = !bool(resp.result.inProgress) && (resp.result.APs.length > 0); |
||||
rescan(done ? 15000 : 1000); |
||||
if (!done) return; // no redraw yet
|
||||
|
||||
// clear the AP list
|
||||
var $list = $('#ap-list'); |
||||
// remove old APs
|
||||
$('#ap-list .AP').remove(); |
||||
|
||||
$list.toggleClass('hidden', !done); |
||||
$('#ap-loader').toggleClass('hidden', done); |
||||
|
||||
// scan done
|
||||
resp.result.APs.sort(function (a, b) { |
||||
return b.rssi - a.rssi; |
||||
}).forEach(function (ap) { |
||||
ap.enc = parseInt(ap.enc); |
||||
|
||||
if (ap.enc > 4) return; // hide unsupported auths
|
||||
|
||||
var item = mk('div'); |
||||
|
||||
var $item = $(item) |
||||
.data('ssid', ap.essid) |
||||
.data('pwd', ap.enc) |
||||
.attr('tabindex', 0) |
||||
.addClass('AP'); |
||||
|
||||
// mark current SSID
|
||||
if (ap.essid == curSSID) { |
||||
$item.addClass('selected'); |
||||
} |
||||
|
||||
var inner = mk('div'); |
||||
$(inner).addClass('inner') |
||||
.htmlAppend('<div class="rssi">{0}</div>'.format(ap.rssi_perc)) |
||||
.htmlAppend('<div class="essid" title="{0}">{0}</div>'.format($.htmlEscape(ap.essid))) |
||||
.htmlAppend('<div class="auth">{0}</div>'.format(authStr[ap.enc])); |
||||
|
||||
$item.on('click', function () { |
||||
var $th = $(this); |
||||
|
||||
var conn_ssid = $th.data('ssid'); |
||||
var conn_pass = ''; |
||||
|
||||
if (+$th.data('pwd')) { |
||||
// this AP needs a password
|
||||
conn_pass = prompt(tr("wifi.enter_passwd").replace(":ssid:", conn_ssid)); |
||||
if (!conn_pass) return; |
||||
} |
||||
|
||||
$('#sta_password').val(conn_pass); |
||||
$('#sta_ssid').val(conn_ssid); |
||||
selectSta(conn_ssid, conn_pass, ''); |
||||
}); |
||||
|
||||
|
||||
item.appendChild(inner); |
||||
$list[0].appendChild(item); |
||||
}); |
||||
} |
||||
|
||||
function startScanning() { |
||||
$('#ap-loader').removeClass('hidden'); |
||||
$('#ap-scan').addClass('hidden'); |
||||
$('#ap-loader .anim-dots').html('.'); |
||||
|
||||
scanAPs(); |
||||
} |
||||
|
||||
/** Ask the CGI what APs are visible (async) */ |
||||
function scanAPs() { |
||||
if (_demo) { |
||||
onScan(_demo_aps, 200); |
||||
} else { |
||||
$.get('http://' + _root + '/cfg/wifi/scan', onScan); |
||||
} |
||||
} |
||||
|
||||
function rescan(time) { |
||||
setTimeout(scanAPs, time); |
||||
} |
||||
|
||||
/** Set up the WiFi page */ |
||||
function wifiInit(cfg) { |
||||
// Update slider value displays
|
||||
$('.Row.range').forEach(function(x) { |
||||
var inp = x.querySelector('input'); |
||||
var disp1 = x.querySelector('.x-disp1'); |
||||
var disp2 = x.querySelector('.x-disp2'); |
||||
var t = rangePt(inp); |
||||
$(disp1).html(t); |
||||
$(disp2).html(t); |
||||
$(inp).on('input', function() { |
||||
t = rangePt(inp); |
||||
$(disp1).html(t); |
||||
$(disp2).html(t); |
||||
}); |
||||
}); |
||||
|
||||
// Forget STA credentials
|
||||
$('#forget-sta').on('click', function() { |
||||
selectSta('', '', ''); |
||||
return false; |
||||
}); |
||||
|
||||
selectSta(cfg.sta_ssid, cfg.sta_password, cfg.sta_active_ip); |
||||
curSSID = cfg.sta_active_ssid; |
||||
} |
||||
|
||||
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,21 @@ |
||||
<?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' => '', |
||||
'menu.ini_export' => '', |
||||
'menu.ini_import' => '', |
||||
]; |
@ -0,0 +1,291 @@ |
||||
<?php |
||||
|
||||
return [ |
||||
'menu.cfg_wifi' => 'Nastavení WiFi', |
||||
'menu.cfg_network' => 'Nastavení sítě', |
||||
'menu.cfg_term' => 'Nastavení terminalu', |
||||
'menu.about' => 'O programu', |
||||
'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čítek', |
||||
'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 Fn klávesy', |
||||
'term.show_config_links' => 'Menu pod obrazovkou', |
||||
'term.show_buttons' => 'Zobrazit tlačítka', |
||||
'term.loopback' => 'Lokální echo (<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' => 'Rozšířené ladění', |
||||
'term.ascii_debug' => 'Ladění vstupních dat', |
||||
'term.backdrop' => 'URL obrázku na pozadí', |
||||
'term.button_count' => 'Počet tlačítek', |
||||
'term.button_colors' => 'Barvy tlačítek', |
||||
'term.font_stack' => 'Font', |
||||
'term.font_size' => 'Velikost písma', |
||||
|
||||
'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.)', |
||||
|
||||
'backup.title' => 'Záloha do souboru', |
||||
'backup.explain' => 'Všechna nastavení kromě systémového hesla je možné uložit do a obnovit z INI souboru.', |
||||
'backup.export' => 'Zálohovat do souboru', |
||||
'backup.import' => 'Nahrát soubor!', |
||||
|
||||
// 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', |
||||
|
||||
'gpio2_config' => 'Funkce GPIO2', |
||||
'gpio4_config' => 'Funkce GPIO4', |
||||
'gpio5_config' => 'Funkce GPIO5', |
||||
'gpio_config.off' => 'Vypnuto', |
||||
'gpio_config.off_2' => 'Debug UART Tx', |
||||
'gpio_config.out_initial0' => 'Výstup (výchozí stav 0)', |
||||
'gpio_config.out_initial1' => 'Výstup (výchozí stav 1)', |
||||
'gpio_config.in_pull' => 'Vstup (s pull-upem)', |
||||
'gpio_config.in_nopull' => 'Vstup (plovoucí)', |
||||
|
||||
// Generic button / dialog labels |
||||
|
||||
'apply' => 'Použít!', |
||||
'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,291 @@ |
||||
<?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', |
||||
'term.backdrop' => 'Hintergrundbild-URL', |
||||
'term.button_count' => 'Tastenanzahl', |
||||
'term.button_colors' => 'Tastenfarben', |
||||
'term.font_stack' => 'Schriftstapel', |
||||
'term.font_size' => 'Schriftgröße', |
||||
|
||||
'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 nicht 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.)', |
||||
|
||||
'backup.title' => 'Konfigurationsdatei sichern', |
||||
'backup.explain' => 'Die ganze Konfiguration außer dem Systempasswort können mit einer INI-Datei gesichert und wiederhergestellt werden.', |
||||
'backup.export' => 'Datei exportieren', |
||||
'backup.import' => 'Importieren!', |
||||
|
||||
// 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' => ' |
||||
Der ESP8266 kann von 80 MHz auf 160 MHz übertaktet werden. |
||||
Alles wird etwas schneller sein, aber mit höherem Stromverbrauch, |
||||
und eventuell auch mit mehr Interferenz. |
||||
Mit Sorgfalt benutzen. |
||||
', |
||||
'hwtuning.overclock' => 'Auf 160MHz übertakten', |
||||
|
||||
'gpio2_config' => 'GPIO2 Funktion', |
||||
'gpio4_config' => 'GPIO4 Funktion', |
||||
'gpio5_config' => 'GPIO5 Funktion', |
||||
'gpio_config.off' => 'Deaktiviert', |
||||
'gpio_config.off_2' => 'UART Tx Debuggen', |
||||
'gpio_config.out_initial0' => 'Output (Anfangslevel 0)', |
||||
'gpio_config.out_initial1' => 'Output (Anfangslevel 1)', |
||||
'gpio_config.in_pull' => 'Input (pull-up)', |
||||
'gpio_config.in_nopull' => 'Input (floating)', |
||||
|
||||
// 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:', |
||||
]; |
@ -0,0 +1,292 @@ |
||||
<?php |
||||
|
||||
return [ |
||||
'menu.cfg_wifi' => 'WiFi Beállítások', |
||||
'menu.cfg_network' => 'Hálózati beállítások', |
||||
'menu.cfg_term' => 'Terminál beállítások', |
||||
'menu.about' => 'Az ESPTerm-ről', |
||||
'menu.help' => 'Gyors referencia', |
||||
'menu.term' => 'Vissza a terminálba', |
||||
'menu.cfg_system' => 'Rendszer beállítások', |
||||
'menu.cfg_wifi_conn' => 'Csatlakozás a hálózathoz', |
||||
'menu.settings' => 'Beállítások', |
||||
|
||||
// Terminal page |
||||
|
||||
'title.term' => 'Terminál', // page title of the terminal page |
||||
|
||||
'term_nav.fullscreen' => 'Teljesképernyő', |
||||
'term_nav.config' => 'Beállítás', |
||||
'term_nav.wifi' => 'WiFi', |
||||
'term_nav.help' => 'Segítség', |
||||
'term_nav.about' => 'Info', |
||||
'term_nav.paste' => 'Beillesztés', |
||||
'term_nav.upload' => 'Feltöltés', |
||||
'term_nav.keybd' => 'Billentyűzet', |
||||
'term_nav.paste_prompt' => 'Szöveg beillesztése és küldése:', |
||||
|
||||
'term_conn.connecting' => 'Csatlakozás', |
||||
'term_conn.waiting_content' => 'Várakozás a csatlakozásra', |
||||
'term_conn.disconnected' => 'Kapcsolat bontva', |
||||
'term_conn.waiting_server' => 'Várakozás a kiszolgálóra', |
||||
'term_conn.reconnecting' => 'Újracsatlakozás', |
||||
|
||||
// Terminal settings page |
||||
|
||||
'term.defaults' => 'Alap beállítások', |
||||
'term.expert' => 'Haladó beállítások', |
||||
'term.explain_initials' => ' |
||||
Ezek az alap beállítások amik az ESPTerm bekapcsolása után, |
||||
vagy amikor képernyő reset parancsa érkezikd (<code>\ec</code>). |
||||
Ezek megváltoztathatóak egy terminál alkalmzás és escape szekveciák segítségével. |
||||
', |
||||
'term.explain_expert' => ' |
||||
Ezek haladó beállítási opciók amiket általában nem kell megváltoztatni. |
||||
Csak akkor változtass rajta ha tudod mit csinálsz!', |
||||
|
||||
'term.example' => 'Alapértelmezet színek előnézete', |
||||
|
||||
'term.explain_scheme' => ' |
||||
Az alapértelmezett szöveg és háttér szín kiválasztásához kattints a |
||||
paletta előnézet gombra. Alternatíva: használd a 0-15 számokat a téma színekhez, |
||||
16-255 számokat a normál színekhez és hexa (#FFFFFF) a True Color (24-bit) színekhez. |
||||
', |
||||
|
||||
'term.fgbg_presets' => 'Alapértelmezett beállítások', |
||||
'term.color_scheme' => 'Szín séma', |
||||
'term.reset_screen' => 'A képernyő olvasó alapállapotba állítása', |
||||
'term.term_title' => 'Fejléc szöveg', |
||||
'term.term_width' => 'Szélesség', |
||||
'term.term_height' => 'Magasség', |
||||
'term.buttons' => 'Gomb cimkék', |
||||
'term.theme' => 'Szín paletta', |
||||
'term.cursor_shape' => 'Kurzor stílus', |
||||
'term.parser_tout_ms' => 'Olvasó időtúllépés', |
||||
'term.display_tout_ms' => 'Újrarajzolás késleltetése', |
||||
'term.display_cooldown_ms' => 'Újrarajzolás cooldown', |
||||
'term.allow_decopt_12' => '\e?12h/l engedélyezés', |
||||
'term.fn_alt_mode' => 'SS3 Fn gombok', |
||||
'term.show_config_links' => 'Navigációs linkek mutatása', |
||||
'term.show_buttons' => 'Gombok mutatása', |
||||
'term.loopback' => 'Helyi visszajelzés (<span style="text-decoration:overline">SRM</span>)', |
||||
'term.crlf_mode' => 'Enter = CR+LF (LNM)', |
||||
'term.want_all_fn' => 'F5, F11, F12 elfogása', |
||||
'term.button_msgs' => 'Gomb kódok<br>(ASCII, dec, CSV)', |
||||
'term.color_fg' => 'Alap előtér.', |
||||
'term.color_bg' => 'Alap háttér', |
||||
'term.color_fg_prev' => 'Előtér', |
||||
'term.color_bg_prev' => 'Háttér', |
||||
'term.colors_preview' => '', |
||||
'term.debugbar' => 'Belső állapot hibakeresés', |
||||
'term.ascii_debug' => 'Kontroll kódok mutatása', |
||||
'term.backdrop' => 'Háttérkép URL.je', |
||||
'term.button_count' => 'Gomb szám', |
||||
'term.button_colors' => 'Gomb színek', |
||||
'term.font_stack' => 'Betű típus', |
||||
'term.font_size' => 'Betű méret', |
||||
|
||||
'cursor.block_blink' => 'Blokk, villog', |
||||
'cursor.block_steady' => 'Blokk, fix', |
||||
'cursor.underline_blink' => 'Aláhúzás, villog', |
||||
'cursor.underline_steady' => 'Aláhúzás, fix', |
||||
'cursor.bar_blink' => 'I, villog', |
||||
'cursor.bar_steady' => 'I, fix', |
||||
|
||||
// Text upload dialog |
||||
|
||||
'upload.title' => 'Szöveg feltöltése', |
||||
'upload.prompt' => 'Szöveg fájl betöltése:', |
||||
'upload.endings' => 'Sor vége:', |
||||
'upload.endings.cr' => 'CR (Enter gomb)', |
||||
'upload.endings.crlf' => 'CR LF (Windows)', |
||||
'upload.endings.lf' => 'LF (Linux)', |
||||
'upload.chunk_delay' => 'Chunk késleltetés (ms):', |
||||
'upload.chunk_size' => 'Chunk méret (0=line):', |
||||
'upload.progress' => 'Feltöltés:', |
||||
|
||||
// Network config page |
||||
|
||||
'net.explain_sta' => ' |
||||
Kapcsold ki a dinamikus IP címet a statikus cím beállításához.', |
||||
|
||||
'net.explain_ap' => ' |
||||
Ezek a beállítások a beépített DHCP szervet és az AP módot befolyásolják.', |
||||
|
||||
'net.ap_dhcp_time' => 'Lízing idő', |
||||
'net.ap_dhcp_start' => 'Kezdő IP cím', |
||||
'net.ap_dhcp_end' => 'Záró IP cím', |
||||
'net.ap_addr_ip' => 'Saját IP cím', |
||||
'net.ap_addr_mask' => 'Hálózati maszk', |
||||
|
||||
'net.sta_dhcp_enable' => 'Dinamikus IP cím használata', |
||||
'net.sta_addr_ip' => 'ESPTerm statikus IP címe', |
||||
'net.sta_addr_mask' => 'Hálózati maszk', |
||||
'net.sta_addr_gw' => 'Útválasztó IP címe', |
||||
|
||||
'net.ap' => 'DHCP Szerver (AP)', |
||||
'net.sta' => 'DHCP Kliens (Station)', |
||||
'net.sta_mac' => 'Állomás MAC címe', |
||||
'net.ap_mac' => 'AP MAC címe', |
||||
'net.details' => 'MAC címek', |
||||
|
||||
// Wifi config page |
||||
|
||||
'wifi.ap' => 'Beépített Access Point', |
||||
'wifi.sta' => 'Kapcsolódás létező hálózathoz', |
||||
|
||||
'wifi.enable' => 'Engedélyezve', |
||||
'wifi.tpw' => 'Adás teljesítmény', |
||||
'wifi.ap_channel' => 'Csatorna', |
||||
'wifi.ap_ssid' => 'AP SSID', |
||||
'wifi.ap_password' => 'Jelszó', |
||||
'wifi.ap_hidden' => 'SSID rejtése', |
||||
'wifi.sta_info' => 'Kiválasztott', |
||||
|
||||
'wifi.not_conn' => 'Nincs csatlkoztatva.', |
||||
'wifi.sta_none' => 'Egyiksem', |
||||
'wifi.sta_active_pw' => '🔒 Jelszó elmentve', |
||||
'wifi.sta_active_nopw' => '🔓 Szabad hozzáférés', |
||||
'wifi.connected_ip_is' => 'Csatlakozva, az IP cím ', |
||||
'wifi.sta_password' => 'Jelszó:', |
||||
|
||||
'wifi.scanning' => 'Keresés', |
||||
'wifi.scan_now' => 'Kattints a keresés indításához!', |
||||
'wifi.cant_scan_no_sta' => 'Kattints a kliens mód engedélyezéséhez és a keresés indításához!', |
||||
'wifi.select_ssid' => 'Elérhető hálózatok:', |
||||
'wifi.enter_passwd' => 'Jelszó a(z) ":ssid:" hálózathoz', |
||||
'wifi.sta_explain' => 'A hálózat kiválasztása után nyomdj meg az Alkamaz gombot a csatlakozáshoz.', |
||||
|
||||
// Wifi connecting status page |
||||
|
||||
'wificonn.status' => 'Státusz:', |
||||
'wificonn.back_to_config' => 'Vissza a WiFi beállításhoz', |
||||
'wificonn.telemetry_lost' => 'Telemetria megszakadt; valami hiba történt, vagy az eszközöd elvesztette a kapcsolatot.', |
||||
'wificonn.explain_android_sucks' => ' |
||||
Ha okostelefonon kapcsolódsz az ESPTerm-hez, vagy amikor csatlakozol |
||||
egy másik hálózatról, az eszközöd elveszítheti a kapcsolatot és |
||||
ez az indikátor nem fog működni. Kérlek várj egy keveset (~ 15 másodpercet), |
||||
és ellenőrizd, hogy a kapcsolat helyrejött-e.', |
||||
|
||||
'wificonn.explain_reset' => ' |
||||
Az beépített AP engedélyezéséhez tarts lenyomva a BOOT gombot amíg a kék led |
||||
villogni nem kezd. Tartsd addig lenyomva amíg a led el nem kezd gyorsan villogni |
||||
a gyári alapállapot visszaállításához".', |
||||
|
||||
'wificonn.disabled' =>"Station mode letiltva.", |
||||
'wificonn.idle' =>"Alapállapot, nincs csatlakozva és nincs IP címe.", |
||||
'wificonn.success' => "Csatlakozva! Kaptam IP címet", |
||||
'wificonn.working' => "Csatlakozás a beállított AP-hez", |
||||
'wificonn.fail' => "Csatlakozás nem sikerült, ellenőrizd a beállítások és próbáld újra. A hibaok: ", |
||||
|
||||
// Access restrictions form |
||||
|
||||
'pwlock.title' => 'Hozzáférés korlátozása', |
||||
'pwlock.explain' => ' |
||||
A web interfész néhany része vagy a teljes interfész jelszavas védelemmel látható el. |
||||
Hagyd a jelszó mezőt üresen ha nem akarod megváltoztatni.<br> |
||||
Az alapértelmezett jelszó "%def_access_pw%". |
||||
', |
||||
'pwlock.region' => 'Védett oldalak', |
||||
'pwlock.region.none' => 'Egyiksem, minden hozzáférhető', |
||||
'pwlock.region.settings_noterm' => 'WiFi, Hálózat és Rendszer beállítások', |
||||
'pwlock.region.settings' => 'Minden beállítás oldal', |
||||
'pwlock.region.menus' => 'Ez a teljes menű rész', |
||||
'pwlock.region.all' => 'Minden, még a terminál is', |
||||
'pwlock.new_access_pw' => 'Új jelszó', |
||||
'pwlock.new_access_pw2' => 'Jelszó ismét', |
||||
'pwlock.admin_pw' => 'Admin jelszó', |
||||
'pwlock.access_name' => 'Felhasználó név', |
||||
|
||||
// Setting admin password |
||||
|
||||
'adminpw.title' => 'Admin jelszó megváltoztatása', |
||||
'adminpw.explain' => |
||||
' |
||||
Az "admin jelszo" a tárolt alap beállítások módosításához és a hozzáférések |
||||
változtatásához kell. Ez a jelszó nincs a többi beállítással egy helyre mentve, |
||||
tehát a mentés és visszaállítás műveletek nem befolyásolják. |
||||
Ha az admin jelszó elveszik akkor a legegyszerűbb módja a hozzáférés |
||||
visszaszerzésére a chip újraflashselésere.<br> |
||||
Az alap jelszó: "%def_admin_pw%". |
||||
', |
||||
'adminpw.new_admin_pw' => 'Új admin jelszó', |
||||
'adminpw.new_admin_pw2' => 'Jelszó ismét', |
||||
'adminpw.old_admin_pw' => 'Régi admin jelszó', |
||||
|
||||
// Persist form |
||||
|
||||
'persist.title' => 'Mentés & Visszaállítás', |
||||
'persist.explain' => ' |
||||
ESPTerm az összes beállítást Flash-be menti. Az aktív beállítások at lehet másolni |
||||
a "alapértelmezett" területre és az később a lenti kék gombbal visszaállítható. |
||||
', |
||||
'persist.confirm_restore' => 'Minden beállítást visszaállítasz az "alap" értékre?', |
||||
'persist.confirm_restore_hard' => |
||||
'Visszaállítod a rendszer alap beállításait? Ez minden aktív ' . |
||||
'beállítást törölni fog és AP módban az alap SSID-vel for újraindulni.', |
||||
'persist.confirm_store_defaults' => |
||||
'Add meg az admin jelszót az alapállapotba állítás megerősítéshez.', |
||||
'persist.password' => 'Admin jelszó:', |
||||
'persist.restore_defaults' => 'Mentett beállítások visszaállítása', |
||||
'persist.write_defaults' => 'Aktív beállítások mentése alapértelmezetnek', |
||||
'persist.restore_hard' => 'Gyári alapbeállítások betöltése', |
||||
'persist.restore_hard_explain' => |
||||
'(Ez törli a Wifi beállításokat, de nincs hatása az admin jelszóra.)', |
||||
|
||||
'backup.title' => 'Configurációs fájl biztonsági másolat készítés', |
||||
'backup.explain' => 'Minden beállítás menthető és visszaállítható az admin jelszó kivételévelAll config except the admin password can be backed up and restored using egy .INI fájllal.', |
||||
'backup.export' => 'Fáljbe exportálás', |
||||
'backup.import' => 'Importálás!', |
||||
|
||||
|
||||
// UART settings form |
||||
|
||||
'uart.title' => 'Soros port paraméterek', |
||||
'uart.explain' => ' |
||||
Ez a beállítás szabályozza a kommunikációs UART-ot. A hibakereső UART fix |
||||
115.200 baud-val, egy stop-bittel és paritás bit nélkül működik. |
||||
', |
||||
'uart.baud' => 'Baud rate', |
||||
'uart.parity' => 'Parity', |
||||
'uart.parity.none' => 'Egyiksem', |
||||
'uart.parity.odd' => 'Páratlan', |
||||
'uart.parity.even' => 'Páros', |
||||
'uart.stop_bits' => 'Stop-bit', |
||||
'uart.stop_bits.one' => 'Egy', |
||||
'uart.stop_bits.one_and_half' => 'Másfél', |
||||
'uart.stop_bits.two' => 'Kettő', |
||||
|
||||
// HW tuning form |
||||
|
||||
'hwtuning.title' => 'Hardware Tuning', |
||||
'hwtuning.explain' => ' |
||||
ESP8266-t órajelét lehetséges 80 MHz-ről 160 MHz-re emelni. Ettől |
||||
jobb válaszidők és gyakoribb képernyő frissítések várhatóak, viszont megnövekszik |
||||
az energia felhasználás. Az interferencia esélye is megnő. |
||||
Ovatosan használd!. |
||||
', |
||||
'hwtuning.overclock' => 'Órajel emelése 160MHz-re', |
||||
|
||||
'gpio2_config' => 'GPIO2 function', // TODO translate |
||||
'gpio4_config' => 'GPIO4 function', |
||||
'gpio5_config' => 'GPIO5 function', |
||||
'gpio_config.off' => 'Disabled', |
||||
'gpio_config.off_2' => 'Debug UART Tx', |
||||
'gpio_config.out_initial0' => 'Output (initial 0)', |
||||
'gpio_config.out_initial1' => 'Output (initial 1)', |
||||
'gpio_config.in_pull' => 'Input (pull-up)', |
||||
'gpio_config.in_nopull' => 'Input (floating)', |
||||
|
||||
// Generic button / dialog labels |
||||
|
||||
'apply' => 'Alkalmaz', |
||||
'start' => 'Start', |
||||
'cancel' => 'Mégse', |
||||
'enabled' => 'Engedélyezve', |
||||
'disabled' => 'Letiltva', |
||||
'yes' => 'Igen', |
||||
'no' => 'Nem', |
||||
'confirm' => 'OK', |
||||
'copy' => 'Másolás', |
||||
'form_errors' => 'Validációs hiba:', |
||||
]; |
@ -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' |
||||
] |
@ -0,0 +1,21 @@ |
||||
{ |
||||
"name": "espterm-front-end", |
||||
"version": "1.0.0", |
||||
"description": "ESPTerm web interface", |
||||
"license": "MPL-2.0", |
||||
"devDependencies": { |
||||
"babel-cli": "^6.26.0", |
||||
"babel-loader": "^7.1.2", |
||||
"babel-preset-env": "^1.6.0", |
||||
"babel-preset-minify": "^0.2.0", |
||||
"html-minifier": "^3.5.5", |
||||
"node-sass": "^4.5.3", |
||||
"standard": "^10.0.3", |
||||
"webpack": "^3.6.0" |
||||
}, |
||||
"scripts": { |
||||
"webpack": "webpack --display-modules $@", |
||||
"sass": "node-sass $@", |
||||
"html-minifier": "html-minifier $@" |
||||
} |
||||
} |
@ -1,90 +1,193 @@ |
||||
<!-- Persist --> |
||||
<div class="Box str mobcol"> |
||||
<h2 tabindex=0><?= tr('system.save_restore') ?></h2>
|
||||
<h2 tabindex=0><?= tr('persist.title') ?></h2>
|
||||
|
||||
<div class="Row explain nomargintop"> |
||||
<?= tr('system.explain_persist') ?> |
||||
<?= tr('persist.explain') ?> |
||||
</div> |
||||
|
||||
<div class="Row buttons2"> |
||||
<a class="button icn-restore" |
||||
onclick="return confirm('<?= tr('system.confirm_restore') ?>');"
|
||||
onclick="return confirm('<?= e(tr('persist.confirm_restore')) ?>');"
|
||||
href="<?= e(url('restore_defaults')) ?>">
|
||||
<?= tr('system.restore_defaults') ?> |
||||
<?= tr('persist.restore_defaults') ?> |
||||
</a> |
||||
</div> |
||||
|
||||
<div class="Row buttons2"> |
||||
<a onclick="writeDefaults(); return false;" href="#"><?= tr('system.write_defaults') ?></a>
|
||||
<a onclick="writeDefaults(); return false;" href="#"><?= tr('persist.write_defaults') ?></a>
|
||||
</div> |
||||
|
||||
<div class="Row buttons2"> |
||||
<a onclick="return confirm('<?= tr('system.confirm_restore_hard') ?>');"
|
||||
<a onclick="return confirm('<?= e(tr('persist.confirm_restore_hard')) ?>');"
|
||||
href="<?= e(url('restore_hard')) ?>">
|
||||
<?= tr('system.restore_hard') ?> |
||||
</a> |
||||
<?= tr('persist.restore_hard') ?> |
||||
</a><br> |
||||
<?= tr('persist.restore_hard_explain') ?> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- Backup --> |
||||
<div class="Box str mobcol"> |
||||
<h2 tabindex=0><?= tr('backup.title') ?></h2>
|
||||
|
||||
<div class="Row explain nomargintop"> |
||||
<?= tr('backup.explain') ?> |
||||
</div> |
||||
|
||||
<form class="Box str mobcol" action="<?= e(url('system_set')) ?>" method="GET" id="form-1">
|
||||
<h2 tabindex=0><?= tr('system.uart') ?></h2>
|
||||
<div class="Row buttons2"> |
||||
<a class="button" |
||||
href="<?= e(url('ini_export')) ?>">
|
||||
<?= tr('backup.export') ?> |
||||
</a> |
||||
</div> |
||||
|
||||
<div class="Row buttons2"> |
||||
<form method="POST" action="<?= e(url('ini_import')) ?>" enctype='multipart/form-data'>
|
||||
<span class="filewrap"><input accept=".ini,text/plain" type="file" name="file"></span><!-- |
||||
--><input type="submit" value="<?= tr('backup.import') ?>">
|
||||
</form> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- Overclock --> |
||||
<form class="Box str mobcol" action="<?= e(url('system_set')) ?>" method="GET" id="form-hw">
|
||||
<h2 tabindex=0><?= tr('hwtuning.title') ?></h2>
|
||||
|
||||
<div class="Row explain"> |
||||
<?= tr('system.explain_uart') ?> |
||||
<?= tr('hwtuning.explain') ?> |
||||
</div> |
||||
|
||||
<div class="Row checkbox" > |
||||
<label><?= tr('hwtuning.overclock') ?></label><!--
|
||||
--><span class="box" tabindex=0 role=checkbox></span> |
||||
<input type="hidden" id="overclock" name="overclock" value="%overclock%"> |
||||
</div> |
||||
|
||||
<div class="Row"> |
||||
<label for="uart_baud"><?= tr('uart.baud') ?><span class="mq-phone"> (bps)</span></label>
|
||||
<select name="uart_baud" id="uart_baud" class="short"> |
||||
<?php foreach([ |
||||
300, 600, 1200, 2400, 4800, 9600, 19200, 38400, |
||||
57600, 74880, 115200, 230400, 460800, 921600, 1843200, 3686400, |
||||
] as $b): |
||||
?><option value="<?=$b?>"><?= number_format($b, 0, ',', '.') ?></option>
|
||||
<?php endforeach; ?> |
||||
<label for="gpio2_conf"><?= tr("gpio2_config") ?></label>
|
||||
<select name="gpio2_conf" id="gpio2_conf"> |
||||
<option value="0"><?= tr("gpio_config.off_2") ?></option>
|
||||
<option value="1"><?= tr("gpio_config.out_initial0") ?></option>
|
||||
<option value="2"><?= tr("gpio_config.out_initial1") ?></option>
|
||||
<option value="3"><?= tr("gpio_config.in_pull") ?></option>
|
||||
<option value="4"><?= tr("gpio_config.in_nopull") ?></option>
|
||||
</select> |
||||
<span class="mq-no-phone"> bps</span> |
||||
</div> |
||||
|
||||
<div class="Row"> |
||||
<label for="uart_parity"><?= tr('uart.parity') ?></label>
|
||||
<select name="uart_parity" id="uart_parity" class="short"> |
||||
<?php foreach([ |
||||
2 => tr('uart.parity.none'), |
||||
1 => tr('uart.parity.odd'), |
||||
0 => tr('uart.parity.even'), |
||||
] as $k => $label): |
||||
?><option value="<?=$k?>"><?=$label?></option>
|
||||
<?php endforeach; ?> |
||||
<label for="gpio4_conf"><?= tr("gpio4_config") ?></label>
|
||||
<select name="gpio4_conf" id="gpio4_conf"> |
||||
<option value="0"><?= tr("gpio_config.off") ?></option>
|
||||
<option value="1"><?= tr("gpio_config.out_initial0") ?></option>
|
||||
<option value="2"><?= tr("gpio_config.out_initial1") ?></option>
|
||||
<option value="3"><?= tr("gpio_config.in_pull") ?></option>
|
||||
<option value="4"><?= tr("gpio_config.in_nopull") ?></option>
|
||||
</select> |
||||
</div> |
||||
|
||||
<div class="Row"> |
||||
<label for="uart_stopbits"><?= tr('uart.stop_bits') ?></label>
|
||||
<select name="uart_stopbits" id="uart_stopbits" class="short"> |
||||
<?php foreach([ |
||||
1 => tr('uart.stop_bits.one'), |
||||
2 => tr('uart.stop_bits.one_and_half'), |
||||
3 => tr('uart.stop_bits.two'), |
||||
] as $k => $label): |
||||
?><option value="<?=$k?>"><?=$label?></option>
|
||||
<?php endforeach; ?> |
||||
<label for="gpio5_conf"><?= tr("gpio5_config") ?></label>
|
||||
<select name="gpio5_conf" id="gpio5_conf"> |
||||
<option value="0"><?= tr("gpio_config.off") ?></option>
|
||||
<option value="1"><?= tr("gpio_config.out_initial0") ?></option>
|
||||
<option value="2"><?= tr("gpio_config.out_initial1") ?></option>
|
||||
<option value="3"><?= tr("gpio_config.in_pull") ?></option>
|
||||
<option value="4"><?= tr("gpio_config.in_nopull") ?></option>
|
||||
</select> |
||||
</div> |
||||
|
||||
<div class="Row buttons"> |
||||
<a class="button icn-ok" href="#" onclick="qs('#form-hw').submit()"><?= tr('apply') ?></a>
|
||||
</div> |
||||
</form> |
||||
|
||||
|
||||
<?php |
||||
$NOFILL = 'readonly onfocus="this.removeAttribute(\'readonly\')" style="cursor:text" autocomplete="off"'; |
||||
?> |
||||
|
||||
<!-- Access perms --> |
||||
<form class="Box str mobcol" action="<?= e(url('system_set')) ?>" method="GET" id="form-access">
|
||||
<h2 tabindex=0><?= tr('pwlock.title') ?></h2>
|
||||
|
||||
<div class="Row explain"> |
||||
<?= tr('pwlock.explain') ?> |
||||
</div> |
||||
|
||||
<div class="Row"> |
||||
<label for="pwlock"><?= tr("pwlock.region") ?></label>
|
||||
<select name="pwlock" id="pwlock"> |
||||
<option value="0"><?= tr("pwlock.region.none") ?></option>
|
||||
<option value="1"><?= tr("pwlock.region.settings_noterm") ?></option>
|
||||
<option value="2"><?= tr("pwlock.region.settings") ?></option>
|
||||
<option value="3"><?= tr("pwlock.region.menus") ?></option>
|
||||
<option value="4"><?= tr("pwlock.region.all") ?></option>
|
||||
</select> |
||||
</div> |
||||
|
||||
<div class="Row"> |
||||
<label for="access_name"><?= tr('pwlock.access_name') ?></label>
|
||||
<input type="text" name="access_name" id="access_name" value="%h:access_name%"> |
||||
</div> |
||||
|
||||
<div class="Row"> |
||||
<label for="access_pw"><?= tr('pwlock.new_access_pw') ?></label>
|
||||
<input type="password" name="access_pw" id="access_pw" <?=$NOFILL?>>
|
||||
</div> |
||||
|
||||
<div class="Row"> |
||||
<label for="access_pw2"><?= tr('pwlock.new_access_pw2') ?></label>
|
||||
<input type="password" name="access_pw2" id="access_pw2" <?=$NOFILL?>>
|
||||
</div> |
||||
|
||||
<div class="Row"> |
||||
<label for="pw"><?= tr('pwlock.admin_pw') ?></label>
|
||||
<input type="password" name="pw" id="pw" required> |
||||
</div> |
||||
|
||||
<div class="Row buttons"> |
||||
<a class="button icn-ok" href="#" onclick="qs('#form-access').submit()"><?= tr('apply') ?></a>
|
||||
</div> |
||||
</form> |
||||
|
||||
<!-- Admin pw --> |
||||
<form class="Box str mobcol" action="<?= e(url('system_set')) ?>" method="GET" id="form-admin">
|
||||
<h2 tabindex=0><?= tr('adminpw.title') ?></h2>
|
||||
|
||||
<div class="Row explain"> |
||||
<?= tr('adminpw.explain') ?> |
||||
</div> |
||||
|
||||
<div class="Row"> |
||||
<label for="admin_pw"><?= tr('adminpw.new_admin_pw') ?></label>
|
||||
<input type="password" name="admin_pw" id="admin_pw"> |
||||
</div> |
||||
|
||||
<div class="Row"> |
||||
<label for="admin_pw2"><?= tr('adminpw.new_admin_pw2') ?></label>
|
||||
<input type="password" name="admin_pw2" id="admin_pw2"> |
||||
</div> |
||||
|
||||
<div class="Row"> |
||||
<label for="pw"><?= tr('adminpw.old_admin_pw') ?></label>
|
||||
<input type="password" name="pw" id="pw" required> |
||||
</div> |
||||
|
||||
<div class="Row buttons"> |
||||
<a class="button icn-ok" href="#" onclick="qs('#form-1').submit()"><?= tr('apply') ?></a>
|
||||
<a class="button icn-ok" href="#" onclick="qs('#form-admin').submit()"><?= tr('apply') ?></a>
|
||||
</div> |
||||
</form> |
||||
|
||||
<script> |
||||
function writeDefaults() { |
||||
var pw = prompt('<?= tr('system.confirm_store_defaults') ?>');
|
||||
var pw = prompt('<?= tr('persist.confirm_store_defaults') ?>');
|
||||
if (!pw) return; |
||||
location.href = <?=json_encode(url('write_defaults')) ?> + '?pw=' + pw;
|
||||
} |
||||
|
||||
$('#uart_baud').val(%uart_baud%); |
||||
$('#uart_parity').val(%uart_parity%); |
||||
$('#uart_stopbits').val(%uart_stopbits%); |
||||
$('#pwlock').val(%pwlock%); |
||||
$('#gpio2_conf').val(%gpio2_conf%); |
||||
$('#gpio4_conf').val(%gpio4_conf%); |
||||
$('#gpio5_conf').val(%gpio5_conf%); |
||||
</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><DestIP></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> |
@ -0,0 +1,39 @@ |
||||
<div class="Box fold"> |
||||
<h2>Remote GPIO Control</h2> |
||||
|
||||
<div class="Row v"> |
||||
<p> |
||||
ESPTerm provides a simple API to remotely control and read GPIO pins GPIO2, GPIO4, and GPIO5. |
||||
The main use of this API is to remotely reset a device that communicates with ESPTerm |
||||
through the UART. |
||||
</p> |
||||
|
||||
<p> |
||||
GPIO2 is normally used for debug UART, so when used as GPIO, debug logging is disabled. You |
||||
can configure the pin functions in <a href="<?= url('cfg_system') ?>">System Settings</a>.
|
||||
</p> |
||||
|
||||
<p> |
||||
The GPIO control endpoint is `/api/v1/gpio`, with optional GET arguments: |
||||
</p> |
||||
|
||||
<ul> |
||||
<li>`do2=<i>x</i>` - set GPIO2 level. <i>x</i> can be `0`, `1`, or `t` to toggle the pin. |
||||
<li>`do4=<i>x</i>` - set GPIO4 level |
||||
<li>`do5=<i>x</i>` - set GPIO5 level |
||||
<li>`pulse=<i>ms</i>` - the command starts a pulse. After the given amount of time |
||||
(milliseconds) has elapsed, the pins are set to the opposite levels than what was specified |
||||
(in the case of toggle, the original pin state) |
||||
</ul> |
||||
|
||||
<p> |
||||
A quick example: <a href="/api/v1/gpio?do4=1&pulse=500">`/api/v1/gpio?do4=1&pulse=500`</a> |
||||
sends a 500ms long positive pulse on GPIO4. |
||||
</p> |
||||
|
||||
<p> |
||||
The GPIO endpoint always returns a JSON object like this: `{"io2":0,"io4":1,"io5":0}`, showing |
||||
the current input levels. Input reading works always, regardless of the GPIO settings. |
||||
</p> |
||||
</div> |
||||
</div> |
@ -1,65 +1,112 @@ |
||||
|
||||
<div class="Box fold theme-0"> |
||||
<h2>Commands: Color SGR</h2> |
||||
<div class="Box fold"> |
||||
<h2>Commands: Color Attributes</h2> |
||||
|
||||
<div class="Row v"> |
||||
<p> |
||||
Colors are set using SGR commands (like `\e[10;20;30m`). The following tables list the SGR codes to use. |
||||
Selected colors are used for any new text entered, as well as for empty space when using line and screen clearing commands. |
||||
The configured default colors can be restored using SGR 39 for foreground and SGR 49 for background. |
||||
Colors are set using SGR commands (like `\e[30;47m`). The following tables list the SGR |
||||
codes to use. Selected colors are used for any new text entered, as well as for empty |
||||
space when using clearing commands (except screen reset `\ec`, which first clears all |
||||
style attriutes. The configured default colors can be restored using `SGR 39` for |
||||
foreground and `SGR 49` for background. |
||||
</p> |
||||
|
||||
<p> |
||||
The actual color representation depends on a color theme which |
||||
The actual color representation of the basic 16 colors depends on a color theme which |
||||
can be selected in <a href="<?= url('cfg_term') ?>">Terminal Settings</a>.
|
||||
</p> |
||||
|
||||
<p> |
||||
Background image can be set using `\e]70;<i>url</i>\a` (see section System Functions). |
||||
</p> |
||||
|
||||
<h3>Foreground colors</h3> |
||||
|
||||
<div class="colorprev"> |
||||
<span class="bg7 fg0">30</span> |
||||
<span class="bg0 fg1">31</span> |
||||
<span class="bg0 fg2">32</span> |
||||
<span class="bg0 fg3">33</span> |
||||
<span class="bg0 fg4">34</span> |
||||
<span class="bg0 fg5">35</span> |
||||
<span class="bg0 fg6">36</span> |
||||
<span class="bg0 fg7">37</span> |
||||
<span data-bg="0" data-fg="0" style="text-shadow: 0 0 3px white;">30</span><!-- |
||||
--><span data-bg="0" data-fg="1">31</span><!-- |
||||
--><span data-bg="0" data-fg="2">32</span><!-- |
||||
--><span data-bg="0" data-fg="3">33</span><!-- |
||||
--><span data-bg="0" data-fg="4">34</span><!-- |
||||
--><span data-bg="0" data-fg="5">35</span><!-- |
||||
--><span data-bg="0" data-fg="6">36</span><!-- |
||||
--><span data-bg="0" data-fg="7">37</span> |
||||
</div> |
||||
|
||||
<div class="colorprev"> |
||||
<span class="bg0 fg8">90</span> |
||||
<span class="bg0 fg9">91</span> |
||||
<span class="bg0 fg10">92</span> |
||||
<span class="bg0 fg11">93</span> |
||||
<span class="bg0 fg12">94</span> |
||||
<span class="bg0 fg13">95</span> |
||||
<span class="bg0 fg14">96</span> |
||||
<span class="bg0 fg15">97</span> |
||||
<span data-bg="0" data-fg="8">90</span><!-- |
||||
--><span data-bg="0" data-fg="9">91</span><!-- |
||||
--><span data-bg="0" data-fg="10">92</span><!-- |
||||
--><span data-bg="0" data-fg="11">93</span><!-- |
||||
--><span data-bg="0" data-fg="12">94</span><!-- |
||||
--><span data-bg="0" data-fg="13">95</span><!-- |
||||
--><span data-bg="0" data-fg="14">96</span><!-- |
||||
--><span data-bg="0" data-fg="15">97</span> |
||||
</div> |
||||
|
||||
<h3>Background colors</h3> |
||||
|
||||
<div class="colorprev"> |
||||
<span class="bg0 fg15">40</span> |
||||
<span class="bg1 fg15">41</span> |
||||
<span class="bg2 fg15">42</span> |
||||
<span class="bg3 fg0">43</span> |
||||
<span class="bg4 fg15">44</span> |
||||
<span class="bg5 fg15">45</span> |
||||
<span class="bg6 fg15">46</span> |
||||
<span class="bg7 fg0">47</span> |
||||
<span data-bg="0" data-fg="15">40</span><!-- |
||||
--><span data-bg="1" data-fg="15">41</span><!-- |
||||
--><span data-bg="2" data-fg="15">42</span><!-- |
||||
--><span data-bg="3" data-fg="0">43</span><!-- |
||||
--><span data-bg="4" data-fg="15">44</span><!-- |
||||
--><span data-bg="5" data-fg="15">45</span><!-- |
||||
--><span data-bg="6" data-fg="15">46</span><!-- |
||||
--><span data-bg="7" data-fg="0">47</span> |
||||
</div> |
||||
|
||||
<div class="colorprev"> |
||||
<span class="bg8 fg15">100</span> |
||||
<span class="bg9 fg0">101</span> |
||||
<span class="bg10 fg0">102</span> |
||||
<span class="bg11 fg0">103</span> |
||||
<span class="bg12 fg0">104</span> |
||||
<span class="bg13 fg0">105</span> |
||||
<span class="bg14 fg0">106</span> |
||||
<span class="bg15 fg0">107</span> |
||||
<span data-bg="8" data-fg="15">100</span><!-- |
||||
--><span data-bg="9" data-fg="0">101</span><!-- |
||||
--><span data-bg="10" data-fg="0">102</span><!-- |
||||
--><span data-bg="11" data-fg="0">103</span><!-- |
||||
--><span data-bg="12" data-fg="15">104</span><!-- |
||||
--><span data-bg="13" data-fg="0">105</span><!-- |
||||
--><span data-bg="14" data-fg="0">106</span><!-- |
||||
--><span data-bg="15" data-fg="0">107</span> |
||||
</div> |
||||
|
||||
<h3>256-color palette</h3> |
||||
|
||||
<p> |
||||
ESPTerm supports in total 256 standard colors. The dark and bright basic colors are |
||||
numbered 0-7 and 8-15. To use colors higher than 15 (or 0-15 using this simpler numbering), |
||||
send `CSI 38 ; 5 ; <i>n</i> m`, where `n` is the color to set. Use `CSI 48 ; 5 ; <i>n</i> m` for background colors. |
||||
</p> |
||||
|
||||
<div class="colorprev" id="pal256"> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<script> |
||||
$.ready(function() { |
||||
var wrap = qs('#pal256'); |
||||
var table = themes.buildColorTable(); |
||||
for (var i = 0; i < 256; i++) { |
||||
var el = document.createElement('span') |
||||
var clr = table[i] |
||||
if (i < 16) { |
||||
clr = themes.themes[1][i] |
||||
} |
||||
el.style.color = 'black' |
||||
if ( i < 7 || i == 12 || i == 8 || |
||||
(i >= 16 && i <= 33) || |
||||
(i >= 52 && i <= 69) || |
||||
(i >= 88 && i <= 99) || |
||||
(i >= 124 && i <= 129)) { |
||||
el.style.color = 'white' |
||||
} |
||||
el.textContent = ""+i |
||||
el.style.backgroundColor = clr |
||||
wrap.appendChild(el) |
||||
|
||||
if (i==15||(i-16)%24==23) { |
||||
el = document.createElement('br') |
||||
wrap.appendChild(el) |
||||
} |
||||
} |
||||
}); |
||||
</script> |
||||
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,115 @@ |
||||
@media print { |
||||
.Row.buttons, nav { |
||||
display: none !important; |
||||
} |
||||
.Row.buttons .button { |
||||
display: none !important; |
||||
} |
||||
|
||||
h1, h2, h3 { |
||||
// chrome ignores those :( |
||||
break-after: avoid-page!important; |
||||
page-break-after: avoid!important; |
||||
font-family: sans-serif; |
||||
} |
||||
|
||||
html, body { |
||||
background: white; |
||||
color: black; |
||||
font-family: serif; |
||||
//font-size: 12pt; |
||||
line-height: 1.3em; |
||||
} |
||||
|
||||
label, p { |
||||
color: black !important; |
||||
text-shadow: none !important; |
||||
} |
||||
|
||||
.Box { |
||||
box-shadow: none; |
||||
} |
||||
|
||||
input, select, button { |
||||
background: white !important; |
||||
color: black !important; |
||||
border: 1px solid black !important; |
||||
} |
||||
|
||||
a { |
||||
color: #004eff !important; |
||||
} |
||||
|
||||
a[href^="http://"]::after, |
||||
a[href^="https://"]::after { |
||||
content: attr(href); |
||||
padding-left: .8ex; |
||||
text-decoration: underline !important; |
||||
} |
||||
p a { |
||||
word-wrap: break-word; |
||||
} |
||||
|
||||
.Row.checkbox .box { |
||||
border-color: black; |
||||
border-radius: 3px; |
||||
background: white; |
||||
color: black; |
||||
} |
||||
|
||||
.button { |
||||
background: white; border: 1px solid black; |
||||
text-shadow: none !important; |
||||
color: black; |
||||
box-shadow: none; |
||||
text-decoration: underline !important; |
||||
} |
||||
|
||||
[class^="icn-"], [class*=" icn-"] { |
||||
&::before { |
||||
display: none; |
||||
} |
||||
} |
||||
|
||||
.Box .Row { |
||||
display: block !important; |
||||
} |
||||
|
||||
.Box.fold h2::after { |
||||
display: none; |
||||
} |
||||
|
||||
#outer { |
||||
display: block; |
||||
overflow: auto; |
||||
width: unset; |
||||
height: unset; |
||||
position: static; |
||||
} |
||||
|
||||
html, body { |
||||
overflow: auto !important; |
||||
width: unset; height: unset; |
||||
} |
||||
|
||||
.Box { |
||||
padding: 0; border: 0 none; |
||||
} |
||||
|
||||
.charset div span:nth-child(1), |
||||
.charset div span:nth-child(2) { |
||||
color: #666; |
||||
} |
||||
|
||||
.page-help code { |
||||
background: rgba(0, 215, 255, 0.31); |
||||
} |
||||
|
||||
[noprint] { |
||||
display: none !important; |
||||
} |
||||
|
||||
#content tbody th { |
||||
color: black !important; |
||||
} |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue