Merge branch 'work', rel 1.1

cpsdqs/unified-input
Ondřej Hruška 7 years ago
commit 891a44624e
  1. 18
      .babelrc
  2. 8
      .eslintignore
  3. 191
      .eslintrc
  4. 2
      .gitignore
  5. 4
      README.md
  6. 16
      _build_assets.sh
  7. 3
      _build_common.sh
  8. 13
      _build_css.sh
  9. 6
      _build_html.sh
  10. 35
      _build_js.sh
  11. 20
      _debug_replacements.php
  12. 13
      _dev_router.php
  13. 7
      base.php
  14. 27
      build.sh
  15. 0
      compile_html.php
  16. 0
      css/.gitkeep
  17. 7
      dump_js_lang.php
  18. BIN
      fontello/fontello.zip
  19. 0
      js/.gitkeep
  20. 131
      js/appcommon.js
  21. 106
      js/debug_screen.js
  22. 758
      js/demo.js
  23. 4
      js/lang.js
  24. 2
      js/lib/chibi.js
  25. 3
      js/lib/keymaster.js
  26. 63
      js/lib/polyfills.js
  27. 44
      js/modal.js
  28. 65
      js/notif.js
  29. 113
      js/soft_keyboard.js
  30. 1144
      js/td/WebAudio.d.ts
  31. 97
      js/term.js
  32. 144
      js/term_conn.js
  33. 303
      js/term_input.js
  34. 1369
      js/term_screen.js
  35. 169
      js/term_upload.js
  36. 90
      js/utils.js
  37. 163
      js/wifi.js
  38. 189
      jssrc/appcommon.js
  39. 44
      jssrc/modal.js
  40. 32
      jssrc/notif.js
  41. 6
      jssrc/term.js
  42. 134
      jssrc/term_conn.js
  43. 264
      jssrc/term_input.js
  44. 380
      jssrc/term_screen.js
  45. 146
      jssrc/term_upload.js
  46. 161
      jssrc/utils.js
  47. 163
      jssrc/wifi.js
  48. 1
      lang/en.php
  49. 18
      package.json
  50. 7
      pages/_head.php
  51. 11
      pages/about.php
  52. 6
      pages/cfg_term.php
  53. 7
      pages/help.php
  54. 18
      pages/help/cmd_cursor.php
  55. 7
      pages/help/cmd_screen.php
  56. 81
      pages/help/cmd_system.php
  57. 15
      pages/help/screen_behavior.php
  58. 26
      pages/help/sgr_colors.php
  59. 18
      pages/help/sgr_styles.php
  60. 46
      pages/term.php
  61. 6
      sass/_fontello.scss
  62. 115
      sass/_print_override.scss
  63. 4
      sass/app.scss
  64. 4
      sass/layout/_base.scss
  65. 2
      sass/layout/_modal.scss
  66. 5
      sass/pages/_about.scss
  67. 65
      sass/pages/_term.scss
  68. 2
      server.sh
  69. 3124
      yarn.lock

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

2
.gitignore vendored

@ -1,6 +1,4 @@
_env.php
js/*
css/*
out/*
!.gitkeep
node_modules/

@ -12,7 +12,7 @@ PHP is used to build the HTML files and apply substitutions.
## Development
JavaScript source files can be found in the `jssrc/` folder, SASS
JavaScript source files can be found in the `js/` folder, SASS
files in the `sass/` folder.
Fontello (icon font) is maintained in the `fontello/` folder. To update Fontello, replace
@ -20,7 +20,7 @@ the `fontello.zip` file and run `unpack.sh` in the same folder. This will extrac
from the zip file and put them into the `sass/` folder to be included with the other styles.
To test you changes (after building JS and CSS), run a PHP local server in the project
directory using the `server.sh` script or by a command like `php -S 0.0.0.0:2000`.
directory using the `server.sh` script or by a command like `php -S 0.0.0.0:2000 _dev_router.php`.
Template substitutions (that are normally done by the ESPTerm's webserver) applied to the
files fior testing can be defined in `_debug_replacements.php`.

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

@ -7,13 +7,17 @@
*/
$vers = '???';
$f = file_get_contents(__DIR__ . '/../user/version.h');
preg_match_all('/#define FW_V_.*? (\d+)/', $f, $vm);
$versfn = __DIR__ . '/../user/version.h';
$fwHash = '00000000';
if (file_exists($versfn)) {
$f = file_get_contents($versfn);
preg_match_all('/#define FW_V_.*? (\d+)/', $f, $vm);
#define FW_V_MAJOR 1
#define FW_V_MINOR 0
#define FW_V_PATCH 0
$vers = $vm[1][0].'.'.$vm[1][1].'.'.$vm[1][2];
$vers = $vm[1][0] . '.' . $vm[1][1] . '.' . $vm[1][2];
$fwHash = trim(shell_exec('cd .. && git rev-parse --short HEAD'));
}
return [
'term_title' => ESP_DEMO ? 'ESPTerm Web UI Demo' : 'ESPTerm local debug',
@ -29,6 +33,7 @@ return [
'bm4' => '',
'bm5' => '05',
'labels_seq' => ESP_DEMO ? 'TESPTerm Web UI DemoOKCancelHelp' : 'TESPTerm local debugOKCancelHelp',
'want_all_fn' => '0',
'parser_tout_ms' => 10,
'display_tout_ms' => 15,
@ -55,7 +60,10 @@ return [
'time' => date('G:i'),
'vers_httpd' => '0.4',
'vers_sdk' => '010502',
'githubrepo' => 'https://github.com/MightyPork/esp-vt100-firmware',
'githubrepo' => 'https://github.com/espterm/espterm-firmware',
'githubrepo_front' => 'https://github.com/espterm/espterm-front-end',
'hash_backend' => $fwHash,
'hash_frontend' => GIT_HASH, // TODO actual versions?
'ap_dhcp_time' => '120',
'ap_dhcp_start' => '192.168.4.100',
@ -75,7 +83,7 @@ return [
'term_height' => '25',
'default_bg' => '0',
'default_fg' => '7',
'show_buttons' => '1',
'show_buttons' => '0',
'show_config_links' => '1',
'uart_baud' => 115200,

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

@ -16,6 +16,8 @@ if (!file_exists(__DIR__ . '/_env.php')) {
die("Copy <b>_env.php.example</b> to <b>_env.php</b> and check the settings inside!");
}
define('GIT_HASH', trim(shell_exec('git rev-parse --short HEAD')));
require_once __DIR__ . '/_env.php';
$prod = defined('STDIN');
@ -25,7 +27,6 @@ define('JS_WEB_ROOT', $root);
define('ESP_DEMO', (bool)getenv('ESP_DEMO'));
if (ESP_DEMO) {
define('DEMO_SCREEN', '"S\u0019\u0001Q\u0001\u0017\u0001K\u0001\u0015\u0004\u0003\b\u0001 \u0002P\u0001┌ESPTerm─Demo─\u0002\u0002\u0001\u0003\u0002\u000131\u0003\u0003\u000132\u0003\u0004\u00013\u0002\u0002\u0001\u0003\u0005\u000134\u0003\u0006\u000135\u0003\u0007\u000136\u0003\b\u000137\u0003\t\u000190\u0003\n\u000191\u0003\u000b\u000192\u0003\f\u000193\u0003\r\u000194\u0003\u000e\u000195\u0003\u000f\u000196\u0003\u0010\u000197\u0003\b\u0001─\u0002\r\u0001┐ \u0002\u0015\u0001│ \u00029\u0001│ \u0002\u0004\u0001│\u0002\t\u0001 \u0002\b\u0001│\u0004\u0002\u0001Bold \u0004\u0003\u0001F\u0004\u0003\u0001a\u0004\u0003\u0001i\u0004\u0003\u0001n\u0004\u0003\u0001t\u0004\u0003\u0001 \u0004\u0005\u0001I\u0004\u0005\u0001t\u0004\u0005\u0001a\u0004\u0005\u0001l\u0004\u0005\u0001i\u0004\u0005\u0001c\u0004\u0005\u0001 \u0004\t\u0001U\u0004\t\u0001n\u0004\t\u0001d\u0004\t\u0001e\u0004\t\u0001r\u0004\t\u0001l\u0004\t\u0001i\u0004\t\u0001n\u0004\t\u0001e\u0004\u0001\u0001 \u0004\u0011\u0001B\u0004\u0011\u0001l\u0004\u0011\u0001i\u0004\u0011\u0001n\u0004\u0011\u0001k\u0004\u0011\u0001 \u0001q\u0001\u0001Inverse\u0003\b\u0001 \u0004A\u0001S\u0004A\u0001t\u0004A\u0001r\u0004A\u0001i\u0004A\u0001k\u0004A\u0001e\u0004\u0001\u0001 \u0004!\u0001F\u0004!\u0001r\u0004!\u0001a\u0004!\u0001k\u0004!\u0001t\u0004!\u0001u\u0004!\u0001r\u0004\u0001\u0001 │ \u0002\u0002\u0001─\u0002\u0002\u0001\u0003\n\u0002 \u0002\t\u0001\u0003\b\u0001─\u0002\u0002\u0001 \u0002\u0006\u0001│ \u00029\u0001│ \u0002\u0002\u0001─\u0002\u0002\u0001\u0003\n\u0002 \u0003\u0002\u0002ESP826\u0002\u0002\u0001\u0003\n\u0002 \u0003\b\u0001─\u0002\u0002\u0001 \u0002\u0006\u0001└─\u00029\u0001┤ \u0002\u0002\u0001─\u0002\u0002\u0001\u0003\n\u0002 \u0002\t\u0001\u0003\b\u0001─\u0002\u0002\u0001 \u0002@\u0001│ \u0002\u0002\u0001─\u0002\u0002\u0001\u0003\n\u0002 \u0003\u0002\u0002(@)#\u0002\u0004\u0001\u0003\n\u0002 \u0003\b\u0001─\u0002\u0002\u0001 \u0002\u0007\u0001\u0003O\u0001 This is a static demo of the ESPTerm Web Interface \u0002\u0004\u0001\u0003\b\u0001 \u0002\u0002\u0001│ \u0002\u0002\u0001─\u0002\u0002\u0001\u0003\n\u0002 \u0002\t\u0001\u0003\b\u0001─\u0002\u0002\u0001 \u0002\u0007\u0001\u0003O\u0001 \u00027\u0001\u0003\b\u0001 \u0002\u0002\u0001│ \u0002\u0004\u0001│\u0002\t\u0001 \u0002\t\u0001\u0003O\u0001 Try the links beneath this scre\u0002\u0002\u0001n to browse the menu. \u0003\b\u0001 \u0002\u0002\u0001♦ \u0002\u0016\u0001\u0003O\u0001 \u00027\u0001\u0003\b\u0001 \u0002\u0019\u0001\u0003O\u0001 <°)\u0002\u0003\u0001>< ESPTerm ful\u0002\u0002\u0001y sup\u0002\u0002\u0001orts UTF-8 \u0002\u0002\u0001><(\u0002\u0003\u0001°> \u0003\b\u0001 \u0002\u0019\u0001\u0003O\u0001 \u00027\u0001\u0003\b\u0001 \u0002i\u0001\u0003\u000b\u0001Other interesting features:\u0003\b\u0001 \u0002\u0018\u0001↓ \u0002n\u0001\u0003\u0003\u0001- Almost ful\u0002\u0002\u0001 VT10\u0002\u0002\u0001 emulation \u0003\b\u0001 \u0003\u0006\u0001()\u0003\b\u0001 \u0003\u0006\u0001()\u0003\b\u0001 \u0002\b\u0001Funguje tu čeština! \u0002\u0011\u0001\u0003\u0005\u0001- Xterm-like mouse tracking\u0003\b\u0001 \u0002\u0003\u0001=\u0002\u0002\u0001\u0003\t\u0002°.°\u0003\b\u0001=\u0002\u0002\u0001 \u0003\u0006\u0001<-\u0002\u0003\u0001, \u0003\b\u0001 \u0002$\u0001\u0003\u0004\u0001- File upload utility\u0003\b\u0001 \u0002\n\u0001\'\u0002\u0002\u0001 \'\u0002\u0002\u0001 \u0002\u0002\u0001\u0003\u0006\u0001 \u0002\u0004\u0001mouse\u0003\b\u0001 \u0002!\u0001\u0003\u0002\u0001- User-friendly config interface\u0003\b\u0001 \u00020\u0001\u0003\u000e\u0001-\u0003\u0002\u0001 \u0003\u000e\u0001Advanced WiFi & network set\u0002\u0002\u0001ings\u0003\b\u0001 \u0002\u0011\u0001\u0003\f\u0001Try ESPTerm today!\u0003\b\u0001 \u0002\u000b\u0001- Built-in help page \u0002\u001a\u0001\u0003\u0007\u0001-\u0002\u0002\u0001>\u0003\b\u0001 \u0002\u0002\u0001\u0003\f\u0001Pre-built binaries\u0003\b\u0001 \u0003\f\u0001are\u0003\b\u0001 \u0002\"\u0001\u0003\u0007\u0001link on the About page \u0002\u0002\u0001\u0003\f\u0001available on GitHub! \u0003\b\u0001 \u0002U\u0001"');
define('DEMO_APS', <<<APS
{
"result": {
@ -115,7 +116,7 @@ if (!function_exists('utf8')) {
if (!function_exists('load_esp_charsets')) {
function load_esp_charsets() {
$chsf = __DIR__ . '/../user/character_sets.h';
if (! file_exists($chsf)) {
return [
'!! ERROR: `../user/character_sets.h` not found !!' => [
@ -123,7 +124,7 @@ if (!function_exists('load_esp_charsets')) {
],
];
}
$re_table = '/\/\/ %%BEGIN:(.)%%\s*(.*?)\s*\/\/ %%END:\1%%/s';
preg_match_all($re_table, file_get_contents($chsf), $m_tbl);

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

@ -8,7 +8,6 @@ $selected = [
'wifi.connected_ip_is',
'wifi.not_conn',
'wifi.enter_passwd',
'wifi.passwd_saved',
];
$out = [];
@ -16,8 +15,8 @@ foreach ($selected as $key) {
$out[$key] = $_messages[$key];
}
file_put_contents(__DIR__. '/jssrc/lang.js',
file_put_contents(__DIR__. '/js/lang.js',
"// Generated from PHP locale file\n" .
'var _tr = ' . json_encode($out, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) . ";\n\n" .
"function tr(key) { return _tr[key] || '?'+key+'?'; }\n"
'let _tr = ' . json_encode($out, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) . ";\n\n" .
"function tr (key) { return _tr[key] || '?' + key + '?' }\n"
);

Binary file not shown.

@ -0,0 +1,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 + '?' }

@ -575,7 +575,7 @@
// Basic XHR
chibi.ajax = function (options) { // if options is a number, it's timeout in ms
var opts = extend({
var opts = Object.assign({
method: 'GET',
nocache: true,
timeout: 5000,

@ -307,4 +307,5 @@
if(typeof module !== 'undefined') module.exports = assignKey;
})(this);
})(window);

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

1144
js/td/WebAudio.d.ts vendored

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) : "&nbsp;";
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 = {});

@ -60,6 +60,7 @@ return [
'term.show_buttons' => 'Show buttons',
'term.loopback' => 'Local Echo',
'term.crlf_mode' => 'Enter sends CR+LF',
'term.want_all_fn' => 'Capture all keys<br>(F5, F11, F12…)',
'term.button_msgs' => 'Button codes<br>(ASCII, dec, CSV)',
'cursor.block_blink' => 'Block, blinking',

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

@ -5,13 +5,12 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title><?= $_GET['PAGE_TITLE'] ?></title>
<link href="/css/app.css" rel="stylesheet">
<script src="/js/app.js"></script>
<link href="/css/app.<?= GIT_HASH ?>.css" rel="stylesheet">
<script src="/js/app.<?= GIT_HASH ?>.js"></script>
<script>
var _root = <?= JS_WEB_ROOT ?>;
var _demo = <?= (int)ESP_DEMO ?>;
<?php if($_GET['page']=='term'): ?>var _demo_screen = <?= ESP_DEMO ? DEMO_SCREEN : 0 ?>;<?php endif; ?>
<?php if($_GET['page']=='cfg_wifi'): ?>var _demo_aps = <?= ESP_DEMO ? json_encode(DEMO_APS) : '' ?>;<?php endif; ?>
<?php if($_GET['page']=='cfg_wifi'): ?>var _demo_aps = <?= ESP_DEMO ? json_encode(DEMO_APS) : '""' ?>;<?php endif; ?>
</script>
</head>
<body class="<?= $_GET['BODYCLASS'] ?>">

@ -2,7 +2,7 @@
<img src="/img/cvut.svg" id="logo" class="mq-tablet-min">
<h2>ESP8266 Remote Terminal</h2>
<img src="/img/cvut.svg" id="logo2" class="mq-phone">
<img src="/img/cvut.svg" id="logo2" class="mq-phone" noprint>
<p>
&copy; Ondřej Hruška, 2016-2017
@ -17,10 +17,16 @@
<div class="Box">
<h2>Version</h2>
<div class="tscroll">
<table>
<tr>
<th>ESPTerm</th>
<td>v%vers_fw%, built %date% at %time%</td>
<td>v%vers_fw%, built %date% at~%time%</td>
</tr>
<tr>
<th>Git hash</th>
<td>back-end: <a href="%githubrepo%/commit/%hash_backend%">%hash_backend%</a>,
front-end: <a href="%githubrepo_front%/commit/%hash_frontend%">%hash_frontend%</td>
</tr>
<tr>
<th>libesphttpd</th>
@ -31,6 +37,7 @@
<td>v%vers_sdk%</td>
</tr>
</table>
</div>
</div>
<div class="Box">

@ -165,6 +165,12 @@
<input type="hidden" id="fn_alt_mode" name="fn_alt_mode" value="%fn_alt_mode%">
</div>
<div class="Row checkbox" >
<label><?= tr('term.want_all_fn') ?></label><!--
--><span class="box" tabindex=0 role=checkbox></span>
<input type="hidden" id="want_all_fn" name="want_all_fn" value="%want_all_fn%">
</div>
<div class="Row checkbox" >
<label><?= tr('term.crlf_mode') ?></label><!--
--><span class="box" tabindex=0 role=checkbox></span>

@ -1,5 +1,10 @@
<div class="Box">
<a href="#" onclick="hpfold(1);return false">Expand all</a>&nbsp;|&nbsp;<a href="#" onclick="hpfold(0);return false">Collapse all</a>
<div noprint><a href="#" onclick="hpfold(1);return false">Expand all</a>&nbsp;|&nbsp;<a href="#" onclick="hpfold(0);return false">Collapse all</a><br>
<span class="smallpad"></span>
</div>
<i>Note: This list of commands is not exhaustive. \\
There's a more detailed and technical
<a href="https://espterm.github.io/docs/espterm-xterm.html">document</a> available online.</i>
</div>
<?php require __DIR__ . "/help/troubleshooting.php"; ?>

@ -184,6 +184,15 @@
</td>
<td>Enable (`h`) or disable (`l`) cursor auto-wrap and screen auto-scroll</td>
</tr>
<tr>
<td>
<code>
\e[?12h \\
\e[?12l
</code>
</td>
<td>Toggle cursor blinking (`h` on, `l` off)</td>
</tr>
<tr>
<td>
<code>
@ -193,6 +202,15 @@
</td>
<td>Show (`h`) or hide (`l`) the cursor</td>
</tr>
<tr>
<td>
<code>
\e[?45h \\
\e[?45l
</code>
</td>
<td>Enable (`h`) or disable (`l`) reverse wrap-around (when using "move left" or backspace)</td>
</tr>
</tbody>
</table>
</div>

@ -34,6 +34,13 @@
Erase _n_ characters in line.
</td>
</tr>
<tr>
<td>
`\e[<i>n</i>b`</td>
<td>
Repeat last printed characters _n_ times (moving cursor and using the current style).
</td>
</tr>
<tr>
<td>
<code>

@ -18,6 +18,10 @@
The screen size, title and button labels remain unchanged.
</td>
</tr>
<tr>
<td>`\e[8;<i>r</i>;<i>c</i>t`</td>
<td>Set screen size to _r_ rows and _c_ columns (this is a command borrowed from Xterm)</td>
</tr>
<tr>
<td>`\e[5n`</td>
<td>
@ -33,6 +37,15 @@
spontaneous restarts which require a full screen repaint.
</td>
</tr>
<tr>
<td>`\e[<i>n</i> q`</td>
<td>
Set cursor style: eg. `\e[3 q` (the space is part of the command!).
0~-~block~(blink), 1~-~default, 2~-~block~(steady), 3~-~underline~(blink),
4~-~underline~(steady), 5~-~I-bar~(blink), 6~-~I-bar~(steady). The default style (number 1)
can be configured in Terminal Settings
</td>
</tr>
<tr>
<td>`\e]0;<i>t</i>\a`</td>
<td>Set screen title to _t_ (this is a standard OSC command)</td>
@ -40,23 +53,48 @@
<tr>
<td>
<code>
\e]<i>80+n</i>;<i>t</i>\a
\e]<i>8x</i>;<i>t</i>\a
</code>
</td>
<td>
Set label for button _n_ = 1-5 (code 81-85) to _t_ - e.g.`\e]81;Yes\a`
Set label for button 1-5 (code 81-85) to _t_ - e.g.`\e]81;Yes\a`
sets the first button text to "Yes".
</td>
</tr>
<tr>
<td>
<code>
\e]<i>90+n</i>;<i>m</i>\a
\e]<i>9x</i>;<i>m</i>\a
</code>
</td>
<td>
Set message for button 1-5 (code 91-95) to _m_ - e.g.`\e]94;*\a`
sets the 3rd button to send "*" when pressed. The message can be up to
10 bytes long.
</td>
</tr>
<tr>
<td>
<code>
\e]9;<i>t</i>\a
</code>
</td>
<td>
Show a notification with text _t_. This will be either a desktop notification
or a pop-up balloon.
</td>
</tr>
<tr>
<td>
<code>
\e[?<i>n</i>s \\
\e[?<i>n</i>r
</code>
</td>
<td>
Set message for button _n_ = 1-5 (code 81-85) to _m_ - e.g.`\e]94;iv\a`
sets the 3rd button to send string "iv" when pressed.
Save (`s`) and restore (`r`) any option set using `CSI ? <i>n</i> h`.
This is used by some applications to back up the original state before
making changes.
</td>
</tr>
<tr>
@ -67,7 +105,7 @@
</code>
</td>
<td>
Show (`h`) or hide (`l`) action buttons (the blue buttons under the screen).
Show (`h`) or hide (`l`) the action buttons (the blue buttons under the screen).
</td>
</tr>
<tr>
@ -81,6 +119,33 @@
Show (`h`) or hide (`l`) menu/help links under the screen.
</td>
</tr>
<tr>
<td>
<code>
\e[?2004h \\
\e[?2004l
</code>
</td>
<td>
Enable (`h`) or disable (`l`) Bracketed Paste mode.
This mode makes any text sent using the Upload Tool be preceded by `\e[200\~`
and terminated by `\e[201\~`. This is useful for distinguishing keyboard input
from uploads.
</td>
</tr>
<tr>
<td>
<code>
\e[?1049h \\
\e[?1049l
</code>
</td>
<td>
Switch to (`h`) or from (`l`) an alternate screen.
ESPTerm can't implement this fully, so the original screen content is not saved,
but it will remember the cursor, screen size, terminal title, button labels and messages.
</td>
</tr>
<tr>
<td>
<code>
@ -93,10 +158,6 @@
SRM is the opposite of Local Echo, meaning `\e[12h` disables and `\e[12l` enables Local Echo.
</td>
</tr>
<tr>
<td>`\e[8;<i>r</i>;<i>c</i>t`</td>
<td>Set screen size to _r_ rows and _c_ columns (this is a command borrowed from Xterm)</td>
</tr>
</tbody>
</table>
</div>

@ -3,7 +3,8 @@
<div class="Row v">
<p>
The initial screen size, title text and button labels can be configured in <a href="<?= url('cfg_term') ?>">Terminal Settings</a>.
The initial screen size, title text and button labels can be configured
in <a href="<?= url('cfg_term') ?>">Terminal Settings</a>.
</p>
<p>
@ -13,5 +14,17 @@
repaint. If you experience issues (broken image due to dropped bytes), try adjusting those config options. It may also
be useful to try different baud rates.
</p>
<h3>UTF-8 support</h3>
<p>
ESPTerm supports all UTF-8 characters, but to reduce the screen buffer RAM size,
only a small amount of unique multi-byte characters can be used at the same time
(up to 160, depending on compile flags). Unique multi-byte characters are stored in a
look-up table and are removed when they are no longer used on the screen. In
rare cases it can happen that a character stays in the table after no longer
being used (this can be noticed when the table fills up and new characters
are not shown correctly). This is fixed by clearing the screen (`\e[2J` or `\ec`).
</p>
</div>
</div>

@ -1,16 +1,18 @@
<div class="Box fold theme-0">
<div class="Box fold theme-1">
<h2>Commands: Color SGR</h2>
<div class="Row v">
<p>
Colors are set using SGR commands (like `\e[10;20;30m`). The following tables list the SGR codes to use.
Selected colors are used for any new text entered, as well as for empty space when using line and screen clearing commands.
The configured default colors can be restored using SGR 39 for foreground and SGR 49 for background.
Colors are set using SGR commands (like `\e[30;47m`). The following tables list the SGR
codes to use. Selected colors are used for any new text entered, as well as for empty
space when using clearing commands (except screen reset `\ec`, which first clears all
style attriutes. The configured default colors can be restored using `SGR 39` for
foreground and `SGR 49` for background.
</p>
<p>
The actual color representation depends on a color theme which
The actual color representation of the basic 16 colors depends on a color theme which
can be selected in <a href="<?= url('cfg_term') ?>">Terminal Settings</a>.
</p>
@ -61,5 +63,19 @@
<span class="bg14 fg0">106</span>
<span class="bg15 fg0">107</span>
</div>
<h3>256-color palette</h3>
<p>
ESPTerm supports in total 256 standard colors. The dark and bright basic colors are
numbered 0-7 and 8-15. To use colors higher than 15 (or 0-15 using this simpler numbering),
send `CSI 38 ; 5 ; <i>n</i> m`, where `n` is the color to set. Use 48 for background colors.
</p>
<p>
For a fererence of all 256 shades please refer to
<a href="https://jonasjacek.github.io/colors/">jonasjacek.github.io/colors</a>
or look it up elsewhere.
</p>
</div>
</div>

@ -3,7 +3,7 @@
<div class="Row v">
<p>
All text attributes are set using SGR commands like `\e[10;20;30m`, with up to 10 numbers separated by semicolons.
All text attributes are set using SGR commands like `\e[1;4m`, with up to 10 numbers separated by semicolons.
To restore all attributes to their default states, use SGR 0: `\e[0m` or `\e[m`.
</p>
@ -16,11 +16,23 @@
<tr><td style="opacity:.6">Faint</td><td>2</td><td>22</td></tr>
<tr><td><i>Italic</i></td><td>3</td><td>23</td></tr>
<tr><td><u>Underlined</u></td><td>4</td><td>24</td></tr>
<tr><td>Blink</td><td>5</td><td>25</td></tr>
<tr><td><span style="color:black;background:#ccc;">Inverse</span></td><td>7</td><td>27</td></tr>
<tr><td><s>Striked</s></td><td>9</td><td>29</td></tr>
<tr><td style="text-decoration: overline;">Overline</td><td>53</td><td>55</td></tr>
<tr><td><span id="blinkdemo">Blink</span></td><td>5</td><td>25</td></tr>
<tr><td><span style="color:black;background:#ccc;">Inverse</span></td><td>7</td><td>27</td></tr>
<tr><td>𝔉𝔯𝔞𝔨𝔱𝔲𝔯</td><td>20</td><td>23</td></tr>
<tr><td>Conceal<sup>1</sup></td><td>8</td><td>28</td></tr>
</tbody>
</table>
<p><sup>1</sup>Conceal turns all characters invisible.</p>
</div>
</div>
<script>
setInterval(function() {
qs('#blinkdemo').className='';
setTimeout(function() {
qs('#blinkdemo').className='invisible';
}, 750);
}, 1000);
</script>

@ -1,3 +1,4 @@
<?php if (!DEBUG): ?>
<script>
// Workaround for badly loaded page
setTimeout(function() {
@ -7,6 +8,7 @@
}
}, 3000);
</script>
<?php endif; ?>
<div class="Modal light hidden" id="fu_modal">
<div id="fu_form" class="Dialog">
@ -31,17 +33,22 @@
</p>
</div>
<div class="fu-buttons">
<button onclick="TermUpl.start()" class="icn-ok x-fu-go">Start</button>&nbsp;
<button onclick="TermUpl.close()" class="icn-cancel x-fu-cancel">Cancel</button>&nbsp;
<button id="term-fu-start" class="icn-ok x-fu-go">Start</button>&nbsp;
<button id="term-fu-close" class="icn-cancel x-fu-cancel">Cancel</button>&nbsp;
<i class="fu-prog-box">Upload: <span id="fu_prog"></span></i>
</div>
</div>
</div>
<h1><!-- Screen title gets loaded here by JS --></h1>
<h1 id="screen-title"><!-- Screen title is loaded here by JS --></h1>
<div id="term-wrap">
<div id="screen" class="theme-%theme%"></div>
<div id="screen">
<input id="softkb-input" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
<div id="touch-select-menu">
<button id="touch-select-copy-btn">Copy</button>
</div>
</div>
<div id="action-buttons">
<button data-n="1"></button><!--
@ -52,11 +59,10 @@
</div>
</div>
<textarea id="softkb-input" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
<nav id="term-nav">
<a href="#" onclick="kbOpen(true);return false" class="mq-tablet-max"><i class="icn-keyboard"></i><span><?= tr('term_nav.keybd') ?></span></a><!--
--><a href="#" onclick="TermUpl.open();return false"><i class="icn-download"></i><span><?= tr('term_nav.upload') ?></span></a><!--
<a href="#" id="term-fit-screen" class="mq-tablet-max"><i id="resize-button-icon" class="icn-resize-small"></i></a><!--
--><a href="#" id="term-kb-open" class="mq-tablet-max"><i class="icn-keyboard"></i><span><?= tr('term_nav.keybd') ?></span></a><!--
--><a href="#" id="term-fu-open"><i class="icn-download"></i><span><?= tr('term_nav.upload') ?></span></a><!--
--><a href="<?= url('cfg_term') ?>" class="x-term-conf-btn"><i class="icn-configure"></i><span><?= tr('term_nav.config') ?></span></a><!--
--><a href="<?= url('cfg_wifi') ?>" class="x-term-conf-btn"><i class="icn-wifi"></i><span><?= tr('term_nav.wifi') ?></span></a><!--
--><a href="<?= url('help') ?>" class="x-term-conf-btn"><i class="icn-help"></i><span><?= tr('term_nav.help') ?></span></a><!--
@ -66,28 +72,18 @@
<script>
try {
window.noAutoShow = true;
termInit(); // the screen will be loaded via ajax
Screen.load('%j:labels_seq%');
// auto-clear the input box
$('#softkb-input').on('input', function(e) {
setTimeout(function(){
var str = $('#softkb-input').val();
$('#softkb-input').val('');
Input.sendString(str);
}, 1);
});
termInit({
labels: '%j:labels_seq%',
theme: +'%theme%',
allFn: !!+'%want_all_fn%',
});
} catch(e) {
console.error(e);
<?php if (!DEBUG): ?>
console.error("Fail, reloading in 3s…");
setTimeout(function() {
location.reload(true);
}, 3000);
}
function kbOpen(yes) {
var i = qs('#softkb-input');
if (yes) i.focus();
else i.blur();
<?php endif; ?>
}
</script>

File diff suppressed because one or more lines are too long

@ -0,0 +1,115 @@
@media print {
.Row.buttons, nav {
display: none !important;
}
.Row.buttons .button {
display: none !important;
}
h1, h2, h3 {
// chrome ignores those :(
break-after: avoid-page!important;
page-break-after: avoid!important;
font-family: sans-serif;
}
html, body {
background: white;
color: black;
font-family: serif;
//font-size: 12pt;
line-height: 1.3em;
}
label, p {
color: black !important;
text-shadow: none !important;
}
.Box {
box-shadow: none;
}
input, select, button {
background: white !important;
color: black !important;
border: 1px solid black !important;
}
a {
color: #004eff !important;
}
a[href^="http://"]::after,
a[href^="https://"]::after {
content: attr(href);
padding-left: .8ex;
text-decoration: underline !important;
}
p a {
word-wrap: break-word;
}
.Row.checkbox .box {
border-color: black;
border-radius: 3px;
background: white;
color: black;
}
.button {
background: white; border: 1px solid black;
text-shadow: none !important;
color: black;
box-shadow: none;
text-decoration: underline !important;
}
[class^="icn-"], [class*=" icn-"] {
&::before {
display: none;
}
}
.Box .Row {
display: block !important;
}
.Box.fold h2::after {
display: none;
}
#outer {
display: block;
overflow: auto;
width: unset;
height: unset;
position: static;
}
html, body {
overflow: auto !important;
width: unset; height: unset;
}
.Box {
padding: 0; border: 0 none;
}
.charset div span:nth-child(1),
.charset div span:nth-child(2) {
color: #666;
}
.page-help code {
background: rgba(0, 215, 255, 0.31);
}
[noprint] {
display: none !important;
}
#content tbody th {
color: black !important;
}
}

@ -20,7 +20,7 @@ $c-form-highlight-a: #2ea1f9;
$c-modal-bg: #242426;
$screen-stack: "DejaVu Sans Mono", "Liberation Mono", "Inconsolata", monospace;
$screen-stack: "DejaVu Sans Mono", "Liberation Mono", "Inconsolata", "Menlo", monospace;
@function dist($x) {
@return modular-scale($x, 1rem, $golden);
@ -55,3 +55,5 @@ $screen-stack: "DejaVu Sans Mono", "Liberation Mono", "Inconsolata", monospace;
@include media($tablet-max) {
.mq-normal-min { display: none !important; }
}
@import "print_override";

@ -26,6 +26,10 @@ a:hover {
display: none !important;
}
.invisible {
visibility: hidden !important;
}
[onclick] {
cursor: pointer;
}

@ -82,7 +82,7 @@
box-shadow: 0 0 6px 0 rgba(black, .6);
border-radius: 5px;
max-width: 80%;
max-width: 600px;
@include media($phone) {
width: calc(100% - #{dist(0)});

@ -126,3 +126,8 @@
}
}
}
.smallpad {
display: block;
padding-bottom: 5px;
}

@ -21,32 +21,49 @@ body.term {
padding: 6px;
display: inline-block;
border: 2px solid #3983CD;
position: relative;
line-height: 0;
font-size: 20px; // some font heights cause visual glitches with some font renderers. This should be configurable.
font-family: $screen-stack;
span {
white-space: pre;
canvas.selectable {
cursor: text;
}
> span {
position: relative;
cursor: pointer;
@include noselect();
// Dummy input field used to open soft keyboard
#softkb-input {
position: absolute;
// compensate for padding
top: 6px;
left: 6px;
width: 1em;
height: 1em;
background: none;
border: none;
resize: none;
overflow: hidden;
opacity: 0;
outline: 0 none !important;
caret-color: transparent;
color: transparent;
@include click-through;
// iOS Safari still shows a caret regardless of the above, so set the font
// size as small as it can be (will show up as a blinking blue pixel)
font-size: 1px;
}
&::before {
content: " ";
}
#touch-select-menu {
display: none;
position: absolute;
// compensate for padding
top: 6px;
left: 6px;
> span {
position:absolute;
left: 0;
z-index: 1;
&.open {
display: block;
}
}
&.noselect {
@include noselect();
}
}
#action-buttons {
@ -118,21 +135,11 @@ body.term {
text-align: center;
}
// Dummy input field used to open android keyboard
#softkb-input {
position: absolute;
top: -9999px;
}
#fu_modal {
align-items: flex-start;
}
#fu_form {
//border: 1px solid #3983CD;
//border-radius: 2px;
//padding: 0 dist(0);
//background: #333e58;
padding: dist(0);
margin-top: 100px; // offset

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

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save