commit
891a44624e
@ -0,0 +1,18 @@ |
|||||||
|
{ |
||||||
|
"presets": [ |
||||||
|
["env", { |
||||||
|
"targets": { |
||||||
|
"browsers": [ |
||||||
|
"last 2 versions", |
||||||
|
"> 4%", |
||||||
|
"ie 11", |
||||||
|
"safari 8", |
||||||
|
"android 4.4" |
||||||
|
] |
||||||
|
} |
||||||
|
}], |
||||||
|
["minify", { |
||||||
|
"mergeVars": false |
||||||
|
}] |
||||||
|
] |
||||||
|
} |
@ -0,0 +1,8 @@ |
|||||||
|
# possibly minified output |
||||||
|
out/**/* |
||||||
|
|
||||||
|
# libraries |
||||||
|
js/lib/* |
||||||
|
|
||||||
|
# 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": "off", |
||||||
|
"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": ["off", { "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": ["error", { "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,3 @@ |
|||||||
|
#!/bin/bash |
||||||
|
|
||||||
|
export FRONT_END_HASH=$(git rev-parse --short HEAD) |
@ -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.css" |
@ -0,0 +1,6 @@ |
|||||||
|
#!/bin/bash |
||||||
|
source "_build_common.sh" |
||||||
|
|
||||||
|
echo 'Building HTML...' |
||||||
|
|
||||||
|
php ./compile_html.php |
@ -0,0 +1,35 @@ |
|||||||
|
#!/bin/bash |
||||||
|
source "_build_common.sh" |
||||||
|
|
||||||
|
mkdir -p out/js |
||||||
|
echo 'Generating lang.js...' |
||||||
|
php ./dump_js_lang.php |
||||||
|
|
||||||
|
if [[ $ESP_DEMO ]]; then |
||||||
|
demofile=js/demo.js |
||||||
|
else |
||||||
|
demofile= |
||||||
|
fi |
||||||
|
|
||||||
|
echo 'Processing JS...' |
||||||
|
if [[ $ESP_PROD ]]; then |
||||||
|
smarg= |
||||||
|
else |
||||||
|
smarg=--source-maps |
||||||
|
fi |
||||||
|
|
||||||
|
npm run babel -- -o "out/js/app.$FRONT_END_HASH.js" ${smarg} \ |
||||||
|
js/lib/chibi.js \ |
||||||
|
js/lib/keymaster.js \ |
||||||
|
js/lib/polyfills.js \ |
||||||
|
js/utils.js \ |
||||||
|
js/modal.js \ |
||||||
|
js/notif.js \ |
||||||
|
js/appcommon.js \ |
||||||
|
$demofile \ |
||||||
|
js/lang.js \ |
||||||
|
js/wifi.js \ |
||||||
|
js/term_* \ |
||||||
|
js/debug_screen.js \ |
||||||
|
js/soft_keyboard.js \ |
||||||
|
js/term.js |
@ -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" |
|
||||||
|
Binary file not shown.
@ -0,0 +1,131 @@ |
|||||||
|
/** Global generic init */ |
||||||
|
$.ready(function () { |
||||||
|
// 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 () { |
||||||
|
$(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('mousewheel', 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.wheelDelta > 0) { |
||||||
|
val += step |
||||||
|
} else { |
||||||
|
val -= step |
||||||
|
} |
||||||
|
|
||||||
|
if (undef(min)) val = Math.max(val, +min) |
||||||
|
if (undef(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() |
||||||
|
}) |
||||||
|
|
||||||
|
// populate the form errors box from GET arg ?err=...
|
||||||
|
// (a way to pass errors back from server via redirect)
|
||||||
|
let errAt = location.search.indexOf('err=') |
||||||
|
if (errAt !== -1 && qs('.Box.errors')) { |
||||||
|
let errs = location.search.substr(errAt + 4).split(',') |
||||||
|
let humanReadableErrors = [] |
||||||
|
errs.forEach(function (er) { |
||||||
|
let lbl = qs('label[for="' + er + '"]') |
||||||
|
if (lbl) { |
||||||
|
lbl.classList.add('error') |
||||||
|
humanReadableErrors.push(lbl.childNodes[0].textContent.trim().replace(/: ?$/, '')) |
||||||
|
} |
||||||
|
// else {
|
||||||
|
// hres.push(er)
|
||||||
|
// }
|
||||||
|
}) |
||||||
|
|
||||||
|
qs('.Box.errors .list').innerHTML = humanReadableErrors.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
|
||||||
|
let br = qs('#brand') |
||||||
|
br && br.addEventListener('click', function () { |
||||||
|
location.href = '/' // go to terminal
|
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// setup the ajax loader
|
||||||
|
$._loader = function (vis) { |
||||||
|
$('#loader').toggleClass('show', vis) |
||||||
|
} |
||||||
|
|
||||||
|
// reveal content on load
|
||||||
|
function showPage () { |
||||||
|
$('#content').addClass('load') |
||||||
|
} |
||||||
|
|
||||||
|
// Auto reveal pages other than the terminal (sets window.noAutoShow)
|
||||||
|
$.ready(function () { |
||||||
|
if (window.noAutoShow !== true) { |
||||||
|
setTimeout(function () { |
||||||
|
showPage() |
||||||
|
}, 1) |
||||||
|
} |
||||||
|
}) |
@ -0,0 +1,106 @@ |
|||||||
|
window.attachDebugScreen = function (screen) { |
||||||
|
const debugCanvas = mk('canvas') |
||||||
|
const ctx = debugCanvas.getContext('2d') |
||||||
|
|
||||||
|
debugCanvas.style.position = 'absolute' |
||||||
|
// hackity hack should probably set this in CSS
|
||||||
|
debugCanvas.style.top = '6px' |
||||||
|
debugCanvas.style.left = '6px' |
||||||
|
debugCanvas.style.pointerEvents = 'none' |
||||||
|
|
||||||
|
let addCanvas = function () { |
||||||
|
if (!debugCanvas.parentNode) screen.canvas.parentNode.appendChild(debugCanvas) |
||||||
|
} |
||||||
|
let removeCanvas = function () { |
||||||
|
if (debugCanvas.parentNode) debugCanvas.parentNode.removeChild(debugCanvas) |
||||||
|
} |
||||||
|
let updateCanvasSize = function () { |
||||||
|
let { width, height, devicePixelRatio } = screen.window |
||||||
|
let cellSize = screen.getCellSize() |
||||||
|
debugCanvas.width = width * cellSize.width * devicePixelRatio |
||||||
|
debugCanvas.height = height * cellSize.height * devicePixelRatio |
||||||
|
debugCanvas.style.width = `${width * cellSize.width}px` |
||||||
|
debugCanvas.style.height = `${height * cellSize.height}px` |
||||||
|
} |
||||||
|
|
||||||
|
let startTime, endTime, lastReason |
||||||
|
let cells = new Map() |
||||||
|
|
||||||
|
let startDrawing |
||||||
|
|
||||||
|
screen._debug = { |
||||||
|
drawStart (reason) { |
||||||
|
lastReason = reason |
||||||
|
startTime = Date.now() |
||||||
|
}, |
||||||
|
drawEnd () { |
||||||
|
endTime = Date.now() |
||||||
|
console.log(`Draw: ${lastReason} (${(endTime - startTime)} ms) with fancy graphics: ${screen.window.graphics}`) |
||||||
|
startDrawing() |
||||||
|
}, |
||||||
|
setCell (cell, flags) { |
||||||
|
cells.set(cell, [flags, Date.now()]) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
let isDrawing = false |
||||||
|
|
||||||
|
let drawLoop = function () { |
||||||
|
if (isDrawing) requestAnimationFrame(drawLoop) |
||||||
|
|
||||||
|
let { devicePixelRatio, width, height } = screen.window |
||||||
|
let { width: cellWidth, height: cellHeight } = screen.getCellSize() |
||||||
|
let screenLength = width * height |
||||||
|
let now = Date.now() |
||||||
|
|
||||||
|
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0) |
||||||
|
ctx.clearRect(0, 0, width * cellWidth, height * cellHeight) |
||||||
|
|
||||||
|
let activeCells = 0 |
||||||
|
for (let cell = 0; cell < screenLength; cell++) { |
||||||
|
if (!cells.has(cell) || cells.get(cell)[0] === 0) continue |
||||||
|
|
||||||
|
let [flags, timestamp] = cells.get(cell) |
||||||
|
let elapsedTime = (now - timestamp) / 1000 |
||||||
|
|
||||||
|
if (elapsedTime > 1) continue |
||||||
|
|
||||||
|
activeCells++ |
||||||
|
ctx.globalAlpha = 0.5 * Math.max(0, 1 - elapsedTime) |
||||||
|
|
||||||
|
let x = cell % width |
||||||
|
let y = Math.floor(cell / width) |
||||||
|
|
||||||
|
if (flags & 1) { |
||||||
|
// redrawn
|
||||||
|
ctx.fillStyle = '#f0f' |
||||||
|
} |
||||||
|
if (flags & 2) { |
||||||
|
// updated
|
||||||
|
ctx.fillStyle = '#0f0' |
||||||
|
} |
||||||
|
|
||||||
|
ctx.fillRect(x * cellWidth, y * cellHeight, cellWidth, cellHeight) |
||||||
|
|
||||||
|
if (flags & 4) { |
||||||
|
// wide cell
|
||||||
|
ctx.lineWidth = 2 |
||||||
|
ctx.strokeStyle = '#f00' |
||||||
|
ctx.strokeRect(x * cellWidth, y * cellHeight, cellWidth, cellHeight) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (activeCells === 0) { |
||||||
|
isDrawing = false |
||||||
|
removeCanvas() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
startDrawing = function () { |
||||||
|
if (isDrawing) return |
||||||
|
addCanvas() |
||||||
|
updateCanvasSize() |
||||||
|
isDrawing = true |
||||||
|
drawLoop() |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,758 @@ |
|||||||
|
class ANSIParser { |
||||||
|
constructor (handler) { |
||||||
|
this.reset() |
||||||
|
this.handler = handler |
||||||
|
this.joinChunks = true |
||||||
|
} |
||||||
|
reset () { |
||||||
|
this.currentSequence = 0 |
||||||
|
this.sequence = '' |
||||||
|
} |
||||||
|
parseSequence (sequence) { |
||||||
|
if (sequence[0] === '[') { |
||||||
|
let type = sequence[sequence.length - 1] |
||||||
|
let content = sequence.substring(1, sequence.length - 1) |
||||||
|
|
||||||
|
let numbers = content ? content.split(';').map(i => +i.replace(/\D/g, '')) : [] |
||||||
|
let numOr1 = numbers.length ? numbers[0] : 1 |
||||||
|
if (type === 'H') { |
||||||
|
this.handler('set-cursor', (numbers[0] | 0) - 1, (numbers[1] | 0) - 1) |
||||||
|
} else if (type >= 'A' && type <= 'D') { |
||||||
|
this.handler(`move-cursor-${type <= 'B' ? 'y' : 'x'}`, ((type === 'B' || type === 'C') ? 1 : -1) * numOr1) |
||||||
|
} else if (type === 'E' || type === 'F') { |
||||||
|
this.handler('move-cursor-line', (type === 'E' ? 1 : -1) * numOr1) |
||||||
|
} else if (type === 'G') { |
||||||
|
this.handler('set-cursor-x', numOr1 - 1) |
||||||
|
} else if (type === 'J') { |
||||||
|
let number = numbers.length ? numbers[0] : 2 |
||||||
|
if (number === 2) this.handler('clear') |
||||||
|
} else if (type === 'P') { |
||||||
|
this.handler('delete', numOr1) |
||||||
|
} else if (type === '@') { |
||||||
|
this.handler('insert-blanks', numOr1) |
||||||
|
} else if (type === 'q') this.handler('set-cursor-style', numOr1) |
||||||
|
else if (type === 'm') { |
||||||
|
if (!numbers.length || numbers[0] === 0) { |
||||||
|
this.handler('reset-style') |
||||||
|
return |
||||||
|
} |
||||||
|
let type = numbers[0] |
||||||
|
if (type === 1) this.handler('add-attrs', 1) // bold
|
||||||
|
else if (type === 2) this.handler('add-attrs', 1 << 1) // faint
|
||||||
|
else if (type === 3) this.handler('add-attrs', 1 << 2) // italic
|
||||||
|
else if (type === 4) this.handler('add-attrs', 1 << 3) // underline
|
||||||
|
else if (type === 5 || type === 6) this.handler('add-attrs', 1 << 4) // blink
|
||||||
|
else if (type === 7) this.handler('add-attrs', -1) // invert
|
||||||
|
else if (type === 9) this.handler('add-attrs', 1 << 6) // strike
|
||||||
|
else if (type === 20) this.handler('add-attrs', 1 << 5) // fraktur
|
||||||
|
else if (type >= 30 && type <= 37) this.handler('set-color-fg', type % 10) |
||||||
|
else if (type >= 40 && type <= 47) this.handler('set-color-bg', type % 10) |
||||||
|
else if (type === 39) this.handler('set-color-fg', 7) |
||||||
|
else if (type === 49) this.handler('set-color-bg', 0) |
||||||
|
else if (type >= 90 && type <= 98) this.handler('set-color-fg', (type % 10) + 8) |
||||||
|
else if (type >= 100 && type <= 108) this.handler('set-color-bg', (type % 10) + 8) |
||||||
|
else if (type === 38 || type === 48) { |
||||||
|
if (numbers[1] === 5) { |
||||||
|
let color = (numbers[2] | 0) & 0xFF |
||||||
|
if (type === 38) this.handler('set-color-fg', color) |
||||||
|
if (type === 48) this.handler('set-color-bg', color) |
||||||
|
} |
||||||
|
} |
||||||
|
} else if (type === 'h' || type === 'l') { |
||||||
|
if (content === '?25') { |
||||||
|
if (type === 'h') this.handler('show-cursor') |
||||||
|
else if (type === 'l') this.handler('hide-cursor') |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
write (text) { |
||||||
|
for (let character of text.toString()) { |
||||||
|
let code = character.codePointAt(0) |
||||||
|
if (code === 0x1b) this.currentSequence = 1 |
||||||
|
else if (this.currentSequence === 1 && character === '[') { |
||||||
|
this.currentSequence = 2 |
||||||
|
this.sequence += '[' |
||||||
|
} else if (this.currentSequence && character.match(/[\x40-\x7e]/)) { |
||||||
|
this.parseSequence(this.sequence + character) |
||||||
|
this.currentSequence = 0 |
||||||
|
this.sequence = '' |
||||||
|
} else if (this.currentSequence > 1) this.sequence += character |
||||||
|
else if (this.currentSequence === 1) { |
||||||
|
// something something nothing
|
||||||
|
this.currentSequence = 0 |
||||||
|
this.handler('write', character) |
||||||
|
} else if (code === 0x07) this.handler('bell') |
||||||
|
else if (code === 0x08) this.handler('back') |
||||||
|
else if (code === 0x0a) this.handler('new-line') |
||||||
|
else if (code === 0x0d) this.handler('return') |
||||||
|
else if (code === 0x15) this.handler('delete-line') |
||||||
|
else if (code === 0x17) this.handler('delete-word') |
||||||
|
else this.handler('write', character) |
||||||
|
} |
||||||
|
if (!this.joinChunks) this.reset() |
||||||
|
} |
||||||
|
} |
||||||
|
const TERM_DEFAULT_STYLE = 7 |
||||||
|
const TERM_MIN_DRAW_DELAY = 10 |
||||||
|
|
||||||
|
let getRainbowColor = t => { |
||||||
|
let r = Math.floor(Math.sin(t) * 2.5 + 2.5) |
||||||
|
let g = Math.floor(Math.sin(t + 2 / 3 * Math.PI) * 2.5 + 2.5) |
||||||
|
let b = Math.floor(Math.sin(t + 4 / 3 * Math.PI) * 2.5 + 2.5) |
||||||
|
return 16 + 36 * r + 6 * g + b |
||||||
|
} |
||||||
|
|
||||||
|
class ScrollingTerminal { |
||||||
|
constructor (screen) { |
||||||
|
this.width = 80 |
||||||
|
this.height = 25 |
||||||
|
this.termScreen = screen |
||||||
|
this.parser = new ANSIParser((...args) => this.handleParsed(...args)) |
||||||
|
|
||||||
|
this.reset() |
||||||
|
|
||||||
|
this._lastLoad = Date.now() |
||||||
|
this.termScreen.load(this.serialize(), 0) |
||||||
|
} |
||||||
|
reset () { |
||||||
|
this.style = TERM_DEFAULT_STYLE |
||||||
|
this.cursor = { x: 0, y: 0, style: 1, visible: true } |
||||||
|
this.trackMouse = false |
||||||
|
this.theme = 0 |
||||||
|
this.rainbow = false |
||||||
|
this.parser.reset() |
||||||
|
this.clear() |
||||||
|
} |
||||||
|
clear () { |
||||||
|
this.screen = [] |
||||||
|
for (let i = 0; i < this.width * this.height; i++) { |
||||||
|
this.screen.push([' ', this.style]) |
||||||
|
} |
||||||
|
} |
||||||
|
scroll () { |
||||||
|
this.screen.splice(0, this.width) |
||||||
|
for (let i = 0; i < this.width; i++) { |
||||||
|
this.screen.push([' ', TERM_DEFAULT_STYLE]) |
||||||
|
} |
||||||
|
this.cursor.y-- |
||||||
|
} |
||||||
|
newLine () { |
||||||
|
this.cursor.y++ |
||||||
|
if (this.cursor.y >= this.height) this.scroll() |
||||||
|
} |
||||||
|
writeChar (character) { |
||||||
|
this.screen[this.cursor.y * this.width + this.cursor.x] = [character, this.style] |
||||||
|
this.cursor.x++ |
||||||
|
if (this.cursor.x >= this.width) { |
||||||
|
this.cursor.x = 0 |
||||||
|
this.newLine() |
||||||
|
} |
||||||
|
} |
||||||
|
moveBack (n = 1) { |
||||||
|
for (let i = 0; i < n; i++) { |
||||||
|
this.cursor.x-- |
||||||
|
if (this.cursor.x < 0) { |
||||||
|
if (this.cursor.y > 0) this.cursor.x = this.width - 1 |
||||||
|
else this.cursor.x = 0 |
||||||
|
this.cursor.y = Math.max(0, this.cursor.y - 1) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
moveForward (n = 1) { |
||||||
|
for (let i = 0; i < n; i++) { |
||||||
|
this.cursor.x++ |
||||||
|
if (this.cursor.x >= this.width) { |
||||||
|
this.cursor.x = 0 |
||||||
|
this.cursor.y++ |
||||||
|
if (this.cursor.y >= this.height) this.scroll() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
deleteChar () { |
||||||
|
this.moveBack() |
||||||
|
this.screen.splice((this.cursor.y + 1) * this.width, 0, [' ', TERM_DEFAULT_STYLE]) |
||||||
|
this.screen.splice(this.cursor.y * this.width + this.cursor.x, 1) |
||||||
|
} |
||||||
|
deleteForward (n) { |
||||||
|
n = Math.min(this.width, n) |
||||||
|
for (let i = 0; i < n; i++) this.screen.splice((this.cursor.y + 1) * this.width, 0, [' ', TERM_DEFAULT_STYLE]) |
||||||
|
this.screen.splice(this.cursor.y * this.width + this.cursor.x, n) |
||||||
|
} |
||||||
|
clampCursor () { |
||||||
|
if (this.cursor.x < 0) this.cursor.x = 0 |
||||||
|
if (this.cursor.y < 0) this.cursor.y = 0 |
||||||
|
if (this.cursor.x > this.width - 1) this.cursor.x = this.width - 1 |
||||||
|
if (this.cursor.y > this.height - 1) this.cursor.y = this.height - 1 |
||||||
|
} |
||||||
|
handleParsed (action, ...args) { |
||||||
|
if (action === 'write') { |
||||||
|
this.writeChar(args[0]) |
||||||
|
} else if (action === 'delete') { |
||||||
|
this.deleteForward(args[0]) |
||||||
|
} else if (action === 'insert-blanks') { |
||||||
|
this.insertBlanks(args[0]) |
||||||
|
} else if (action === 'clear') { |
||||||
|
this.clear() |
||||||
|
} else if (action === 'bell') { |
||||||
|
this.terminal.load('B') |
||||||
|
} else if (action === 'back') { |
||||||
|
this.moveBack() |
||||||
|
} else if (action === 'new-line') { |
||||||
|
this.newLine() |
||||||
|
} else if (action === 'return') { |
||||||
|
this.cursor.x = 0 |
||||||
|
} else if (action === 'set-cursor') { |
||||||
|
this.cursor.x = args[0] |
||||||
|
this.cursor.y = args[1] |
||||||
|
this.clampCursor() |
||||||
|
} else if (action === 'move-cursor-y') { |
||||||
|
this.cursor.y += args[0] |
||||||
|
this.clampCursor() |
||||||
|
} else if (action === 'move-cursor-x') { |
||||||
|
this.cursor.x += args[0] |
||||||
|
this.clampCursor() |
||||||
|
} else if (action === 'move-cursor-line') { |
||||||
|
this.cursor.x = 0 |
||||||
|
this.cursor.y += args[0] |
||||||
|
this.clampCursor() |
||||||
|
} else if (action === 'set-cursor-x') { |
||||||
|
this.cursor.x = args[0] |
||||||
|
} else if (action === 'set-cursor-style') { |
||||||
|
this.cursor.style = Math.max(0, Math.min(6, args[0])) |
||||||
|
} else if (action === 'reset-style') { |
||||||
|
this.style = TERM_DEFAULT_STYLE |
||||||
|
} else if (action === 'add-attrs') { |
||||||
|
if (args[0] === -1) { |
||||||
|
this.style = (this.style & 0xFF0000) | ((this.style >> 8) & 0xFF) | ((this.style & 0xFF) << 8) |
||||||
|
} else { |
||||||
|
this.style |= (args[0] << 16) |
||||||
|
} |
||||||
|
} else if (action === 'set-color-fg') { |
||||||
|
this.style = (this.style & 0xFFFF00) | args[0] |
||||||
|
} else if (action === 'set-color-bg') { |
||||||
|
this.style = (this.style & 0xFF00FF) | (args[0] << 8) |
||||||
|
} else if (action === 'hide-cursor') { |
||||||
|
this.cursor.visible = false |
||||||
|
} else if (action === 'show-cursor') { |
||||||
|
this.cursor.visible = true |
||||||
|
} |
||||||
|
} |
||||||
|
write (text) { |
||||||
|
this.parser.write(text) |
||||||
|
this.scheduleLoad() |
||||||
|
} |
||||||
|
serialize () { |
||||||
|
let serialized = 'S' |
||||||
|
serialized += encode2B(this.height) + encode2B(this.width) |
||||||
|
serialized += encode2B(this.cursor.y) + encode2B(this.cursor.x) |
||||||
|
|
||||||
|
let attributes = +this.cursor.visible |
||||||
|
attributes |= (3 << 5) * +this.trackMouse // track mouse controls both
|
||||||
|
attributes |= 3 << 7 // buttons/links always visible
|
||||||
|
attributes |= (this.cursor.style << 9) |
||||||
|
serialized += encode3B(attributes) |
||||||
|
|
||||||
|
let lastStyle = null |
||||||
|
let index = 0 |
||||||
|
for (let cell of this.screen) { |
||||||
|
let style = cell[1] |
||||||
|
if (this.rainbow) { |
||||||
|
let x = index % this.width |
||||||
|
let y = Math.floor(index / this.width) |
||||||
|
style = (style & 0xFF0000) | getRainbowColor((x + y) / 10 + Date.now() / 1000) |
||||||
|
index++ |
||||||
|
} |
||||||
|
if (style !== lastStyle) { |
||||||
|
let foreground = style & 0xFF |
||||||
|
let background = (style >> 8) & 0xFF |
||||||
|
let attributes = (style >> 16) & 0xFF |
||||||
|
let setForeground = foreground !== (lastStyle & 0xFF) |
||||||
|
let setBackground = background !== ((lastStyle >> 8) & 0xFF) |
||||||
|
let setAttributes = attributes !== ((lastStyle >> 16) & 0xFF) |
||||||
|
|
||||||
|
if (setForeground && setBackground) serialized += '\x03' + encode3B(style & 0xFFFF) |
||||||
|
else if (setForeground) serialized += '\x05' + encode2B(foreground) |
||||||
|
else if (setBackground) serialized += '\x06' + encode2B(background) |
||||||
|
if (setAttributes) serialized += '\x04' + encode2B(attributes) |
||||||
|
|
||||||
|
lastStyle = style |
||||||
|
} |
||||||
|
serialized += cell[0] |
||||||
|
} |
||||||
|
return serialized |
||||||
|
} |
||||||
|
scheduleLoad () { |
||||||
|
clearTimeout(this._scheduledLoad) |
||||||
|
if (this._lastLoad < Date.now() - TERM_MIN_DRAW_DELAY) { |
||||||
|
this.termScreen.load(this.serialize(), this.theme) |
||||||
|
} else { |
||||||
|
this._scheduledLoad = setTimeout(() => { |
||||||
|
this.termScreen.load(this.serialize()) |
||||||
|
}, TERM_MIN_DRAW_DELAY - this._lastLoad) |
||||||
|
} |
||||||
|
} |
||||||
|
rainbowTimer () { |
||||||
|
if (!this.rainbow) return |
||||||
|
clearInterval(this._rainbowTimer) |
||||||
|
this._rainbowTimer = setInterval(() => { |
||||||
|
if (this.rainbow) this.scheduleLoad() |
||||||
|
}, 50) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class Process { |
||||||
|
constructor (args) { |
||||||
|
// event listeners
|
||||||
|
this._listeners = {} |
||||||
|
} |
||||||
|
on (event, listener) { |
||||||
|
if (!this._listeners[event]) this._listeners[event] = [] |
||||||
|
this._listeners[event].push({ listener }) |
||||||
|
} |
||||||
|
once (event, listener) { |
||||||
|
if (!this._listeners[event]) this._listeners[event] = [] |
||||||
|
this._listeners[event].push({ listener, once: true }) |
||||||
|
} |
||||||
|
off (event, listener) { |
||||||
|
let listeners = this._listeners[event] |
||||||
|
if (listeners) { |
||||||
|
for (let i in listeners) { |
||||||
|
if (listeners[i].listener === listener) { |
||||||
|
listeners.splice(i, 1) |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
emit (event, ...args) { |
||||||
|
let listeners = this._listeners[event] |
||||||
|
if (listeners) { |
||||||
|
let remove = [] |
||||||
|
for (let listener of listeners) { |
||||||
|
try { |
||||||
|
listener.listener(...args) |
||||||
|
if (listener.once) remove.push(listener) |
||||||
|
} catch (err) { |
||||||
|
console.error(err) |
||||||
|
} |
||||||
|
} |
||||||
|
for (let listener of remove) { |
||||||
|
listeners.splice(listeners.indexOf(listener), 1) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
write (data) { |
||||||
|
this.emit('in', data) |
||||||
|
} |
||||||
|
destroy () { |
||||||
|
// death.
|
||||||
|
this.emit('exit', 0) |
||||||
|
} |
||||||
|
run () { |
||||||
|
// noop
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
let demoData = { |
||||||
|
buttons: { |
||||||
|
1: '', |
||||||
|
2: '', |
||||||
|
3: '', |
||||||
|
4: '', |
||||||
|
5: function (terminal, shell) { |
||||||
|
if (shell.child) shell.child.destroy() |
||||||
|
let chars = 'info\r' |
||||||
|
let loop = function () { |
||||||
|
shell.write(chars[0]) |
||||||
|
chars = chars.substr(1) |
||||||
|
if (chars) setTimeout(loop, 100) |
||||||
|
} |
||||||
|
setTimeout(loop, 200) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
let demoshIndex = { |
||||||
|
clear: class Clear extends Process { |
||||||
|
run () { |
||||||
|
this.emit('write', '\x1b[2J\x1b[1;1H') |
||||||
|
this.destroy() |
||||||
|
} |
||||||
|
}, |
||||||
|
screenfetch: class Screenfetch extends Process { |
||||||
|
run () { |
||||||
|
let image = ` |
||||||
|
###. ESPTerm Demo |
||||||
|
'###. Hostname: ${window.location.hostname} |
||||||
|
'###. Shell: ESPTerm Demo Shell |
||||||
|
'###. Resolution: 80x25@${window.devicePixelRatio}x |
||||||
|
:###- |
||||||
|
.###' |
||||||
|
.###' |
||||||
|
.###' ############### |
||||||
|
###' ############### |
||||||
|
`.split('\n').filter(line => line.trim())
|
||||||
|
|
||||||
|
let chars = '' |
||||||
|
for (let y = 0; y < image.length; y++) { |
||||||
|
for (let x = 0; x < 80; x++) { |
||||||
|
if (image[y][x]) { |
||||||
|
chars += `\x1b[38;5;${getRainbowColor((x + y) / 10)}m${image[y][x]}` |
||||||
|
} else chars += ' ' |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
this.emit('write', '\r\n\x1b[?25l') |
||||||
|
let loop = () => { |
||||||
|
this.emit('write', chars.substr(0, 80)) |
||||||
|
chars = chars.substr(80) |
||||||
|
if (chars.length) setTimeout(loop, 50) |
||||||
|
else { |
||||||
|
this.emit('write', '\r\n\x1b[?25h') |
||||||
|
this.destroy() |
||||||
|
} |
||||||
|
} |
||||||
|
loop() |
||||||
|
} |
||||||
|
}, |
||||||
|
'local-echo': class LocalEcho extends Process { |
||||||
|
run (...args) { |
||||||
|
if (!args.includes('--suppress-note')) { |
||||||
|
this.emit('write', '\x1b[38;5;239mNote: not all terminal features are supported or and may not work as expected in this demo\x1b[0m\r\n') |
||||||
|
} |
||||||
|
} |
||||||
|
write (data) { |
||||||
|
this.emit('write', data) |
||||||
|
} |
||||||
|
}, |
||||||
|
'info': class Info extends Process { |
||||||
|
run (...args) { |
||||||
|
let fast = args.includes('--fast') |
||||||
|
this.showSplash().then(() => { |
||||||
|
this.printText(fast) |
||||||
|
}) |
||||||
|
} |
||||||
|
showSplash () { |
||||||
|
let splash = ` |
||||||
|
-#####- -###*..#####- ######- |
||||||
|
-#* -#- .## .##. *#- |
||||||
|
-##### .-###*..#####- *#- -*##*- #*-#--#**#-*##- |
||||||
|
-#* -#-.##. *#- *##@#* ##. -#* *# .#* |
||||||
|
-#####--####- .##. *#- -*#@@- ##. -#* *# .#* |
||||||
|
`.split('\n').filter(line => line.trim())
|
||||||
|
let levels = { |
||||||
|
' ': -231, |
||||||
|
'.': 4, |
||||||
|
'-': 8, |
||||||
|
'*': 17, |
||||||
|
'#': 24 |
||||||
|
} |
||||||
|
for (let i in splash) { |
||||||
|
if (splash[i].length < 79) splash[i] += ' '.repeat(79 - splash[i].length) |
||||||
|
} |
||||||
|
this.emit('write', '\r\n'.repeat(splash.length + 1)) |
||||||
|
this.emit('write', '\x1b[A'.repeat(splash.length)) |
||||||
|
this.emit('write', '\x1b[?25l') |
||||||
|
|
||||||
|
let cursorX = 0 |
||||||
|
let cursorY = 0 |
||||||
|
let moveTo = (x, y) => { |
||||||
|
let moveX = x - cursorX |
||||||
|
let moveY = y - cursorY |
||||||
|
this.emit('write', `\x1b[${Math.abs(moveX)}${moveX > 0 ? 'C' : 'D'}`) |
||||||
|
this.emit('write', `\x1b[${Math.abs(moveY)}${moveY > 0 ? 'B' : 'A'}`) |
||||||
|
cursorX = x |
||||||
|
cursorY = y |
||||||
|
} |
||||||
|
let drawCell = (x, y) => { |
||||||
|
moveTo(x, y) |
||||||
|
if (splash[y][x] === '@') { |
||||||
|
this.emit('write', '\x1b[48;5;8m\x1b[38;5;255m▄\b') |
||||||
|
} else { |
||||||
|
this.emit('write', `\x1b[48;5;${231 + levels[splash[y][x]]}m \b`) |
||||||
|
} |
||||||
|
} |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
const self = this |
||||||
|
let x = 14 |
||||||
|
let cycles = 0 |
||||||
|
let loop = function () { |
||||||
|
for (let y = 0; y < splash.length; y++) { |
||||||
|
let dx = x - y |
||||||
|
if (dx > 0) drawCell(dx, y) |
||||||
|
} |
||||||
|
|
||||||
|
if (++x < 79) { |
||||||
|
if (++cycles >= 3) { |
||||||
|
setTimeout(loop, 20) |
||||||
|
cycles = 0 |
||||||
|
} else loop() |
||||||
|
} else { |
||||||
|
moveTo(0, splash.length) |
||||||
|
self.emit('write', '\x1b[m\x1b[?25h') |
||||||
|
resolve() |
||||||
|
} |
||||||
|
} |
||||||
|
loop() |
||||||
|
}) |
||||||
|
} |
||||||
|
printText (fast = false) { |
||||||
|
// lots of printing
|
||||||
|
let parts = [ |
||||||
|
'', |
||||||
|
' ESPTerm is a VT100-like terminal emulator running on the ESP8266 WiFi chip.', |
||||||
|
'', |
||||||
|
' \x1b[93mThis is an online demo of the web user interface, simulating a simple ', |
||||||
|
' terminal in your browser.\x1b[m', |
||||||
|
'', |
||||||
|
' Type \x1b[92mls\x1b[m to list available commands.', |
||||||
|
' Use the \x1b[94mlinks\x1b[m below this screen for a demo of the options and more info.', |
||||||
|
'' |
||||||
|
] |
||||||
|
|
||||||
|
if (fast) { |
||||||
|
this.emit('write', parts.join('\r\n') + '\r\n') |
||||||
|
this.destroy() |
||||||
|
} else { |
||||||
|
const self = this |
||||||
|
let loop = function () { |
||||||
|
self.emit('write', parts.shift() + '\r\n') |
||||||
|
if (parts.length) setTimeout(loop, 17) |
||||||
|
else self.destroy() |
||||||
|
} |
||||||
|
loop() |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
colors: class PrintColors extends Process { |
||||||
|
run () { |
||||||
|
this.emit('write', '\r\n') |
||||||
|
let fgtext = 'foreground-color' |
||||||
|
this.emit('write', ' ') |
||||||
|
for (let i = 0; i < 16; i++) { |
||||||
|
this.emit('write', '\x1b[' + (i < 8 ? `3${i}` : `9${i - 8}`) + 'm') |
||||||
|
this.emit('write', fgtext[i] + ' ') |
||||||
|
} |
||||||
|
this.emit('write', '\r\n ') |
||||||
|
for (let i = 0; i < 16; i++) { |
||||||
|
this.emit('write', '\x1b[' + (i < 8 ? `4${i}` : `10${i - 8}`) + 'm ') |
||||||
|
} |
||||||
|
this.emit('write', '\x1b[m\r\n') |
||||||
|
for (let r = 0; r < 6; r++) { |
||||||
|
this.emit('write', ' ') |
||||||
|
for (let g = 0; g < 6; g++) { |
||||||
|
for (let b = 0; b < 6; b++) { |
||||||
|
this.emit('write', `\x1b[48;5;${16 + r * 36 + g * 6 + b}m `) |
||||||
|
} |
||||||
|
this.emit('write', '\x1b[m') |
||||||
|
} |
||||||
|
this.emit('write', '\r\n') |
||||||
|
} |
||||||
|
this.emit('write', ' ') |
||||||
|
for (let g = 0; g < 24; g++) { |
||||||
|
this.emit('write', `\x1b[48;5;${232 + g}m `) |
||||||
|
} |
||||||
|
this.emit('write', '\x1b[m\r\n\n') |
||||||
|
this.destroy() |
||||||
|
} |
||||||
|
}, |
||||||
|
ls: class ListCommands extends Process { |
||||||
|
run () { |
||||||
|
this.emit('write', '\x1b[92mList of demo commands\x1b[m\r\n') |
||||||
|
for (let i in demoshIndex) { |
||||||
|
if (typeof demoshIndex[i] === 'string') continue |
||||||
|
this.emit('write', i + '\r\n') |
||||||
|
} |
||||||
|
this.destroy() |
||||||
|
} |
||||||
|
}, |
||||||
|
theme: class SetTheme extends Process { |
||||||
|
constructor (shell) { |
||||||
|
super() |
||||||
|
this.shell = shell |
||||||
|
} |
||||||
|
run (...args) { |
||||||
|
let theme = args[0] | 0 |
||||||
|
if (!args.length || !Number.isFinite(theme) || theme < 0 || theme > 5) { |
||||||
|
this.emit('write', '\x1b[31mUsage: theme [0–5]\r\n') |
||||||
|
this.destroy() |
||||||
|
return |
||||||
|
} |
||||||
|
this.shell.terminal.theme = theme |
||||||
|
// HACK: reset drawn screen to prevent only partly redrawn screen
|
||||||
|
this.shell.terminal.termScreen.drawnScreenFG = [] |
||||||
|
this.emit('write', '') |
||||||
|
this.destroy() |
||||||
|
} |
||||||
|
}, |
||||||
|
cursor: class SetCursor extends Process { |
||||||
|
run (...args) { |
||||||
|
let steady = args.includes('--steady') |
||||||
|
if (args.includes('block')) { |
||||||
|
this.emit('write', `\x1b[${0 + 2 * steady} q`) |
||||||
|
} else if (args.includes('line')) { |
||||||
|
this.emit('write', `\x1b[${3 + steady} q`) |
||||||
|
} else if (args.includes('bar') || args.includes('beam')) { |
||||||
|
this.emit('write', `\x1b[${5 + steady} q`) |
||||||
|
} else { |
||||||
|
this.emit('write', '\x1b[31mUsage: cursor [block|line|bar] [--steady]\r\n') |
||||||
|
} |
||||||
|
this.destroy() |
||||||
|
} |
||||||
|
}, |
||||||
|
rainbow: class ToggleRainbow extends Process { |
||||||
|
constructor (shell) { |
||||||
|
super() |
||||||
|
this.shell = shell |
||||||
|
} |
||||||
|
run () { |
||||||
|
this.shell.terminal.rainbow = !this.shell.terminal.rainbow |
||||||
|
this.shell.terminal.rainbowTimer() |
||||||
|
this.emit('write', '') |
||||||
|
this.destroy() |
||||||
|
} |
||||||
|
}, |
||||||
|
pwd: '/this/is/a/demo\r\n', |
||||||
|
cd: '\x1b[38;5;239mNo directories to change to\r\n', |
||||||
|
whoami: `${window.navigator.userAgent}\r\n`, |
||||||
|
hostname: `${window.location.hostname}`, |
||||||
|
uname: 'ESPTerm Demo\r\n', |
||||||
|
mkdir: '\x1b[38;5;239mDid not create a directory because this is a demo.\r\n', |
||||||
|
rm: '\x1b[38;5;239mDid not delete anything because this is a demo.\r\n', |
||||||
|
cp: '\x1b[38;5;239mNothing to copy because this is a demo.\r\n', |
||||||
|
mv: '\x1b[38;5;239mNothing to move because this is a demo.\r\n', |
||||||
|
ln: '\x1b[38;5;239mNothing to link because this is a demo.\r\n', |
||||||
|
touch: '\x1b[38;5;239mNothing to touch\r\n', |
||||||
|
exit: '\x1b[38;5;239mNowhere to go\r\n' |
||||||
|
} |
||||||
|
|
||||||
|
class DemoShell { |
||||||
|
constructor (terminal, printInfo) { |
||||||
|
this.terminal = terminal |
||||||
|
this.terminal.reset() |
||||||
|
this.parser = new ANSIParser((...args) => this.handleParsed(...args)) |
||||||
|
this.input = '' |
||||||
|
this.cursorPos = 0 |
||||||
|
this.child = null |
||||||
|
this.index = demoshIndex |
||||||
|
|
||||||
|
if (printInfo) this.run('info') |
||||||
|
else this.prompt() |
||||||
|
} |
||||||
|
write (text) { |
||||||
|
if (this.child) { |
||||||
|
if (text.codePointAt(0) === 3) this.child.destroy() |
||||||
|
else this.child.write(text) |
||||||
|
} else this.parser.write(text) |
||||||
|
} |
||||||
|
prompt (success = true) { |
||||||
|
if (this.terminal.cursor.x !== 0) this.terminal.write('\x1b[m\x1b[38;5;238m⏎\r\n') |
||||||
|
this.terminal.write('\x1b[34;1mdemosh \x1b[m') |
||||||
|
if (!success) this.terminal.write('\x1b[31m') |
||||||
|
this.terminal.write('$ \x1b[m') |
||||||
|
this.input = '' |
||||||
|
this.cursorPos = 0 |
||||||
|
} |
||||||
|
handleParsed (action, ...args) { |
||||||
|
this.terminal.write('\b\x1b[P'.repeat(this.cursorPos)) |
||||||
|
if (action === 'write') { |
||||||
|
this.input = this.input.substr(0, this.cursorPos) + args[0] + this.input.substr(this.cursorPos) |
||||||
|
this.cursorPos++ |
||||||
|
} else if (action === 'back') { |
||||||
|
this.input = this.input.substr(0, this.cursorPos - 1) + this.input.substr(this.cursorPos) |
||||||
|
this.cursorPos-- |
||||||
|
if (this.cursorPos < 0) this.cursorPos = 0 |
||||||
|
} else if (action === 'move-cursor-x') { |
||||||
|
this.cursorPos = Math.max(0, Math.min(this.input.length, this.cursorPos + args[0])) |
||||||
|
} else if (action === 'delete-line') { |
||||||
|
this.input = '' |
||||||
|
this.cursorPos = 0 |
||||||
|
} else if (action === 'delete-word') { |
||||||
|
let words = this.input.substr(0, this.cursorPos).split(' ') |
||||||
|
words.pop() |
||||||
|
this.input = words.join(' ') + this.input.substr(this.cursorPos) |
||||||
|
this.cursorPos = words.join(' ').length |
||||||
|
} |
||||||
|
|
||||||
|
this.terminal.write(this.input) |
||||||
|
this.terminal.write('\b'.repeat(this.input.length)) |
||||||
|
this.terminal.moveForward(this.cursorPos) |
||||||
|
this.terminal.write('') // dummy. Apply the moveFoward
|
||||||
|
|
||||||
|
if (action === 'return') { |
||||||
|
this.terminal.write('\r\n') |
||||||
|
this.parse(this.input) |
||||||
|
} |
||||||
|
} |
||||||
|
parse (input) { |
||||||
|
if (input === 'help') input = 'info' |
||||||
|
// TODO: basic chaining (i.e. semicolon)
|
||||||
|
this.run(input) |
||||||
|
} |
||||||
|
run (command) { |
||||||
|
let parts = [''] |
||||||
|
|
||||||
|
let inQuote = false |
||||||
|
for (let character of command.trim()) { |
||||||
|
if (inQuote && character !== inQuote) { |
||||||
|
parts[parts.length - 1] += character |
||||||
|
} else if (inQuote) { |
||||||
|
inQuote = false |
||||||
|
} else if (character === '"' || character === "'") { |
||||||
|
inQuote = character |
||||||
|
} else if (character.match(/\s/)) { |
||||||
|
if (parts[parts.length - 1]) parts.push('') |
||||||
|
} else parts[parts.length - 1] += character |
||||||
|
} |
||||||
|
|
||||||
|
let name = parts.shift() |
||||||
|
if (name in this.index) { |
||||||
|
this.spawn(name, parts) |
||||||
|
} else { |
||||||
|
this.terminal.write(`demosh: Unknown command: ${name}\r\n`) |
||||||
|
this.prompt(false) |
||||||
|
} |
||||||
|
} |
||||||
|
spawn (name, args = []) { |
||||||
|
let Process = this.index[name] |
||||||
|
if (Process instanceof Function) { |
||||||
|
this.child = new Process(this) |
||||||
|
let write = data => this.terminal.write(data) |
||||||
|
this.child.on('write', write) |
||||||
|
this.child.on('exit', code => { |
||||||
|
if (this.child) this.child.off('write', write) |
||||||
|
this.child = null |
||||||
|
this.prompt(!code) |
||||||
|
}) |
||||||
|
this.child.run(...args) |
||||||
|
} else { |
||||||
|
this.terminal.write(Process) |
||||||
|
this.prompt() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
window.demoInterface = { |
||||||
|
input (data) { |
||||||
|
let type = data[0] |
||||||
|
let content = data.substr(1) |
||||||
|
|
||||||
|
if (type === 's') { |
||||||
|
this.shell.write(content) |
||||||
|
} else if (type === 'b') { |
||||||
|
let button = content.charCodeAt(0) |
||||||
|
let action = demoData.buttons[button] |
||||||
|
if (action) { |
||||||
|
if (typeof action === 'string') this.shell.write(action) |
||||||
|
else if (action instanceof Function) action(this.terminal, this.shell) |
||||||
|
} |
||||||
|
} else if (type === 'm' || type === 'p' || type === 'r') { |
||||||
|
console.log(JSON.stringify(data)) |
||||||
|
} |
||||||
|
}, |
||||||
|
init (screen) { |
||||||
|
this.terminal = new ScrollingTerminal(screen) |
||||||
|
this.shell = new DemoShell(this.terminal, true) |
||||||
|
} |
||||||
|
} |
@ -1,8 +1,8 @@ |
|||||||
// Generated from PHP locale file
|
// Generated from PHP locale file
|
||||||
var _tr = { |
let _tr = { |
||||||
"wifi.connected_ip_is": "Connected, IP is ", |
"wifi.connected_ip_is": "Connected, IP is ", |
||||||
"wifi.not_conn": "Not connected.", |
"wifi.not_conn": "Not connected.", |
||||||
"wifi.enter_passwd": "Enter password for \":ssid:\"" |
"wifi.enter_passwd": "Enter password for \":ssid:\"" |
||||||
}; |
}; |
||||||
|
|
||||||
function tr(key) { return _tr[key] || '?'+key+'?'; } |
function tr (key) { return _tr[key] || '?' + key + '?' } |
@ -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 @@ |
|||||||
|
/** Module for toggling a modal overlay */ |
||||||
|
(function () { |
||||||
|
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') |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
window.Modal = modal |
||||||
|
})() |
@ -0,0 +1,65 @@ |
|||||||
|
window.Notify = (function () { |
||||||
|
let nt = {} |
||||||
|
const sel = '#notif' |
||||||
|
let $balloon |
||||||
|
|
||||||
|
let timerHideBegin // timeout to start hiding (transition)
|
||||||
|
let timerHideEnd // timeout to add the hidden class
|
||||||
|
let timerCanCancel |
||||||
|
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 (undef(timeout) || timeout === null || timeout <= 0) { |
||||||
|
timeout = 2500 |
||||||
|
} |
||||||
|
|
||||||
|
timerHideBegin = setTimeout(nt.hide, timeout) |
||||||
|
|
||||||
|
canCancel = false |
||||||
|
timerCanCancel = setTimeout(function () { |
||||||
|
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') |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
return nt |
||||||
|
})() |
@ -0,0 +1,113 @@ |
|||||||
|
window.initSoftKeyboard = function (screen, input) { |
||||||
|
const keyInput = qs('#softkb-input') |
||||||
|
if (!keyInput) return // abort, we're not on the terminal page
|
||||||
|
|
||||||
|
let keyboardOpen = false |
||||||
|
|
||||||
|
let updateInputPosition = function () { |
||||||
|
if (!keyboardOpen) return |
||||||
|
|
||||||
|
let [x, y] = screen.gridToScreen(screen.cursor.x, screen.cursor.y, true) |
||||||
|
keyInput.style.transform = `translate(${x}px, ${y}px)` |
||||||
|
} |
||||||
|
|
||||||
|
keyInput.addEventListener('focus', () => { |
||||||
|
keyboardOpen = true |
||||||
|
updateInputPosition() |
||||||
|
}) |
||||||
|
|
||||||
|
keyInput.addEventListener('blur', () => (keyboardOpen = false)) |
||||||
|
|
||||||
|
screen.on('cursor-moved', updateInputPosition) |
||||||
|
|
||||||
|
let kbOpen = function (open) { |
||||||
|
keyboardOpen = open |
||||||
|
updateInputPosition() |
||||||
|
if (open) keyInput.focus() |
||||||
|
else keyInput.blur() |
||||||
|
} |
||||||
|
|
||||||
|
qs('#term-kb-open').addEventListener('click', function () { |
||||||
|
kbOpen(true) |
||||||
|
return false |
||||||
|
}) |
||||||
|
|
||||||
|
// 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 = '' |
||||||
|
let compositing = false |
||||||
|
|
||||||
|
// sends the difference between the last and the new composition string
|
||||||
|
let sendInputDelta = function (newValue) { |
||||||
|
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 = '' |
||||||
|
|
||||||
|
if (e.key === 'Backspace') { |
||||||
|
e.preventDefault() |
||||||
|
input.sendString('\b') |
||||||
|
} else if (e.key === 'Enter') { |
||||||
|
e.preventDefault() |
||||||
|
input.sendString('\x0d') |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
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) { |
||||||
|
sendInputDelta(e.data) |
||||||
|
} 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 = '' |
||||||
|
compositing = true |
||||||
|
}) |
||||||
|
|
||||||
|
keyInput.addEventListener('compositionend', e => { |
||||||
|
lastCompositionString = '' |
||||||
|
compositing = false |
||||||
|
keyInput.value = '' |
||||||
|
}) |
||||||
|
|
||||||
|
screen.on('open-soft-keyboard', () => keyInput.focus()) |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,97 @@ |
|||||||
|
/** Init the terminal sub-module - called from HTML */ |
||||||
|
window.termInit = function (opts) { |
||||||
|
let { labels, theme, allFn } = opts |
||||||
|
|
||||||
|
const screen = new TermScreen() |
||||||
|
const conn = Conn(screen) |
||||||
|
const input = Input(conn) |
||||||
|
const termUpload = TermUpl(conn, input, screen) |
||||||
|
screen.input = input |
||||||
|
|
||||||
|
conn.init() |
||||||
|
input.init({ allFn }) |
||||||
|
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.canvas) |
||||||
|
screen.load(labels, theme) // load labels and theme
|
||||||
|
|
||||||
|
window.initSoftKeyboard(screen, input) |
||||||
|
if (window.attachDebugScreen) window.attachDebugScreen(screen) |
||||||
|
|
||||||
|
let isFullscreen = false |
||||||
|
let fitScreen = false |
||||||
|
let fitScreenIfNeeded = function fitScreenIfNeeded () { |
||||||
|
if (isFullscreen) { |
||||||
|
screen.window.fitIntoWidth = window.screen.width |
||||||
|
screen.window.fitIntoHeight = window.screen.height |
||||||
|
} else { |
||||||
|
screen.window.fitIntoWidth = fitScreen ? window.innerWidth - 20 : 0 |
||||||
|
screen.window.fitIntoHeight = fitScreen ? window.innerHeight : 0 |
||||||
|
} |
||||||
|
} |
||||||
|
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 (Element.prototype.requestFullscreen || Element.prototype.webkitRequestFullscreen) { |
||||||
|
let checkForFullscreen = function () { |
||||||
|
// document.fullscreenElement is not really supported yet, so here's a hack
|
||||||
|
if (isFullscreen && (innerWidth !== window.screen.width || innerHeight !== window.screen.height)) { |
||||||
|
isFullscreen = false |
||||||
|
fitScreenIfNeeded() |
||||||
|
} |
||||||
|
} |
||||||
|
setInterval(checkForFullscreen, 500) |
||||||
|
|
||||||
|
// (why are the buttons anchors?)
|
||||||
|
let button = mk('a') |
||||||
|
button.href = '#' |
||||||
|
button.addEventListener('click', e => { |
||||||
|
e.preventDefault() |
||||||
|
|
||||||
|
isFullscreen = true |
||||||
|
fitScreenIfNeeded() |
||||||
|
screen.updateSize() |
||||||
|
|
||||||
|
if (screen.canvas.requestFullscreen) screen.canvas.requestFullscreen() |
||||||
|
else screen.canvas.webkitRequestFullscreen() |
||||||
|
}) |
||||||
|
let icon = mk('i') |
||||||
|
icon.classList.add('icn-resize-full') // TODO: less confusing icons
|
||||||
|
button.appendChild(icon) |
||||||
|
let span = mk('span') |
||||||
|
span.textContent = 'Fullscreen' |
||||||
|
button.appendChild(span) |
||||||
|
qs('#term-nav').insertBefore(button, qs('#term-nav').firstChild) |
||||||
|
} |
||||||
|
|
||||||
|
// for debugging
|
||||||
|
window.termScreen = screen |
||||||
|
window.conn = conn |
||||||
|
window.input = input |
||||||
|
window.termUpl = termUpload |
||||||
|
} |
@ -0,0 +1,144 @@ |
|||||||
|
/** Handle connections */ |
||||||
|
window.Conn = function (screen) { |
||||||
|
let ws |
||||||
|
let heartbeatTout |
||||||
|
let pingIv |
||||||
|
let xoff = false |
||||||
|
let autoXoffTout |
||||||
|
let reconTout |
||||||
|
|
||||||
|
let pageShown = false |
||||||
|
|
||||||
|
function onOpen (evt) { |
||||||
|
console.log('CONNECTED') |
||||||
|
heartbeat() |
||||||
|
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 '.': |
||||||
|
// heartbeat, no-op message
|
||||||
|
break |
||||||
|
|
||||||
|
case '-': |
||||||
|
// console.log('xoff');
|
||||||
|
xoff = true |
||||||
|
autoXoffTout = setTimeout(function () { |
||||||
|
xoff = false |
||||||
|
}, 250) |
||||||
|
break |
||||||
|
|
||||||
|
case '+': |
||||||
|
// console.log('xon');
|
||||||
|
xoff = false |
||||||
|
clearTimeout(autoXoffTout) |
||||||
|
break |
||||||
|
|
||||||
|
default: |
||||||
|
screen.load(evt.data) |
||||||
|
if (!pageShown) { |
||||||
|
showPage() |
||||||
|
pageShown = true |
||||||
|
} |
||||||
|
break |
||||||
|
} |
||||||
|
heartbeat() |
||||||
|
} catch (e) { |
||||||
|
console.error(e) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function canSend () { |
||||||
|
return !xoff |
||||||
|
} |
||||||
|
|
||||||
|
function doSend (message) { |
||||||
|
if (_demo) { |
||||||
|
if (typeof demoInterface !== 'undefined') { |
||||||
|
demoInterface.input(message) |
||||||
|
} else { |
||||||
|
console.log(`TX: ${JSON.stringify(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 (window._demo) { |
||||||
|
if (typeof demoInterface === 'undefined') { |
||||||
|
alert('Demoing non-demo demo!') // this will catch mistakes when deploying to the website
|
||||||
|
} else { |
||||||
|
demoInterface.init(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
|
||||||
|
} |
||||||
|
} |
@ -0,0 +1,303 @@ |
|||||||
|
/** |
||||||
|
* 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 |
||||||
|
*/ |
||||||
|
window.Input = function (conn) { |
||||||
|
let cfg = { |
||||||
|
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 cfg.fn_alt ? alt : normal |
||||||
|
} |
||||||
|
|
||||||
|
/** Cursor alt choice for key message */ |
||||||
|
function ca (alt, normal) { |
||||||
|
return cfg.cu_alt ? alt : normal |
||||||
|
} |
||||||
|
|
||||||
|
/** Numpad alt choice for key message */ |
||||||
|
function na (alt, normal) { |
||||||
|
return cfg.np_alt ? alt : normal |
||||||
|
} |
||||||
|
|
||||||
|
function _bindFnKeys (allFn) { |
||||||
|
const keymap = { |
||||||
|
'tab': '\x09', |
||||||
|
'backspace': '\x08', |
||||||
|
'enter': cfg.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)
|
||||||
|
} |
||||||
|
|
||||||
|
const blacklist = [ |
||||||
|
'f5', 'f11', 'f12', 'shift+f5' |
||||||
|
] |
||||||
|
|
||||||
|
for (let k in keymap) { |
||||||
|
if (!allFn && blacklist.includes(k)) continue |
||||||
|
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 (cfg.no_keys) return |
||||||
|
e.preventDefault() |
||||||
|
sendStrMsg(str) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/** Bind/rebind key messages */ |
||||||
|
function _initKeys (opts) { |
||||||
|
let { allFn } = opts |
||||||
|
|
||||||
|
// This takes care of text characters typed
|
||||||
|
window.addEventListener('keypress', function (evt) { |
||||||
|
if (cfg.no_keys) return |
||||||
|
let 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);
|
||||||
|
// prevent space from scrolling
|
||||||
|
if (evt.which === 32) evt.preventDefault() |
||||||
|
sendStrMsg(str) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// ctrl-letter codes are sent as simple low ASCII codes
|
||||||
|
for (let i = 1; i <= 26; i++) { |
||||||
|
bind('ctrl+' + String.fromCharCode(96 + i), String.fromCharCode(i)) |
||||||
|
} |
||||||
|
/* eslint-disable */ |
||||||
|
bind('ctrl+]', '\x1b') // alternate way to enter ESC
|
||||||
|
bind('ctrl+\\', '\x1c') |
||||||
|
bind('ctrl+[', '\x1d') |
||||||
|
bind('ctrl+^', '\x1e') |
||||||
|
bind('ctrl+_', '\x1f') |
||||||
|
|
||||||
|
// extra ctrl-
|
||||||
|
bind('ctrl+left', '\x1f[1;5D') |
||||||
|
bind('ctrl+right', '\x1f[1;5C') |
||||||
|
bind('ctrl+up', '\x1f[1;5A') |
||||||
|
bind('ctrl+down', '\x1f[1;5B') |
||||||
|
bind('ctrl+home', '\x1f[1;5H') |
||||||
|
bind('ctrl+end', '\x1f[1;5F') |
||||||
|
|
||||||
|
// extra shift-
|
||||||
|
bind('shift+left', '\x1f[1;2D') |
||||||
|
bind('shift+right', '\x1f[1;2C') |
||||||
|
bind('shift+up', '\x1f[1;2A') |
||||||
|
bind('shift+down', '\x1f[1;2B') |
||||||
|
bind('shift+home', '\x1f[1;2H') |
||||||
|
bind('shift+end', '\x1f[1;2F') |
||||||
|
|
||||||
|
// macOS editing commands
|
||||||
|
bind('⌥+left', '\x1bb') // ⌥← to go back a word (^[b)
|
||||||
|
bind('⌥+right', '\x1bf') // ⌥→ to go forward one word (^[f)
|
||||||
|
bind('⌘+left', '\x01') // ⌘← to go to the beginning of a line (^A)
|
||||||
|
bind('⌘+right', '\x05') // ⌘→ to go to the end of a line (^E)
|
||||||
|
bind('⌥+backspace', '\x17') // ⌥⌫ to delete a word (^W, I think)
|
||||||
|
bind('⌘+backspace', '\x15') // ⌘⌫ to delete to the beginning of a line (possibly ^U)
|
||||||
|
/* eslint-enable */ |
||||||
|
|
||||||
|
_bindFnKeys(allFn) |
||||||
|
} |
||||||
|
|
||||||
|
// mouse button states
|
||||||
|
let mb1 = 0 |
||||||
|
let mb2 = 0 |
||||||
|
let mb3 = 0 |
||||||
|
|
||||||
|
/** Init the Input module */ |
||||||
|
function init (opts) { |
||||||
|
_initKeys(opts) |
||||||
|
|
||||||
|
// 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 (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 |
||||||
|
|
||||||
|
// rebind keys - codes have changed
|
||||||
|
_bindFnKeys() |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
setMouseMode: function (click, move) { |
||||||
|
cfg.mt_click = click |
||||||
|
cfg.mt_move = move |
||||||
|
}, |
||||||
|
|
||||||
|
// Mouse events
|
||||||
|
onMouseMove: function (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: function (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)) |
||||||
|
// console.log("B ",b," M ",m);
|
||||||
|
}, |
||||||
|
|
||||||
|
onMouseUp: function (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: function (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);
|
||||||
|
}, |
||||||
|
|
||||||
|
mouseTracksClicks: function () { |
||||||
|
return cfg.mt_click |
||||||
|
}, |
||||||
|
|
||||||
|
blockKeys: function (yes) { |
||||||
|
cfg.no_keys = yes |
||||||
|
} |
||||||
|
} |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,169 @@ |
|||||||
|
/** File upload utility */ |
||||||
|
window.TermUpl = 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 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 |
||||||
|
} |
||||||
|
|
||||||
|
console.log(chunk) |
||||||
|
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 FileReader() |
||||||
|
let 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) { |
||||||
|
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', function () { |
||||||
|
openUploadDialog() |
||||||
|
return false |
||||||
|
}) |
||||||
|
|
||||||
|
qs('#term-fu-start').addEventListener('click', function () { |
||||||
|
startUpload() |
||||||
|
return false |
||||||
|
}) |
||||||
|
|
||||||
|
qs('#term-fu-close').addEventListener('click', function () { |
||||||
|
fuClose() |
||||||
|
return false |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,90 @@ |
|||||||
|
/** 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() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** HTML escape */ |
||||||
|
function esc (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 = 0) { |
||||||
|
return (s.charCodeAt(i++) - 1) + (s.charCodeAt(i) - 1) * 127 |
||||||
|
} |
||||||
|
|
||||||
|
/** Decode number from 3B encoding */ |
||||||
|
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. */ |
||||||
|
function encode2B (n) { |
||||||
|
let 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) { |
||||||
|
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 Chr(lsb) + Chr(msb) + Chr(xsb) |
||||||
|
} |
@ -0,0 +1,163 @@ |
|||||||
|
(function (w) { |
||||||
|
const authStr = ['Open', 'WEP', 'WPA', 'WPA2', 'WPA/WPA2'] |
||||||
|
let 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(esc(name)) |
||||||
|
const 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 |
||||||
|
} |
||||||
|
|
||||||
|
const done = !bool(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(function (a, b) { |
||||||
|
return 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">${authStr[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 = 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) { |
||||||
|
let inp = x.querySelector('input') |
||||||
|
let disp1 = x.querySelector('.x-disp1') |
||||||
|
let disp2 = x.querySelector('.x-disp2') |
||||||
|
let 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 = {}) |
@ -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,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,18 @@ |
|||||||
|
{ |
||||||
|
"name": "espterm-front-end", |
||||||
|
"version": "1.0.0", |
||||||
|
"description": "ESPTerm web interface", |
||||||
|
"license": "MPL-2.0", |
||||||
|
"devDependencies": { |
||||||
|
"babel-cli": "^6.26.0", |
||||||
|
"babel-minify": "^0.2.0", |
||||||
|
"babel-preset-env": "^1.6.0", |
||||||
|
"node-sass": "^4.5.3", |
||||||
|
"standard": "^10.0.3" |
||||||
|
}, |
||||||
|
"scripts": { |
||||||
|
"babel": "babel $@", |
||||||
|
"minify": "babel-minify $@", |
||||||
|
"sass": "node-sass $@" |
||||||
|
} |
||||||
|
} |
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; |
||||||
|
} |
||||||
|
} |
@ -1,3 +1,3 @@ |
|||||||
#!/bin/bash |
#!/bin/bash |
||||||
|
|
||||||
xterm -e "php -S 0.0.0.0:2000" |
xterm -e "php -S 0.0.0.0:2000 _dev_router.php" |
||||||
|
Loading…
Reference in new issue