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 |
#!/bin/bash |
||||||
|
|
||||||
echo "Packing JS..." |
cd $(dirname $0) |
||||||
|
|
||||||
cat jssrc/chibi.js \ |
source "_build_common.sh" |
||||||
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 |
|
||||||
|
|
||||||
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 'ESPTerm front-end ready' |
||||||
|
|
||||||
echo "Building HTML..." |
|
||||||
php ./build_html.php |
|
||||||
|
|
||||||
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"> |
<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"> |
<div class="Row explain nomargintop"> |
||||||
<?= tr('system.explain_persist') ?> |
<?= tr('persist.explain') ?> |
||||||
</div> |
</div> |
||||||
|
|
||||||
<div class="Row buttons2"> |
<div class="Row buttons2"> |
||||||
<a class="button icn-restore" |
<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')) ?>">
|
href="<?= e(url('restore_defaults')) ?>">
|
||||||
<?= tr('system.restore_defaults') ?> |
<?= tr('persist.restore_defaults') ?> |
||||||
</a> |
</a> |
||||||
</div> |
</div> |
||||||
|
|
||||||
<div class="Row buttons2"> |
<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> |
||||||
|
|
||||||
<div class="Row buttons2"> |
<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')) ?>">
|
href="<?= e(url('restore_hard')) ?>">
|
||||||
<?= tr('system.restore_hard') ?> |
<?= tr('persist.restore_hard') ?> |
||||||
</a> |
</a><br> |
||||||
|
<?= tr('persist.restore_hard_explain') ?> |
||||||
</div> |
</div> |
||||||
</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">
|
<div class="Row buttons2"> |
||||||
<h2 tabindex=0><?= tr('system.uart') ?></h2>
|
<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"> |
<div class="Row explain"> |
||||||
<?= tr('system.explain_uart') ?> |
<?= tr('hwtuning.explain') ?> |
||||||
</div> |
</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"> |
<div class="Row"> |
||||||
<label for="uart_baud"><?= tr('uart.baud') ?><span class="mq-phone"> (bps)</span></label>
|
<label for="gpio2_conf"><?= tr("gpio2_config") ?></label>
|
||||||
<select name="uart_baud" id="uart_baud" class="short"> |
<select name="gpio2_conf" id="gpio2_conf"> |
||||||
<?php foreach([ |
<option value="0"><?= tr("gpio_config.off_2") ?></option>
|
||||||
300, 600, 1200, 2400, 4800, 9600, 19200, 38400, |
<option value="1"><?= tr("gpio_config.out_initial0") ?></option>
|
||||||
57600, 74880, 115200, 230400, 460800, 921600, 1843200, 3686400, |
<option value="2"><?= tr("gpio_config.out_initial1") ?></option>
|
||||||
] as $b): |
<option value="3"><?= tr("gpio_config.in_pull") ?></option>
|
||||||
?><option value="<?=$b?>"><?= number_format($b, 0, ',', '.') ?></option>
|
<option value="4"><?= tr("gpio_config.in_nopull") ?></option>
|
||||||
<?php endforeach; ?> |
|
||||||
</select> |
</select> |
||||||
<span class="mq-no-phone"> bps</span> |
|
||||||
</div> |
</div> |
||||||
|
|
||||||
<div class="Row"> |
<div class="Row"> |
||||||
<label for="uart_parity"><?= tr('uart.parity') ?></label>
|
<label for="gpio4_conf"><?= tr("gpio4_config") ?></label>
|
||||||
<select name="uart_parity" id="uart_parity" class="short"> |
<select name="gpio4_conf" id="gpio4_conf"> |
||||||
<?php foreach([ |
<option value="0"><?= tr("gpio_config.off") ?></option>
|
||||||
2 => tr('uart.parity.none'), |
<option value="1"><?= tr("gpio_config.out_initial0") ?></option>
|
||||||
1 => tr('uart.parity.odd'), |
<option value="2"><?= tr("gpio_config.out_initial1") ?></option>
|
||||||
0 => tr('uart.parity.even'), |
<option value="3"><?= tr("gpio_config.in_pull") ?></option>
|
||||||
] as $k => $label): |
<option value="4"><?= tr("gpio_config.in_nopull") ?></option>
|
||||||
?><option value="<?=$k?>"><?=$label?></option>
|
|
||||||
<?php endforeach; ?> |
|
||||||
</select> |
</select> |
||||||
</div> |
</div> |
||||||
|
|
||||||
|
<div class="Row"> |
||||||
|
<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"> |
<div class="Row"> |
||||||
<label for="uart_stopbits"><?= tr('uart.stop_bits') ?></label>
|
<label for="pwlock"><?= tr("pwlock.region") ?></label>
|
||||||
<select name="uart_stopbits" id="uart_stopbits" class="short"> |
<select name="pwlock" id="pwlock"> |
||||||
<?php foreach([ |
<option value="0"><?= tr("pwlock.region.none") ?></option>
|
||||||
1 => tr('uart.stop_bits.one'), |
<option value="1"><?= tr("pwlock.region.settings_noterm") ?></option>
|
||||||
2 => tr('uart.stop_bits.one_and_half'), |
<option value="2"><?= tr("pwlock.region.settings") ?></option>
|
||||||
3 => tr('uart.stop_bits.two'), |
<option value="3"><?= tr("pwlock.region.menus") ?></option>
|
||||||
] as $k => $label): |
<option value="4"><?= tr("pwlock.region.all") ?></option>
|
||||||
?><option value="<?=$k?>"><?=$label?></option>
|
|
||||||
<?php endforeach; ?> |
|
||||||
</select> |
</select> |
||||||
</div> |
</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"> |
<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> |
</div> |
||||||
</form> |
</form> |
||||||
|
|
||||||
<script> |
<script> |
||||||
function writeDefaults() { |
function writeDefaults() { |
||||||
var pw = prompt('<?= tr('system.confirm_store_defaults') ?>');
|
var pw = prompt('<?= tr('persist.confirm_store_defaults') ?>');
|
||||||
if (!pw) return; |
if (!pw) return; |
||||||
location.href = <?=json_encode(url('write_defaults')) ?> + '?pw=' + pw;
|
location.href = <?=json_encode(url('write_defaults')) ?> + '?pw=' + pw;
|
||||||
} |
} |
||||||
|
|
||||||
$('#uart_baud').val(%uart_baud%); |
$('#pwlock').val(%pwlock%); |
||||||
$('#uart_parity').val(%uart_parity%); |
$('#gpio2_conf').val(%gpio2_conf%); |
||||||
$('#uart_stopbits').val(%uart_stopbits%); |
$('#gpio4_conf').val(%gpio4_conf%); |
||||||
|
$('#gpio5_conf').val(%gpio5_conf%); |
||||||
</script> |
</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"> |
<div class="Box fold"> |
||||||
<h2>Commands: Color SGR</h2> |
<h2>Commands: Color Attributes</h2> |
||||||
|
|
||||||
<div class="Row v"> |
<div class="Row v"> |
||||||
<p> |
<p> |
||||||
Colors are set using SGR commands (like `\e[10;20;30m`). The following tables list the SGR codes to use. |
Colors are set using SGR commands (like `\e[30;47m`). The following tables list the SGR |
||||||
Selected colors are used for any new text entered, as well as for empty space when using line and screen clearing commands. |
codes to use. Selected colors are used for any new text entered, as well as for empty |
||||||
The configured default colors can be restored using SGR 39 for foreground and SGR 49 for background. |
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> |
||||||
|
|
||||||
<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>.
|
can be selected in <a href="<?= url('cfg_term') ?>">Terminal Settings</a>.
|
||||||
</p> |
</p> |
||||||
|
|
||||||
|
<p> |
||||||
|
Background image can be set using `\e]70;<i>url</i>\a` (see section System Functions). |
||||||
|
</p> |
||||||
|
|
||||||
<h3>Foreground colors</h3> |
<h3>Foreground colors</h3> |
||||||
|
|
||||||
<div class="colorprev"> |
<div class="colorprev"> |
||||||
<span class="bg7 fg0">30</span> |
<span data-bg="0" data-fg="0" style="text-shadow: 0 0 3px white;">30</span><!-- |
||||||
<span class="bg0 fg1">31</span> |
--><span data-bg="0" data-fg="1">31</span><!-- |
||||||
<span class="bg0 fg2">32</span> |
--><span data-bg="0" data-fg="2">32</span><!-- |
||||||
<span class="bg0 fg3">33</span> |
--><span data-bg="0" data-fg="3">33</span><!-- |
||||||
<span class="bg0 fg4">34</span> |
--><span data-bg="0" data-fg="4">34</span><!-- |
||||||
<span class="bg0 fg5">35</span> |
--><span data-bg="0" data-fg="5">35</span><!-- |
||||||
<span class="bg0 fg6">36</span> |
--><span data-bg="0" data-fg="6">36</span><!-- |
||||||
<span class="bg0 fg7">37</span> |
--><span data-bg="0" data-fg="7">37</span> |
||||||
</div> |
</div> |
||||||
|
|
||||||
<div class="colorprev"> |
<div class="colorprev"> |
||||||
<span class="bg0 fg8">90</span> |
<span data-bg="0" data-fg="8">90</span><!-- |
||||||
<span class="bg0 fg9">91</span> |
--><span data-bg="0" data-fg="9">91</span><!-- |
||||||
<span class="bg0 fg10">92</span> |
--><span data-bg="0" data-fg="10">92</span><!-- |
||||||
<span class="bg0 fg11">93</span> |
--><span data-bg="0" data-fg="11">93</span><!-- |
||||||
<span class="bg0 fg12">94</span> |
--><span data-bg="0" data-fg="12">94</span><!-- |
||||||
<span class="bg0 fg13">95</span> |
--><span data-bg="0" data-fg="13">95</span><!-- |
||||||
<span class="bg0 fg14">96</span> |
--><span data-bg="0" data-fg="14">96</span><!-- |
||||||
<span class="bg0 fg15">97</span> |
--><span data-bg="0" data-fg="15">97</span> |
||||||
</div> |
</div> |
||||||
|
|
||||||
<h3>Background colors</h3> |
<h3>Background colors</h3> |
||||||
|
|
||||||
<div class="colorprev"> |
<div class="colorprev"> |
||||||
<span class="bg0 fg15">40</span> |
<span data-bg="0" data-fg="15">40</span><!-- |
||||||
<span class="bg1 fg15">41</span> |
--><span data-bg="1" data-fg="15">41</span><!-- |
||||||
<span class="bg2 fg15">42</span> |
--><span data-bg="2" data-fg="15">42</span><!-- |
||||||
<span class="bg3 fg0">43</span> |
--><span data-bg="3" data-fg="0">43</span><!-- |
||||||
<span class="bg4 fg15">44</span> |
--><span data-bg="4" data-fg="15">44</span><!-- |
||||||
<span class="bg5 fg15">45</span> |
--><span data-bg="5" data-fg="15">45</span><!-- |
||||||
<span class="bg6 fg15">46</span> |
--><span data-bg="6" data-fg="15">46</span><!-- |
||||||
<span class="bg7 fg0">47</span> |
--><span data-bg="7" data-fg="0">47</span> |
||||||
</div> |
</div> |
||||||
|
|
||||||
<div class="colorprev"> |
<div class="colorprev"> |
||||||
<span class="bg8 fg15">100</span> |
<span data-bg="8" data-fg="15">100</span><!-- |
||||||
<span class="bg9 fg0">101</span> |
--><span data-bg="9" data-fg="0">101</span><!-- |
||||||
<span class="bg10 fg0">102</span> |
--><span data-bg="10" data-fg="0">102</span><!-- |
||||||
<span class="bg11 fg0">103</span> |
--><span data-bg="11" data-fg="0">103</span><!-- |
||||||
<span class="bg12 fg0">104</span> |
--><span data-bg="12" data-fg="15">104</span><!-- |
||||||
<span class="bg13 fg0">105</span> |
--><span data-bg="13" data-fg="0">105</span><!-- |
||||||
<span class="bg14 fg0">106</span> |
--><span data-bg="14" data-fg="0">106</span><!-- |
||||||
<span class="bg15 fg0">107</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> |
</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