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 |
||||
|
||||
echo "Packing JS..." |
||||
cd $(dirname $0) |
||||
|
||||
cat jssrc/chibi.js \ |
||||
jssrc/keymaster.js \ |
||||
jssrc/utils.js \ |
||||
jssrc/modal.js \ |
||||
jssrc/notif.js \ |
||||
jssrc/appcommon.js \ |
||||
jssrc/lang.js \ |
||||
jssrc/wifi.js \ |
||||
jssrc/term_* \ |
||||
jssrc/term.js > js/app-full.js |
||||
source "_build_common.sh" |
||||
|
||||
yuicompressor js/app-full.js > js/app.js |
||||
rm -fr out/* |
||||
|
||||
echo "Building CSS..." |
||||
./_build_css.sh |
||||
./_build_js.sh |
||||
./_build_html.sh |
||||
./_build_assets.sh |
||||
|
||||
sass --style=compressed sass/app.scss css/app.css |
||||
|
||||
echo "Building HTML..." |
||||
php ./build_html.php |
||||
|
||||
echo "ESPTerm front-end ready" |
||||
echo 'ESPTerm front-end ready' |
||||
|
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
|
||||
var _tr = { |
||||
let _tr = { |
||||
"wifi.connected_ip_is": "Connected, IP is ", |
||||
"wifi.not_conn": "Not connected.", |
||||
"wifi.enter_passwd": "Enter password for \":ssid:\"" |
||||
}; |
||||
|
||||
function tr(key) { return _tr[key] || '?'+key+'?'; } |
||||
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 |
||||
|
||||
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