Compare commits

...

404 Commits

Author SHA1 Message Date
Ondřej Hruška f729d91651
minor updates in about.php 6 years ago
Ondřej Hruška ae38110816
added a help section about GPIO API, update DE translations 6 years ago
Ondřej Hruška 2e014f4312
add a form for configuring GPIOs 2, 4, 5 6 years ago
Ondřej Hruška ef05565155
upgrade node packages so build works again, compat w node 10 6 years ago
cpsdqs 86abce86aa
Update German translation 6 years ago
Ondřej Hruška e25523de11
Merge hungarian translations from master to work 7 years ago
Ondřej Hruška 1a05b70b6c
fix some js escaping issues 7 years ago
schneemaier c55ceb1298 Added missing Hungarian translations (#2) 7 years ago
Ondřej Hruška 8b43b1d171
Merge branch 'work' 7 years ago
Ondřej Hruška 36f0f60dbd
added some missing czech translations 7 years ago
Ondřej Hruška a0412c0e9e
Merge branch 'work' 7 years ago
Ondřej Hruška fed47ffda9
update help 7 years ago
Ondřej Hruška 68a467c33e
Merge branch 'double-lines' into work 7 years ago
cpsdqs 681685fa3b
Stop using weird transforms for double-width 7 years ago
cpsdqs 915113a628
Fix incorrect rendering of double-sized lines 7 years ago
Ondřej Hruška 0893b0a268
added update topic for doublelines 7 years ago
Ondřej Hruška d489e99194
fixed double height always being also double width 7 years ago
cpsdqs f61d861883
Make cursor work in double-width lines 7 years ago
cpsdqs 4e9970300b
Add layout support for double size lines 7 years ago
cpsdqs 45766fccce
Add renderer support for double-size lines 7 years ago
cpsdqs 244ee72531
Improve fancy graphics performance 7 years ago
cpsdqs f7f5658685
Make touch & mouse tracking work 7 years ago
cpsdqs f590741d97
Update demo for new protocol 7 years ago
Ondřej Hruška 9cab25ed4d
add info about new OSC to help 7 years ago
cpsdqs e9e0600b24
Remove dangling comma and debug line 7 years ago
Ondřej Hruška 30af1ad2f6
+themeable buttons, +fontFamily, +fontSize, +cursorFgBg 7 years ago
cpsdqs cdfcb6ba9a
Fix reverse video having no effect 7 years ago
cpsdqs e3b279866c
Clean up and comment term/debug 7 years ago
cpsdqs f31d04d279
Remove unused ScreenLayout#window.graphics 7 years ago
cpsdqs 78d32f7fdc Merge branch 'fancy-debug' into work 7 years ago
cpsdqs a83e4549e8
Scroll region is inclusive 7 years ago
cpsdqs 3490e220b5
Round debug tooltip pos, use 1-based coords 7 years ago
cpsdqs 64108302fc
Show last screen update in debug toolbar 7 years ago
cpsdqs ed59b0cb44
Add (back) heartbeat to debug toolbar 7 years ago
cpsdqs ef4c867007
Add fancy debug toolbar 7 years ago
schneemaier ad788db754 Missing translations and spelling corrections (#1) 7 years ago
Ondřej Hruška 445d78d4e1
explain new osc in help 7 years ago
Ondřej Hruška 9ae5e928ac
czech translations updated 7 years ago
Ondřej Hruška 57f7e4b5e0
config field but button count 7 years ago
Ondřej Hruška 8de41f9e4f
Added Hungarian translations by Ákos Schneemaier 7 years ago
Ondřej Hruška 161707fa60
Fix button removal not working well 7 years ago
cpsdqs 0794030210
Use secure protocols if page was loaded securely 7 years ago
Ondřej Hruška cf29deaa0c
fix broken logic in err reporting 7 years ago
Ondřej Hruška c8305dea1a
dont crash with bad url 7 years ago
Ondřej Hruška a96b522ca8
add errmessage 7 years ago
Ondřej Hruška 172e1e0b19
fix spacing at file upload box 7 years ago
Ondřej Hruška 3a4d4b5937
form to backup and restore 7 years ago
Ondřej Hruška 8327ff0108
update front-end for new backend config storage 7 years ago
cpsdqs ce46ad7e96
Add capability to color buttons 7 years ago
cpsdqs e81b2d409c
Add rudimentary and inefficient tooltip 7 years ago
cpsdqs 6d7bd89f0b
Start the fancy-debug rewrite 7 years ago
cpsdqs 9a79537910
Make fancy graphics work with padding 7 years ago
cpsdqs eef42af52b
Fix screen not resizing and debug causing redraw 7 years ago
cpsdqs f84705f376
Only mask to adjacent cells with fancy graphics 7 years ago
cpsdqs c6b59f7094
Remove temporary code 7 years ago
cpsdqs 60a78afdab
Fix soft keyboard not working 7 years ago
cpsdqs 4547ea668a
Make fullscreen mode work again 7 years ago
cpsdqs 50913f3331
German word order 7 years ago
cpsdqs 15cdfa18da
Fix status screen misbehaving 7 years ago
cpsdqs eaec57948e
Fix status screen using worst font 7 years ago
cpsdqs 623ee30544
Fix debug toolbar getting wrong value 7 years ago
cpsdqs 7fe6b35243
Merge branch 'work' into screen-layout 7 years ago
cpsdqs 39277fa958
Add back blinkingCellCount functionality 7 years ago
cpsdqs f53d3390c8
Make debug bar work again and fix cursor artifacts 7 years ago
cpsdqs 73b390d7de
Make screen work again with screen-layout 7 years ago
cpsdqs 3851cd6aa6
Put layout between screen and renderer 7 years ago
Ondřej Hruška 32c889b714
debug toolbar css tuning 7 years ago
cpsdqs 3ef4f0712b
Screen layout! 7 years ago
Ondřej Hruška 9040b1a8a6
make heart larger and animation slower to match real timeout 7 years ago
Ondřej Hruška 6c3dd0c5e8
made heart red 7 years ago
cpsdqs a460b5fe89
Clean up and add a few comments 7 years ago
Ondřej Hruška c3f6b4d851
add default terminal title if load fails 7 years ago
cpsdqs 289cc5aaf2
Remove all refs to external DOM objects in screen 7 years ago
cpsdqs 094727c922
Fix demo being loaded regardless of ESP_PROD 7 years ago
cpsdqs f5bbb60468
Stop using pointers for button labels 7 years ago
cpsdqs 3d93cd3690
Add input queue 7 years ago
cpsdqs a5aa536f89
ScreenParser is now standalone…-able! 7 years ago
cpsdqs 32c6246c10
Fix cursor-hanging not updating properly 7 years ago
cpsdqs 174e6950d0
Add blinking cell counter back 7 years ago
cpsdqs 90a1676084
Remove most references to screen in parser 7 years ago
Ondřej Hruška 180ab41299
Merge branch 'master' into work 7 years ago
cpsdqs 0cfb648ac8
Rename debug_screen to debug, add heartbeat 7 years ago
cpsdqs 735ee58340
Start removing references to screen in parser 7 years ago
Ondřej Hruška d2fabc40f1
add URL Hintergrundbild 7 years ago
Ondřej Hruška 4115decb12
remove line len limit from html minifier 7 years ago
Ondřej Hruška 08b7927bc5
use html minifier from npm, optimize help page size 7 years ago
cpsdqs d154df6360
Stop logging heartbeats 7 years ago
cpsdqs 74ce1de432
Make themes.toHex use getColor, ensure .selectable 7 years ago
cpsdqs 394654099a
Move color mapping to themes 7 years ago
cpsdqs 3cf83d0fc5
Fix derp in e2cdb40 7 years ago
cpsdqs e2cdb4091c
Remove refs to single-instance objects in parser 7 years ago
Ondřej Hruška 855d308962
Merge remote-tracking branch 'origin/work' 7 years ago
Ondřej Hruška bd933657f4
updated help 7 years ago
Ondřej Hruška 7322035fd7
add form field for bg image url default value 7 years ago
Ondřej Hruška 9e62a8ddbb
make it so default bg is used as canvas bg behind the image 7 years ago
Ondřej Hruška 80f0ca7202
fix a megabug 7 years ago
Ondřej Hruška 608a393b63
added backdrop url topic W 7 years ago
Ondřej Hruška 75fb70c083
move attr bits to own file, make Load Failed text look nicer 7 years ago
cpsdqs af5f47d890
Fix call to non-existent method 7 years ago
cpsdqs 3e7a216450
Add TermScreen#backgroundImage 7 years ago
cpsdqs f28fc5094c
Draw nothing if backgroundImage is true 7 years ago
Ondřej Hruška 7fbe736ce2
fix charset parsing code to work with new format in back-end sources 7 years ago
cpsdqs d4931c1f31
Add support for background images in renderer 7 years ago
cpsdqs 09da0690dd
Demo `themes`: Finish basic functionality 7 years ago
cpsdqs 0da19bca30
Start on new `themes` command in demo 7 years ago
Ondřej Hruška 420353efc9
Merge branch 'work' 7 years ago
Ondřej Hruška f8cc9f30d3
oops forgot a console log 7 years ago
Ondřej Hruška b0589ed35c
some cs lang fixes, fixed spacebar pushing buttons 7 years ago
cpsdqs 336849a9bb
[demo] Mouse demo: Fix issues with box rendering 7 years ago
cpsdqs d987f504a2
Fancier mouse demo 7 years ago
cpsdqs fcfd351b6e
[demo] Add Exit to local-echo 7 years ago
Ondřej Hruška 4a032ee3b5
Merge branch 'work' 7 years ago
Ondřej Hruška 8b610aa95b
add mention of nonce to the help page 7 years ago
cpsdqs ce1bda3a70
Fix German translation of word I don't know 7 years ago
cpsdqs 01148465b7
Fix some issues with buttons, use buttons in demo 7 years ago
Ondřej Hruška 5e89ec0271
move bg color to html,body to possibly fix https://github.com/espterm/espterm-firmware/issues/241 7 years ago
Ondřej Hruška 6558e4eba0
Merge branch 'webpack-lang-loader' into work 7 years ago
Ondřej Hruška 1c673a5a71
renamed files to make it more clear what are actual translations in the lang foler 7 years ago
cpsdqs dcdfa511e7
[lang] Use a PHP script instead of parsing PHP 7 years ago
cpsdqs 8e36765c2e
Use webpack to load locale data 7 years ago
cpsdqs 795969842c
Fix upload buttons not canceling events 7 years ago
cpsdqs 3894ffbcab
Show color preview in debug toolbar 7 years ago
cpsdqs b3eaf63d19
Fix German derps 7 years ago
cpsdqs 1af4cd54f3
Update German locale 7 years ago
cpsdqs db8505b75d
Demo: fix sudo crashing if no argument is passed 7 years ago
Ondřej Hruška 2bd167304c
localized upload tool (cs, en) 7 years ago
Ondřej Hruška f080dcc55f
workaround for https://github.com/espterm/espterm-firmware/issues/240 7 years ago
Ondřej Hruška ab5a3cfec3
fixed some stuff on help page being too wide 7 years ago
Ondřej Hruška e34ded0b8c
add lang to css and js output filename to bust cache when changing localized images 7 years ago
Ondřej Hruška 2e64f0e20b
clarify the use of GET instead of POST 7 years ago
Ondřej Hruška 69e5b77447
improve lang 7 years ago
Ondřej Hruška dac15d80a4
update throubleshooting 7 years ago
Ondřej Hruška 6c0b0aa6e1
document networking commands 7 years ago
Ondřej Hruška 6f165da9b6
update api routes 7 years ago
Ondřej Hruška 8d2d5fda42
some reconn timing adjustments 7 years ago
cpsdqs 5eec2f523b
Make debug toolbar a bit nicer 7 years ago
cpsdqs 29b813457c
Improve smooth scrolling 7 years ago
cpsdqs 57cdd04523
Fix wrong screen coords; show flags in binary 7 years ago
cpsdqs 1d4c41ff5a
Add 2x to color triangle, and use camelCase & τ 7 years ago
Ondřej Hruška d41d4ce2d2
lang.. 7 years ago
Ondřej Hruška 0a281414a8
support binary socket messages 7 years ago
Ondřej Hruška 55e2def6e3
add toggle for ascii debug 7 years ago
Ondřej Hruška 979d457b7b
fixed broken wheel number box flipping caused by some dubious optimization 7 years ago
Ondřej Hruška 454b92e878
woo fixed triangle 7 years ago
Ondřej Hruška cf1ef08db4
some triangle cleaning, not fixed 7 years ago
cpsdqs e2e89c6053
Show update frames in debug mode 7 years ago
cpsdqs 822e2694b2
Administratorpasswort -> Systempasswort 7 years ago
cpsdqs 8c12000791
Make strings shorter in lang/de 7 years ago
Ondřej Hruška df575cba61
Merge branch 'czech' into work 7 years ago
Ondřej Hruška 4711d1c17c
added more cs lang 7 years ago
cpsdqs f9e687bf8d
Fix debug toolbar being too wide 7 years ago
Ondřej Hruška 4c42230a44
partial cz translation, improvd lang funcs 7 years ago
cpsdqs 4e65c57529
Fix emit being called on the wrong object 7 years ago
cpsdqs c7b13ddaff
Screen length & frame length are different things 7 years ago
cpsdqs e105344ccc
Remove generated lang.js 7 years ago
cpsdqs 7c91203528
Fix parser derp, show internal info if available 7 years ago
Ondřej Hruška b2c14d34e7
wrap long lines 7 years ago
Ondřej Hruška ffc3a9b3e2
added lang env variable 7 years ago
Ondřej Hruška 58eb4cfbed
Merge branch 'rgb-pickers' into work 7 years ago
Ondřej Hruška e2c57ac80a
oops broke color table 7 years ago
Ondřej Hruška 0468b3adf5
term_conf refactor, add color pickers 7 years ago
cpsdqs 4234c70f52
Localize term connection and fullscreen button 7 years ago
cpsdqs f9b11bdc30
Pass build arguments to build_html and build_js 7 years ago
cpsdqs e72ad6282f
lang/de 7 years ago
cpsdqs d85f6c6ea0 Refactor wifi.js a tiny bit 7 years ago
Ondřej Hruška f47b7a5c40
add toggle for decopt 12, some lang improvements 7 years ago
Ondřej Hruška 6ee7754a6a
Merge branch 'upload-tool-improve' into work 7 years ago
Ondřej Hruška 85a007b32b
upload chunk size configurable 7 years ago
cpsdqs 3fba2d88c4
Dynamic buttons 7 years ago
Ondřej Hruška 73cb0a4b2b
Interpret bold 0-7 as bold 8-15 (matches Xterm reference impl), add SEQ_SKIP 7 years ago
cpsdqs 3de6e621e5
−console.log 7 years ago
cpsdqs 9c047eec82 merge.png 7 years ago
cpsdqs d2dc99b6ff
Add various things to the demo 7 years ago
Ondřej Hruška a25d0a4c05
Fix https://github.com/espterm/espterm-firmware/issues/226 foreach on node list 7 years ago
cpsdqs 276d3af7da
Update demo for new new protocol 7 years ago
Ondřej Hruška cee23ca951
Fix for unpaired surrogates in data codepoints. PROTOCOL CHANGE! 7 years ago
cpsdqs a66e90e3eb merge.jpg 7 years ago
cpsdqs 5245917c88
Update demo for new protocol, add `themes` command 7 years ago
Ondřej Hruška 46099bb749
Fix def fg bg color encoding. Protocol change! 7 years ago
Ondřej Hruška 23fc105754
implement #208 preview foreground palette on configured default bg + other improvements 7 years ago
cpsdqs 61953f608e
Surreptitiously add 24-color support 7 years ago
Ondřej Hruška af723bf783
added Solarized Dark High-Contrast, fix theme numbers for backcompat 7 years ago
Ondřej Hruška c0aa3083ae
Add Solarized Light theme and Wasp preset; Improve preset picker 7 years ago
cpsdqs dc4e585978
Add pseudo-fullscreen mode for iOS 7 years ago
Ondřej Hruška a5a157ad56
removed spammy logging 7 years ago
cpsdqs 83a6c169a1
Fancier debug mode 7 years ago
cpsdqs 4943abb354
Prevent unnecessary full screen redraws 7 years ago
Ondřej Hruška 572dea2370
add tag 0x07 7 years ago
cpsdqs 273fc6a820
Fix #225 7 years ago
cpsdqs 20512bcd7a
Ignore taps if clicking is enabled 7 years ago
cpsdqs bf7863b8fa
Use less Arial 7 years ago
cpsdqs 673358e2ce
Fix remaining issues with scaling 7 years ago
cpsdqs c0527c556e
Fix various issues with padding and screen sizing 7 years ago
cpsdqs cfcac66020
Keep themes in renderer 7 years ago
cpsdqs 1bd6d3ae27
Implement partial screen updates 7 years ago
Ondřej Hruška dbd90cf166
Merge branch 'new-parser' into work 7 years ago
Ondřej Hruška b5c135e505
new parser, may be a bit buggy 7 years ago
cpsdqs 91d270fce3
Draw hanging cursor in the margin 7 years ago
cpsdqs 65eb8b1830
Add powerline triangles 7 years ago
cpsdqs 342433be53
Turns out backdrop-filter works fine in Chrome 7 years ago
cpsdqs aa41cf512e
Add screen margin overlays 7 years ago
cpsdqs 4c1da9aaac
Do padding in canvas instead of CSS 7 years ago
Ondřej Hruška 57a295b544
html minification 7 years ago
cpsdqs ba24c4f967
Remove hackity hack setting debug canvas styles 7 years ago
cpsdqs 036cc23e9b
Fix file permissions 7 years ago
Ondřej Hruška ced948b6eb
Merge branch 'master' into work 7 years ago
Ondřej Hruška f5dd70a6f3
Fix strange behavior with loading screens showing when they should not etc 7 years ago
Ondřej Hruška 18ece41390
some menu cleaning & added overclock toggle 7 years ago
Ondřej Hruška c846cffedb
make confirm msg auto-fade 7 years ago
Ondřej Hruška 64c1ebbf8f
implemented "success messages" for submitted forms (?msg=...) 7 years ago
Ondřej Hruška d831d47f53
show tx power as dBm 7 years ago
Ondřej Hruška 5d907c11ef
grammar v. 2 7 years ago
cpsdqs bbba9ec527
Grammar 7 years ago
Ondřej Hruška ddfc4c01d0
some style and text fixes 7 years ago
Ondřej Hruška d59c0e876a
Merge remote-tracking branch 'origin/work' 7 years ago
cpsdqs 16436a9a98
Make events that can be passive passive 7 years ago
cpsdqs 0b6e4439bc
Possibly fix #198 7 years ago
Ondřej Hruška b18a0f3890
disallow short hex 7 years ago
Ondřej Hruška a9cae0e76a
better ordering of theme conf 7 years ago
Ondřej Hruška a2fbabef66
theme config improvements 7 years ago
Ondřej Hruška 8d7cd8ac19
doc comments 7 years ago
Ondřej Hruška 7ddc64ff01
lower bell volume (i know, yeah... picky) 7 years ago
cpsdqs 8e513cbb70
kb ext bar: mark modifier key as modifier, add Esc 7 years ago
cpsdqs 842d1fd93c
Replace ^C with ^ in soft keyboard extension bar 7 years ago
cpsdqs 310033b911
Make cfg.no_keys work properly 7 years ago
cpsdqs 0b39d19276
Fix #196 7 years ago
cpsdqs 9c4027d2d1
Add “Waiting for content” screen 7 years ago
cpsdqs c68017bd4a
Update demo to work with new protocol 7 years ago
cpsdqs a196f9a9de
Refactor and fix getColor returning undefined 7 years ago
Ondřej Hruška 52bde8c266
Merge branch 'offscale-bg-and-other-monstrosities' into work 7 years ago
Ondřej Hruška 8138fc0bd7
offscale default fg bg support, utf8 data encoding 7 years ago
Ondřej Hruška c744755679 Comsume TAB in demo instead of printing U+FFFD 7 years ago
Ondřej Hruška a72c229ab3 no such thing as inexplicable 7 years ago
cpsdqs e9aa3bfba6
Fix demo having black spots and refactor a bit 7 years ago
cpsdqs 91c17e0fc3
Fix EventEmitter#off not existing 7 years ago
cpsdqs cc28a69d85
Add iOS keyboard shortcut bar 7 years ago
Ondřej Hruška 6c6424877c html-escape access name 7 years ago
Ondřej Hruška 4c11d7a619 add access_name to form 7 years ago
Ondřej Hruška 172a890be2 highlight all labels with for=name in ?err=... 7 years ago
Ondřej Hruška afd8c47a74 adjust pw forms 7 years ago
cpsdqs 9ccf9dd2cf
Move term stuff to term and partially split screen 7 years ago
cpsdqs df4c75b370
Handle all received keydown keys in soft input 7 years ago
cpsdqs 09d3fff59d
Make input 𝘬𝘪𝘯𝘥𝘢 work with Firefox mobile 7 years ago
Ondřej Hruška ddd55c1ae0 added password edit forms & protection options, moved uart cfg to term conf page 7 years ago
Ondřej Hruška c9c8b33b43 ditto in the help page 7 years ago
Ondřej Hruška c0b599ec16 change default debug theme to 1 and make black "30" readable in the preview 7 years ago
Ondřej Hruška 6eac6af319 added ctrl+insert for copy to clipboard 7 years ago
Ondřej Hruška 4ee85af4f4 ctrl+f12 toggles debug mode 7 years ago
Ondřej Hruška e098ceb6ea use U+00XX for unicode in debugbar 7 years ago
cpsdqs eda55a89a7
Add rudimentary debug toolbar 7 years ago
cpsdqs aca60da67d
Fix uglify removing source map 7 years ago
Ondřej Hruška 3dcb3fe972 removed themes from css and added 256color preview in help page 7 years ago
Ondřej Hruška 865efe41f7 visibility api 7 years ago
Ondřej Hruška b51a2d1b54 Merge branch 'webpack' into work 7 years ago
Ondřej Hruška d5bb87cd24 moved themes and color table to themes.js 7 years ago
Ondřej Hruška 91f68c7c95 removed some unused globals 7 years ago
Ondřej Hruška da12d4e7ee some cleaning and made all pages work 7 years ago
cpsdqs d63271714c
Fix ESP_PROD breaking everything 7 years ago
Ondřej Hruška c0414e31b2 findign bsug 7 years ago
cpsdqs 430a9746d8 Merge 7 years ago
cpsdqs 81656b53ac
Minify webpack output 7 years ago
Ondřej Hruška f44a3e222d add --display-modules to webpack so it doesnt hide modules 7 years ago
cpsdqs b8100e162c
Ignore demo when ESP_PROD is in env 7 years ago
cpsdqs ef249ebc79
Make term work with webpack 7 years ago
cpsdqs 540c93a4bd
Add webpack 7 years ago
Ondřej Hruška e016fc9a3a Updated the Thanks section 7 years ago
Ondřej Hruška 953377b3c6 add timezone to the about page (in demo build and local server) 7 years ago
Ondřej Hruška b307ed6568 removed call to undefined 7 years ago
cpsdqs ff72058bfc
Soften beep 7 years ago
cpsdqs 3bb02a5eed
Fix invisible cursor with graphics=1 7 years ago
cpsdqs 49d6c7d1bf
Fix demo not working anymore 7 years ago
cpsdqs 9dee9e1628
Use simple pasting for <90chars or open termUpload 7 years ago
cpsdqs 97e08e71bd
Add back ⌃⇧C shortcut 7 years ago
cpsdqs cd6f4c5887
Added clipboard events and removed keymaster 7 years ago
Ondřej Hruška a54343ec23 implemented changing window title 7 years ago
Ondřej Hruška 65bdb0abd7 implemented socket close/reopen on window blur/focus to save server resources 7 years ago
Ondřej Hruška 2b4b364d0d improved loader and reconnect behavior 7 years ago
cpsdqs e8e84ce4ca Merge? 7 years ago
cpsdqs 2acaf9e97a
Show big “Disconnected” text when disconnected 7 years ago
Ondřej Hruška 37e8441eca Merge branch 'work' of github.com:espterm/espterm-front-end into work 7 years ago
Ondřej Hruška ee004de890 add the remaining quadrants 7 years ago
cpsdqs 92c4c2ff98
Make Term Conn an EventEmitter, add status screen 7 years ago
Ondřej Hruška b33695e543 added 1-quadrants 7 years ago
Ondřej Hruška e60f94cdae Merge branch 'work' of github.com:espterm/espterm-front-end into work 7 years ago
Ondřej Hruška 9bbc0cf7aa reference comments & move const tables init to constructor 7 years ago
cpsdqs c4a2039834
Duplicate code -> EventEmitter 7 years ago
cpsdqs 7b0c1d666f
Use block elems for demo title; make 2593 denser 7 years ago
cpsdqs 0520b043f8
Use rectangles to draw most block elements 7 years ago
cpsdqs 6241b0122f
Demo: Fix caret behaving strangely when in history 7 years ago
cpsdqs 697918775d
Sudo does not work as expected 7 years ago
Ondřej Hruška e3b21adc45 Merge branch 'work' 7 years ago
Ondřej Hruška bb76f87b13 Merge branch 'master' into work (bugfixes) 7 years ago
Ondřej Hruška 6e021402c2 colours and sudo works as expected 7 years ago
Ondřej Hruška d44dce766b added a very important demo command 7 years ago
cpsdqs a9f59488fd
Remove debug code 7 years ago
cpsdqs a8354f2288
Always round DPR to nice value to prevent gaps 7 years ago
Ondřej Hruška 53a6ab4a84 fix LMB not tracked 7 years ago
Ondřej Hruška 83f24cbe34 undo one es6 fix that wasn't really all that great + some comments cleaning and removed one forgotten console.log 7 years ago
cpsdqs 54d314517f
ES6ify some things 7 years ago
cpsdqs f6b47ee358
Don't make moving the cursor update input 7 years ago
cpsdqs 6ac00ad747 Add command history to demo 7 years ago
cpsdqs c767b9d613 Merge the rest of 'fancy-demo' into work 7 years ago
cpsdqs 6725734900
Also add 0x05 and 0x06 to ignored 7 years ago
Ondřej Hruška d7551c3ea9 Merge branch 'work' 7 years ago
Ondřej Hruška 72279bf035 Revert "fix lib being added to the package twice lol" 7 years ago
Ondřej Hruška 891a44624e Merge branch 'work', rel 1.1 7 years ago
Ondřej Hruška b2174fc528 fix lib being added to the package twice lol 7 years ago
Ondřej Hruška 9488becd81 some build script tuning, exclude demo script for prod build 7 years ago
Ondřej Hruška b219447e4f Merge branch 'fancy-demo' into work 7 years ago
Ondřej Hruška 420761d343 Merge branch 'help-page-update' into work 7 years ago
Ondřej Hruška da738838e5 updated help page 7 years ago
Ondřej Hruška e23191e704 add print stylesheet 7 years ago
Ondřej Hruška 6193634a33 split build script for easier use with phpstorm file watchers 7 years ago
cpsdqs 9affe93ae5
Add mouse demo 7 years ago
cpsdqs e1a761cd7f
r a i n b o w 7 years ago
cpsdqs 720a9ecaa1
Add cursor and theme commands 7 years ago
cpsdqs 0875182d38
Rename print-demo to info and make it print slowly 7 years ago
cpsdqs cc1dae16bc
Add commands and things 7 years ago
Ondřej Hruška dcd2cc1173 fix https://github.com/espterm/espterm-firmware/issues/148 7 years ago
Ondřej Hruška 9db61f0c1a cache busting for js and css files 7 years ago
Ondřej Hruška f6ab97a886 improved error reporting in forms 7 years ago
Ondřej Hruška 58dd0c929f implemented bracketed paste for file upload 7 years ago
Ondřej Hruška fd2a6df245 added git hashes to about page 7 years ago
Ondřej Hruška bdadb9b532 added option to not capture f5,f11,f12 7 years ago
Ondřej Hruška 8548c619d8 remove mysterious gap under canvas caused by line height 7 years ago
Ondřej Hruška 52a5d79691 added doc comment to palette setter so phpstorm doesn't throw a fit 7 years ago
Ondřej Hruška f0da18890f Workaround for the see-through gaps bug 7 years ago
cpsdqs 8d994009fb
Add local-echo 7 years ago
Ondřej Hruška 2b93cedd3f remove junk files from outdir in prod build 7 years ago
cpsdqs 758d3c58a6
Add fake screenfetch and clear 7 years ago
Ondřej Hruška 7aef4e2ef7 fix a typo in help 7 years ago
cpsdqs ff8f2cef81
Add basic shell 7 years ago
cpsdqs afd46544a6
Fix ⌥← and ⌥→ sending wrong codes 7 years ago
cpsdqs c7a5215fe1
Add basic terminal echoing input 7 years ago
Ondřej Hruška 322b0dab87 Remove False Gods 7 years ago
Ondřej Hruška affe087d79 Merge branch 'fix-fule-upload' into work. horrible 7 years ago
cpsdqs 259b9f05e9
Fix upload not working 7 years ago
Ondřej Hruška 2f0d0187a1 add js error handler with notify balloon, fix file upload 7 years ago
cpsdqs 4565469216
Typo. 7 years ago
cpsdqs 2c3f081d99
Add fullscreen button 7 years ago
Ondřej Hruška 3e4394f0cb fix server.sh 7 years ago
cpsdqs acae3992f5
Restructuring 7 years ago
cpsdqs e923fa1746
Fix chibi.js not working without extend 7 years ago
cpsdqs 2b9258a587
Remove unused utils 7 years ago
cpsdqs 5dab1c649f
Use fewer global variables in term* and softkb 7 years ago
cpsdqs 8f0d6c7873
Prevent accidental h1 misidentification 7 years ago
Ondřej Hruška e3944df189 Merge branch 'canvas' into work 7 years ago
cpsdqs 190b1e6422
Fix #151 7 years ago
cpsdqs 2d22efc8f3
Fix #120 7 years ago
cpsdqs 166003cd0e
Fix input events not working on iOS 7 years ago
cpsdqs 3e743ef397
Document TermScreen 7 years ago
cpsdqs f74e689196
Fix cursor style switch not redrawing cell 7 years ago
cpsdqs 1f07aa9886
Fix what 2febe19 broke 7 years ago
cpsdqs 2febe199e4
Don't overwrite window.screen 7 years ago
cpsdqs c82a955dfc
Fix issue with cursor timer 7 years ago
cpsdqs 0c23da7bf8
Fancy graphics should be enabled at graphics=1 7 years ago
cpsdqs 5d2e8d845a
Actually fix #142 7 years ago
cpsdqs 4f58900aac
Fix #142 and #141 7 years ago
cpsdqs ec270c3fdf
Move screen init to term.js 7 years ago
cpsdqs de897f013c
Add fancy graphics 7 years ago
cpsdqs 16f37aafd5
Improve visual debug mode 7 years ago
cpsdqs d5f6bf2dfa
Fix invisible cursors 7 years ago
Ondřej Hruška a78db76327 update demo content for github pages 7 years ago
Ondřej Hruška 810782a7f2 new colors decoding 7 years ago
Ondřej Hruška c07a6162f2 Merge branch 'canvas-babel' into canvas 7 years ago
Ondřej Hruška d1fd0b9f95 oops completely broke high unicode 7 years ago
Ondřej Hruška 0a975e8801 babel, sourcemaps, fixed some uglify errors + a crash on wifi page 7 years ago
cpsdqs c354ec871f
Remove strange comment 7 years ago
cpsdqs e187cf545b
Ensure blink style is updated by just setting text 7 years ago
Ondřej Hruška 6eebc781ed enhance draw debuggin + do not blink if no cells are blinky 7 years ago
Ondřej Hruška 03637dd43e do not redraw in blink timer if blinking is turned off 7 years ago
cpsdqs ab23d80799
Increase update check radius for wide characters 7 years ago
Ondřej Hruška 11d9a40951 eslint tuning, re-enabled some rules 7 years ago
cpsdqs 4731a259dd
Possibly fix #121 7 years ago
Ondřej Hruška 6cde517c55 comments and removed unused flag from some old version 7 years ago
Ondřej Hruška 23fe523412 Merge branch 'canvas' of github.com:espterm/espterm-front-end into canvas 7 years ago
Ondřej Hruška 912f69c725 added editing commands 7 years ago
cpsdqs f6f804bd14
Remove String#format in favor of template strings 7 years ago
cpsdqs f059a35277
Add eslintignore and adhere or be ignored 7 years ago
Ondřej Hruška 87ed5491a4 make themes work again 7 years ago
Ondřej Hruška 086532ff4c fix #122 7 years ago
Ondřej Hruška 79664f56a6 eslint all the things 7 years ago
Ondřej Hruška 7e1611ff7a eslit disable false positives and camelcase requirement 7 years ago
Ondřej Hruška 135a4dfe8e added eslint official file 7 years ago
cpsdqs 7207ce9fd7
Add 256color and truecolor in TermScreen#getColor 7 years ago
cpsdqs e28d338eca
Use yarn instead of npm 7 years ago
Ondřej Hruška 8c10605156 update build script from master 7 years ago
cpsdqs 062609c5d8
Add license 7 years ago
Ondřej Hruška 2b3a5b4b55 add notification fallback to use Notify.show() 7 years ago
Ondřej Hruška 2921566cc3 add G to the select in term_conn 7 years ago
Ondřej Hruška 0fefa529b1 updated package json 7 years ago
Ondřej Hruška 55336d4f5c stop node sass putting garbage in the css file 7 years ago
cpsdqs ba6e5f1723
Switch statements require break (sigh) 7 years ago
cpsdqs 5cbb3ea2c9
Make G (growl) send a Notification 7 years ago
cpsdqs ec4897ae2a
Add overline style (attr bit 7) 7 years ago
cpsdqs b18aca1438
Some sort of merge...? Cursor styles work fine now 7 years ago
cpsdqs 72069d2219 Load new 3B attribute and cursor styles 7 years ago
Ondřej Hruška cbc21e9e3b Merge branch 'canvas' of github.com:espterm/espterm-front-end into canvas 7 years ago
Ondřej Hruška 036a58ce12 updated to latest backend, cursor variants setting, reformat some files 7 years ago
cpsdqs a7a2daee11
Prevent horrible screeching when BEL is spammed 7 years ago
cpsdqs b3608272a0
Fix #119 7 years ago
Ondřej Hruška 963bfce9cc updated build script 7 years ago
Ondřej Hruška 30f6428af0 updated unified build script 7 years ago
Ondřej Hruška 7d08250f46 Import canvas version from old ESPTerm repo 7 years ago
  1. 14
      .babelrc
  2. 9
      .eslintignore
  3. 191
      .eslintrc
  4. 2
      .gitignore
  5. 4
      README.md
  6. 16
      _build_assets.sh
  7. 7
      _build_common.sh
  8. 13
      _build_css.sh
  9. 6
      _build_html.sh
  10. 7
      _build_js.sh
  11. 47
      _debug_replacements.php
  12. 13
      _dev_router.php
  13. 4
      _pages.php
  14. 76
      base.php
  15. 27
      build.sh
  16. 50
      build_html.php
  17. 85
      compile_html.php
  18. 0
      css/.gitkeep
  19. 23
      dump_js_lang.php
  20. BIN
      fontello/fontello.zip
  21. 0
      js/.gitkeep
  22. 205
      js/appcommon.js
  23. 18
      js/index.js
  24. 5
      js/lang.js
  25. 4
      js/lib/chibi.js
  26. 118
      js/lib/color_utils.js
  27. 572
      js/lib/colortriangle.js
  28. 63
      js/lib/polyfills.js
  29. 44
      js/modal.js
  30. 65
      js/notif.js
  31. 1144
      js/td/WebAudio.d.ts
  32. 112
      js/term/buttons.js
  33. 239
      js/term/connection.js
  34. 539
      js/term/debug.js
  35. 1381
      js/term/demo.js
  36. 250
      js/term/index.js
  37. 461
      js/term/input.js
  38. 590
      js/term/screen.js
  39. 15
      js/term/screen_attr_bits.js
  40. 285
      js/term/screen_layout.js
  41. 416
      js/term/screen_parser.js
  42. 936
      js/term/screen_renderer.js
  43. 167
      js/term/soft_keyboard.js
  44. 151
      js/term/themes.js
  45. 182
      js/term/upload.js
  46. 106
      js/term_conf.js
  47. 60
      js/utils.js
  48. 173
      js/wifi.js
  49. 189
      jssrc/appcommon.js
  50. 310
      jssrc/keymaster.js
  51. 8
      jssrc/lang.js
  52. 44
      jssrc/modal.js
  53. 32
      jssrc/notif.js
  54. 6
      jssrc/term.js
  55. 134
      jssrc/term_conn.js
  56. 264
      jssrc/term_input.js
  57. 380
      jssrc/term_screen.js
  58. 146
      jssrc/term_upload.js
  59. 161
      jssrc/utils.js
  60. 163
      jssrc/wifi.js
  61. 14
      lang/_js-dump.php
  62. 54
      lang/_js-lang-loader.js
  63. 21
      lang/common.php
  64. 291
      lang/cs.php
  65. 291
      lang/de.php
  66. 248
      lang/en.php
  67. 292
      lang/hu.php
  68. 12
      lang/js-keys.js
  69. 21
      package.json
  70. 5
      pages/_cfg_menu.php
  71. 12
      pages/_head.php
  72. 49
      pages/about.php
  73. 11
      pages/cfg_network.php
  74. 189
      pages/cfg_system.php
  75. 368
      pages/cfg_term.php
  76. 10
      pages/cfg_wifi.php
  77. 20
      pages/cfg_wifi_conn.php
  78. 12
      pages/help.php
  79. 28
      pages/help/charsets.php
  80. 18
      pages/help/cmd_cursor.php
  81. 92
      pages/help/cmd_d2d.php
  82. 103
      pages/help/cmd_screen.php
  83. 136
      pages/help/cmd_system.php
  84. 2
      pages/help/input.php
  85. 39
      pages/help/iocontrol.php
  86. 15
      pages/help/screen_behavior.php
  87. 123
      pages/help/sgr_colors.php
  88. 20
      pages/help/sgr_styles.php
  89. 7
      pages/help/troubleshooting.php
  90. 84
      pages/term.php
  91. 6
      sass/_fontello.scss
  92. 0
      sass/_grid-settings.scss
  93. 0
      sass/_normalize.scss
  94. 115
      sass/_print_override.scss
  95. 0
      sass/_utils.scss
  96. 7
      sass/app.scss
  97. 0
      sass/form/_buttons.scss
  98. 0
      sass/form/_fancy_button_mixins.scss
  99. 8
      sass/form/_form_elements.scss
  100. 39
      sass/form/_form_layout.scss
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,14 @@
{
"presets": [
["env", {
"targets": {
"browsers": [
"last 2 versions",
"> 4%",
"ie 11",
"safari 8"
]
}
}]
]
}

@ -0,0 +1,9 @@
# possibly minified output
out/**/*
# libraries
js/lib/chibi.js
js/lib/polyfills.js
# php generated file
js/lang.js

@ -0,0 +1,191 @@
{
"parserOptions": {
"ecmaVersion": 8,
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
},
"sourceType": "module"
},
"env": {
"es6": true,
"node": true
},
"plugins": [
"import",
"node",
"promise",
"standard"
],
"globals": {
"document": false,
"navigator": false,
"window": false
},
"rules": {
"accessor-pairs": "error",
"arrow-spacing": ["error", { "before": true, "after": true }],
"block-spacing": ["error", "always"],
"brace-style": ["warn", "1tbs", { "allowSingleLine": true }],
"camelcase": ["off", { "properties": "never" }],
"comma-dangle": ["error", {
"arrays": "never",
"objects": "never",
"imports": "never",
"exports": "never",
"functions": "never"
}],
"comma-spacing": ["error", { "before": false, "after": true }],
"comma-style": ["error", "last"],
"constructor-super": "error",
"curly": ["error", "multi-line"],
"dot-location": ["error", "property"],
"eol-last": "error",
"eqeqeq": ["error", "smart"],
"func-call-spacing": ["error", "never"],
"generator-star-spacing": ["error", { "before": true, "after": true }],
"handle-callback-err": ["error", "^(err|error)$" ],
"indent": ["error", 2, { "SwitchCase": 1 }],
"key-spacing": ["error", { "beforeColon": false, "afterColon": true }],
"keyword-spacing": ["error", { "before": true, "after": true }],
"new-cap": ["error", { "newIsCap": true, "capIsNew": false }],
"new-parens": "error",
"no-array-constructor": "error",
"no-caller": "error",
"no-class-assign": "error",
"no-compare-neg-zero": "error",
"no-cond-assign": "error",
"no-const-assign": "error",
"no-constant-condition": ["error", { "checkLoops": false }],
"no-control-regex": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-dupe-args": "error",
"no-dupe-class-members": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-empty-character-class": "error",
"no-empty-pattern": "error",
"no-eval": "error",
"no-ex-assign": "error",
"no-extend-native": "warn",
"no-extra-bind": "error",
"no-extra-boolean-cast": "error",
"no-extra-parens": ["error", "functions"],
"no-fallthrough": "error",
"no-floating-decimal": "error",
"no-func-assign": "error",
"no-global-assign": "error",
"no-implied-eval": "error",
"no-inner-declarations": ["error", "functions"],
"no-invalid-regexp": "error",
"no-irregular-whitespace": "error",
"no-iterator": "error",
"no-label-var": "error",
"no-labels": ["error", { "allowLoop": false, "allowSwitch": false }],
"no-lone-blocks": "warn",
"no-mixed-operators": ["error", {
"groups": [
["==", "!=", "===", "!==", ">", ">=", "<", "<="],
["&&", "||"],
["in", "instanceof"]
],
"allowSamePrecedence": true
}],
"no-mixed-spaces-and-tabs": "error",
"no-multi-spaces": "warn",
"no-multi-str": "error",
"no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 0 }],
"no-negated-in-lhs": "error",
"no-new": "error",
"no-new-func": "error",
"no-new-object": "error",
"no-new-require": "error",
"no-new-symbol": "error",
"no-new-wrappers": "error",
"no-obj-calls": "error",
"no-octal": "error",
"no-octal-escape": "error",
"no-path-concat": "error",
"no-proto": "error",
"no-redeclare": "error",
"no-regex-spaces": "error",
"no-return-assign": ["error", "except-parens"],
"no-return-await": "error",
"no-self-assign": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-shadow-restricted-names": "error",
"no-sparse-arrays": "error",
"no-tabs": "error",
"no-template-curly-in-string": "error",
"no-this-before-super": "error",
"no-throw-literal": "error",
"no-trailing-spaces": "off",
"no-undef": "warn",
"no-undef-init": "error",
"no-unexpected-multiline": "error",
"no-unmodified-loop-condition": "error",
"no-unneeded-ternary": ["error", { "defaultAssignment": false }],
"no-unreachable": "error",
"no-unsafe-finally": "error",
"no-unsafe-negation": "error",
"no-unused-expressions": ["warn", { "allowShortCircuit": true, "allowTernary": true, "allowTaggedTemplates": true }],
"no-unused-vars": ["warn", { "vars": "local", "args": "none", "ignoreRestSiblings": true }],
"no-use-before-define": ["error", { "functions": false, "classes": false, "variables": false }],
"no-useless-call": "error",
"no-useless-computed-key": "error",
"no-useless-constructor": "error",
"no-useless-escape": "error",
"no-useless-rename": "error",
"no-useless-return": "error",
"no-whitespace-before-property": "error",
"no-with": "error",
"object-property-newline": ["error", { "allowMultiplePropertiesPerLine": true }],
"one-var": ["error", { "initialized": "never" }],
"operator-linebreak": ["error", "after", { "overrides": { "?": "before", ":": "before" } }],
"padded-blocks": ["off", { "blocks": "never", "switches": "never", "classes": "never" }],
"prefer-promise-reject-errors": "error",
"quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": true }],
"rest-spread-spacing": ["error", "never"],
"semi": ["error", "never"],
"semi-spacing": ["error", { "before": false, "after": true }],
"space-before-blocks": ["error", "always"],
"space-before-function-paren": ["error", "always"],
"space-in-parens": ["error", "never"],
"space-infix-ops": "error",
"space-unary-ops": ["error", { "words": true, "nonwords": false }],
"spaced-comment": ["error", "always", {
"line": { "markers": ["*package", "!", "/", ","] },
"block": { "balanced": true, "markers": ["*package", "!", ",", ":", "::", "flow-include"], "exceptions": ["*"] }
}],
"symbol-description": "error",
"template-curly-spacing": ["error", "never"],
"template-tag-spacing": ["error", "never"],
"unicode-bom": ["error", "never"],
"use-isnan": "error",
"valid-typeof": ["error", { "requireStringLiterals": true }],
"wrap-iife": ["error", "any", { "functionPrototypeMethods": true }],
"yield-star-spacing": ["error", "both"],
"yoda": ["error", "never"],
"import/export": "error",
"import/first": "error",
"import/no-duplicates": "error",
"import/no-webpack-loader-syntax": "error",
"node/no-deprecated-api": "error",
"node/process-exit-as-throw": "error",
"promise/param-names": "error",
"standard/array-bracket-even-spacing": ["error", "either"],
"standard/computed-property-even-spacing": ["error", "even"],
"standard/no-callback-literal": "error",
"standard/object-curly-even-spacing": ["error", "either"]
}
}

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,7 @@
#!/bin/bash
export FRONT_END_HASH=$(git rev-parse --short HEAD)
if [ -z "$ESP_LANG" ]; then
export ESP_LANG=en
fi

@ -0,0 +1,13 @@
#!/bin/bash
source "_build_common.sh"
echo 'Building CSS...'
if [[ $ESP_PROD ]]; then
stylearg=compressed
else
stylearg=expanded
fi
mkdir -p out/css
npm run sass -- --output-style ${stylearg} sass/app.scss "out/css/app.$FRONT_END_HASH-$ESP_LANG.css"

@ -0,0 +1,6 @@
#!/bin/bash
source "_build_common.sh"
echo 'Building HTML...'
php ./compile_html.php $@

@ -0,0 +1,7 @@
#!/bin/bash
source "_build_common.sh"
mkdir -p out/js
echo 'Processing JS...'
npm run webpack

@ -7,28 +7,42 @@
*/
$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',
'title' => ESP_DEMO ? 'ESPTerm Web UI Demo' : 'ESPTerm local debug',
'btn1' => 'OK',
'btn2' => 'Cancel',
'btn3' => '',
'btn4' => '',
'btn5' => 'Help',
'bm1' => '01,'.ord('y'),
'bm2' => '01,'.ord('n'),
'bm3' => '',
'bm4' => '',
'bm5' => '05',
'bc1' => '',
'bc2' => '',
'bc3' => '',
'bc4' => '',
'bc5' => '',
'button_count' => 5,
'labels_seq' => ESP_DEMO ? 'TESPTerm Web UI DemoOKCancelHelp' : 'TESPTerm local debugOKCancelHelp',
'want_all_fn' => '0',
'parser_tout_ms' => 10,
'display_tout_ms' => 15,
@ -52,10 +66,13 @@ return [
'vers_fw' => $vers,
'date' => date('Y-m-d'),
'time' => date('G:i'),
'time' => date('G:i')." ".TIMEZONE,
'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,
'ap_dhcp_time' => '120',
'ap_dhcp_start' => '192.168.4.100',
@ -71,16 +88,26 @@ return [
'sta_mac' => '5c:cf:7f:02:74:51',
'ap_mac' => '5e:cf:7f:02:74:51',
'term_width' => '80',
'term_height' => '25',
'gpio2_conf' => '0',
'gpio4_conf' => '1',
'gpio5_conf' => '1',
'width' => '80',
'height' => '25',
'default_bg' => '0',
'default_fg' => '7',
'show_buttons' => '1',
'show_config_links' => '1',
'font_stack' => '',
'font_size' => '20',
'uart_baud' => 115200,
'uart_baudrate' => 115200,
'uart_stopbits' => 1,
'uart_parity' => 2,
'theme' => 0,
'pwlock' => 0,
'access_name' => 'espterm',
'allow_decopt_12' => 0,
];

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

@ -41,7 +41,9 @@ pg('help', 'cfg page-help', 'help', '/help');
pg('about', 'cfg page-about', 'about', '/about');
pg('term', 'term', '', '/', 'title.term');
pg('reset_screen', 'api', '', '/system/cls', 'title.term');
pg('reset_screen', 'api', '', '/api/v1/clear');
pg('ini_export', 'api', '', '/cfg/system/export');
pg('ini_import', 'api', '', '/cfg/system/import');
pg('index', 'api', '', '/', '');

@ -12,20 +12,32 @@ if (!empty($argv[1])) {
parse_str($argv[1], $_GET);
}
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';
define('TIMEZONE', trim(shell_exec('date +%Z'))); // for replacements
$prod = defined('STDIN');
define('DEBUG', !$prod);
$root = DEBUG ? json_encode(ESP_IP) : 'location.host';
// Resolve hostname for ajax etc
$root = 'location.host';
if (!file_exists(__DIR__ . '/_env.php')) {
if (DEBUG) {
die("No _env.php found! Copy _env.php.example</b> to <b>_env.php</b> and check the settings inside!");
}
} else {
if (DEBUG) {
require_once __DIR__ . '/_env.php';
$root = json_encode(ESP_IP);
}
}
define('JS_WEB_ROOT', $root);
define('ESP_PROD', (bool)getenv('ESP_PROD'));
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": {
@ -47,9 +59,11 @@ APS
);
}
define('LOCALE', isset($_GET['locale']) ? $_GET['locale'] : 'en');
define('LOCALE', isset($_GET['locale']) ? $_GET['locale'] : (getenv('ESP_LANG') ?: 'en'));
$_messages = require(__DIR__ . '/lang/' . LOCALE . '.php');
$_messages_fallback = require(__DIR__ . '/lang/en.php');
$_messages_common = require(__DIR__ . '/lang/common.php');
$_pages = require(__DIR__ . '/_pages.php');
define('APP_NAME', 'ESPTerm');
@ -85,8 +99,26 @@ function je($s)
function tr($key)
{
global $_messages;
return isset($_messages[$key]) ? $_messages[$key] : ('??' . $key . '??');
global $_messages, $_messages_fallback, $_messages_common;
if (isset($_messages[$key])) {
$str = $_messages[$key];
}
else if (isset($_messages_fallback[$key])) {
$str = $_messages_fallback[$key];
}
else if (isset($_messages_common[$key])) {
$str = $_messages_common[$key];
}
else{
ob_end_clean();
die('??' . $key . '??');
}
// allow tildes in translation
$str = preg_replace('/(?<=[^ \\\\])~(?=[^ ])/', '&nbsp;', $str);
$str = str_replace('\~', '~', $str);
return $str;
}
/** Like eval, but allows <?php and ?> */
@ -119,7 +151,8 @@ if (!function_exists('load_esp_charsets')) {
if (! file_exists($chsf)) {
return [
'!! ERROR: `../user/character_sets.h` not found !!' => [
['65', 'A', '&'],
'start' => 65,
'chars' => ['&'],
],
];
}
@ -141,20 +174,31 @@ if (!function_exists('load_esp_charsets')) {
$rows = array_map('trim', $rows);
foreach($rows as $j => $v) {
$literal = false;
if (strpos($v, '0x') === 0) {
// hexa codes
$v = substr($v, 2);
$v = hexdec($v);
} else if (strpos($v, 'u\'\\0\'') === 0) {
// zero
$v = 0;
} else if (strpos($v, 'u\'') === 0) {
// utf8 literals
$v = mb_substr($v, 2, 1, 'utf-8');
$literal = true;
} else {
$v = intval($v);
}
$ascii = $start+$j;
$table[] = [
$ascii,
chr($ascii),
utf8($v==0? $ascii :$v),
];
$table[] = $literal ? $v : utf8($v==0? $ascii :$v);
}
$cps[$name] = $table;
$obj = [
'start' => $start,
'chars' => $table,
];
$cps[$name] = $obj;
}
return $cps;
}

@ -1,25 +1,14 @@
#!/bin/bash
echo "Packing JS..."
cd $(dirname $0)
cat jssrc/chibi.js \
jssrc/keymaster.js \
jssrc/utils.js \
jssrc/modal.js \
jssrc/notif.js \
jssrc/appcommon.js \
jssrc/lang.js \
jssrc/wifi.js \
jssrc/term_* \
jssrc/term.js > js/app-full.js
source "_build_common.sh"
yuicompressor js/app-full.js > js/app.js
rm -fr out/*
echo "Building CSS..."
./_build_css.sh
./_build_js.sh $@
./_build_html.sh $@
./_build_assets.sh
sass --style=compressed sass/app.scss css/app.css
echo "Building HTML..."
php ./build_html.php
echo "ESPTerm front-end ready"
echo 'ESPTerm front-end ready'

@ -1,50 +0,0 @@
<?php
require_once __DIR__ . '/base.php';
function process_html($s) {
$pattern = '/<!--(.*)-->/Uis';
$s = preg_replace($pattern, '', $s);
$pattern = '/(?:(?:\/\*(?:[^*]|(?:\*+[^*\/]))*\*+\/)|(?:(?<!\:|\\\|\'|\")\/\/.*))/';
$s = preg_replace($pattern, '', $s);
$pattern = '/\s+/s';
$s = preg_replace($pattern, ' ', $s);
return $s;
}
$no_tpl_files = ['help', 'cfg_wifi_conn'];
$dest = __DIR__ . '/out/';
ob_start();
foreach($_pages as $_k => $p) {
if ($p->bodyclass == 'api') {
if (ESP_DEMO) {
$target = 'term.html';
echo "Generating: ~$_k.html -> $target\n";
$s = "<!DOCTYPE HTML><meta http-equiv=\"refresh\" content=\"0;url=$target\">";
} else {
continue;
}
} else {
echo "Generating: $_k ($p->title)\n";
$_GET['page'] = $_k;
ob_flush(); // print the message
ob_clean(); // clean up
include(__DIR__ . '/index.php');
$s = ob_get_contents(); // grab the output
// remove newlines and comments
// as tests have shown, it saves just a couple kilobytes,
// making it not a very big improvement at the expense of ugly html.
// $s = process_html($s);
ob_clean();
} // clean up
$of = $dest . $_k . ((in_array($_k, $no_tpl_files)||ESP_DEMO) ? '.html' : '.tpl');
file_put_contents($of, $s); // write to a file
}
ob_flush();

@ -0,0 +1,85 @@
<?php
require_once __DIR__ . '/base.php';
function process_html($s) {
$pattern = '/<!--(.*)-->/Uis';
$s = preg_replace($pattern, '', $s);
$pattern = '/(?:(?:\/\*(?:[^*]|(?:\*+[^*\/]))*\*+\/)|(?:(?<!\:|\\\|\'|\")\/\/.*))/';
$s = preg_replace($pattern, '', $s);
$pattern = '/\s+/s';
$s = preg_replace($pattern, ' ', $s);
return $s;
}
$no_tpl_files = ['help', 'cfg_wifi_conn'];
$dest = __DIR__ . '/out/';
ob_start();
foreach($_pages as $_k => $p) {
if ($p->bodyclass == 'api') {
if (ESP_DEMO) {
echo "Generating: ~$_k.html (bounce)\n";
if ($_k=='index') {
$s = "<!DOCTYPE HTML><meta http-equiv=\"refresh\" content=\"0;url=term.html\">";
}
else {
$s = "<!DOCTYPE HTML>
<script>
var ref = document.referrer;
var qat = document.referrer.indexOf('?');
if (qat !== -1) ref = ref.substring(0, qat)
location.href=ref+'?msg=Request ignored, this is a demo.';
</script>";
}
} else {
continue;
}
} else {
echo "Generating: $_k ($p->title)\n";
$_GET['page'] = $_k;
ob_flush(); // print the message
ob_clean(); // clean up
include(__DIR__ . '/index.php');
$s = ob_get_contents(); // grab the output
// remove newlines and comments
// as tests have shown, it saves just a couple kilobytes,
// making it not a very big improvement at the expense of ugly html.
// $s = process_html($s);
ob_clean();
}
$outputPath = $dest . $_k . ((in_array($_k, $no_tpl_files)||ESP_DEMO) ? '.html' : '.tpl');
if (file_exists($outputPath)) unlink($outputPath);
if (ESP_PROD) {
$tmpfile = tempnam('/tmp', 'espterm').'.html';
file_put_contents($tmpfile, $s);
system('npm run html-minifier --'.
' --remove-optional-tags'.
' --remove-script-type-attributes'.
' --remove-style-link-type-attributes'.
' --remove-comments'.
' --collapse-whitespace'.
' --collapse-boolean-attributes'.
' --html5'.
//' --max-line-length 120'.
' -o '.escapeshellarg($outputPath).
' '.escapeshellarg($tmpfile), $rv);
// fallback if minify is not installed
if (!file_exists($outputPath)) file_put_contents($outputPath, $s);
} else {
file_put_contents($outputPath, $s);
}
}
ob_flush();

@ -1,23 +0,0 @@
<?php
/* This script is run on demand to generate JS version of tr() */
require_once __DIR__ . '/base.php';
$selected = [
'wifi.connected_ip_is',
'wifi.not_conn',
'wifi.enter_passwd',
'wifi.passwd_saved',
];
$out = [];
foreach ($selected as $key) {
$out[$key] = $_messages[$key];
}
file_put_contents(__DIR__. '/jssrc/lang.js',
"// Generated from PHP locale file\n" .
'var _tr = ' . json_encode($out, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) . ";\n\n" .
"function tr(key) { return _tr[key] || '?'+key+'?'; }\n"
);

Binary file not shown.

@ -0,0 +1,205 @@
const $ = require('./lib/chibi')
const { mk, qs, qsa, cr } = require('./utils')
const modal = require('./modal')
const notify = require('./notif')
/** Global generic init */
$.ready(function () {
// Opening menu on mobile / narrow screen
function menuOpen () {
$('#menu').toggleClass('expanded')
}
$('#brand')
.on('click', menuOpen)
.on('keypress', cr(menuOpen))
// Checkbox UI (checkbox CSS and hidden input with int value)
$('.Row.checkbox').forEach(function (x) {
let inp = x.querySelector('input')
let box = x.querySelector('.box')
$(box).toggleClass('checked', inp.value)
let hdl = function () {
inp.value = 1 - inp.value
$(box).toggleClass('checked', inp.value)
}
$(x).on('click', hdl).on('keypress', cr(hdl))
})
// Expanding boxes on mobile
$('.Box.mobcol,.Box.fold').forEach(function (x) {
let h = x.querySelector('h2')
let hdl = function () {
if ($(x).hasClass('d-expanded')) {
$(x).removeClass('d-expanded')
} else {
$(x).toggleClass('expanded')
}
}
$(h).on('click', hdl).on('keypress', cr(hdl))
})
$('form').forEach(function (x) {
$(x).on('keypress', function (e) {
if ((e.keyCode === 10 || e.keyCode === 13) && e.ctrlKey) {
x.submit()
}
})
})
// loader dots...
setInterval(function () {
$('.anim-dots').each(function (x) {
let $x = $(x)
let dots = $x.html() + '.'
if (dots.length === 5) dots = '.'
$x.html(dots)
})
}, 1000)
// flipping number boxes with the mouse wheel
$('input[type=number]').on('wheel', function (e) {
let $this = $(this)
let val = +$this.val()
if (isNaN(val)) val = 1
const step = +($this.attr('step') || 1)
const min = +$this.attr('min')
const max = +$this.attr('max')
if (e.deltaY > 0) {
val += step
} else {
val -= step
}
if (Number.isFinite(min)) val = Math.max(val, +min)
if (Number.isFinite(max)) val = Math.min(val, +max)
$this.val(val)
if ('createEvent' in document) {
let evt = document.createEvent('HTMLEvents')
evt.initEvent('change', false, true)
$this[0].dispatchEvent(evt)
} else {
$this[0].fireEvent('onchange')
}
e.preventDefault()
})
try {
do {
let msgAt, box
// populate the form errors box from GET arg ?err=...
// (a way to pass errors back from server via redirect)
msgAt = window.location.search.indexOf('err=')
if (msgAt !== -1 && qs('.Box.errors')) {
let errs = decodeURIComponent(window.location.search.substr(msgAt + 4)).split(',')
let humanReadableErrors = []
errs.forEach(function (er) {
if (er.length === 0) return
let lbls = qsa('label[for="' + er + '"]')
if (lbls && lbls.length > 0) {
for (let i = 0; i < lbls.length; i++) {
let lbl = lbls[i]
lbl.classList.add('error')
if (i === 0) humanReadableErrors.push(lbl.childNodes[0].textContent.trim().replace(/: ?$/, ''))
}
} else {
console.log(JSON.stringify(er))
humanReadableErrors.push(er)
}
})
qs('.Box.errors .list').innerHTML = humanReadableErrors.join(', ')
qs('.Box.errors').classList.remove('hidden')
break
}
let fademsgbox = function (box, time) {
box.classList.remove('hidden')
setTimeout(() => {
box.classList.add('hiding')
setTimeout(() => {
box.classList.add('hidden')
}, 1000)
}, time)
}
msgAt = window.location.search.indexOf('errmsg=')
box = qs('.Box.errmessage')
if (msgAt !== -1 && box) {
let msg = decodeURIComponent(window.location.search.substr(msgAt + 7))
box.innerHTML = msg
fademsgbox(box, 3000)
break
}
msgAt = window.location.search.indexOf('msg=')
box = qs('.Box.message')
if (msgAt !== -1 && box) {
let msg = decodeURIComponent(window.location.search.substr(msgAt + 4))
box.innerHTML = msg
fademsgbox(box, 2000)
break
}
} while (0)
} catch (e) {
console.error(e)
}
modal.init()
notify.init()
// remove tabindices from h2 if wide
if (window.innerWidth > 550) {
$('.Box h2').forEach(function (x) {
x.removeAttribute('tabindex')
})
// brand works as a link back to term in widescreen mode
let br = qs('#brand')
br && br.addEventListener('click', function () {
window.location.href = '/' // go to terminal
})
}
})
// setup the ajax loader
$._loader = function (vis) {
$('#loader').toggleClass('show', vis)
}
let pageShown = false
// reveal content on load
function showPage () {
pageShown = true
$('#content').addClass('load')
}
// HACKITY HACK: fix this later
window.showPage = showPage
// Auto reveal pages other than the terminal (sets window.noAutoShow)
$.ready(function () {
if (window.noAutoShow === true) {
setTimeout(function () {
if (!pageShown) {
let bnr = mk('P')
bnr.id = 'load-failed'
bnr.innerHTML =
'Server connection failed! Trying again' +
'<span class="anim-dots" style="width:1.5em;text-align:left;display:inline-block">.</span>'
qs('#screen').appendChild(bnr)
qs('#screen').classList.add('failed')
showPage()
}
}, 2000)
} else {
setTimeout(function () {
showPage()
}, 1)
}
})

@ -0,0 +1,18 @@
require('./lib/polyfills')
require('./modal')
require('./notif')
require('./appcommon')
try { require('./term/demo') } catch (err) {}
require('./wifi')
const $ = require('./lib/chibi')
const { qs } = require('./utils')
/* Export stuff to the global scope for inline scripts */
window.termInit = require('./term')
window.$ = $
window.qs = qs
window.themes = require('./term/themes')
window.TermConf = require('./term_conf')

@ -0,0 +1,5 @@
let data = require('locale-data')
module.exports = function localize (key) {
return data[key] || `?${key}?`
}

@ -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,
@ -699,5 +699,5 @@
};
// Set Chibi's global namespace here ($)
w.$ = chibi;
module.exports = chibi;
}());

@ -0,0 +1,118 @@
/*
* Copyright (c) 2010 Tim Baumann
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
// NOTE:
// Extracted from ColorTriangle and
// Converted to ES6 by MightyPork (2017)
/*******************
* Color conversion *
*******************/
const M = Math
const TAU = 2 * M.PI
exports.hue2rgb = function (v1, v2, h) {
if (h < 0) h += 1
if (h > 1) h -= 1
if ((6 * h) < 1) return v1 + (v2 - v1) * 6 * h
if ((2 * h) < 1) return v2
if ((3 * h) < 2) return v1 + (v2 - v1) * ((2 / 3) - h) * 6
return v1
}
exports.hsl2rgb = function (h, s, l) {
h /= TAU
let r, g, b
if (s === 0) {
r = g = b = l
} else {
let var_1, var_2
if (l < 0.5) var_2 = l * (1 + s)
else var_2 = (l + s) - (s * l)
var_1 = 2 * l - var_2
r = exports.hue2rgb(var_1, var_2, h + (1 / 3))
g = exports.hue2rgb(var_1, var_2, h)
b = exports.hue2rgb(var_1, var_2, h - (1 / 3))
}
return [r, g, b]
}
exports.rgb2hsl = function (r, g, b) {
const min = M.min(r, g, b)
const max = M.max(r, g, b)
const d = max - min // delta
let h, s, l
l = (max + min) / 2
if (d === 0) {
// gray
h = s = 0 // HSL results from 0 to 1
} else {
// chroma
if (l < 0.5) s = d / (max + min)
else s = d / (2 - max - min)
const d_r = (((max - r) / 6) + (d / 2)) / d
const d_g = (((max - g) / 6) + (d / 2)) / d
const d_b = (((max - b) / 6) + (d / 2)) / d // deltas
if (r === max) h = d_b - d_g
else if (g === max) h = (1 / 3) + d_r - d_b
else if (b === max) h = (2 / 3) + d_g - d_r
if (h < 0) h += 1
else if (h > 1) h -= 1
}
h *= TAU
return [h, s, l]
}
exports.hex2rgb = function (hex) {
const groups = hex.match(/^#([\da-f]{3,6})$/i)
if (groups) {
hex = groups[1]
const bytes = hex.length / 3
const max = (16 ** bytes) - 1
return [0, 1, 2].map(x => parseInt(hex.slice(x * bytes, (x + 1) * bytes), 16) / max)
}
return [0, 0, 0]
}
function pad (n) {
return `00${n}`.substr(-2)
}
exports.rgb255ToHex = function (r, g, b) {
return '#' + [r, g, b].map(x => pad(x.toString(16))).join('')
}
exports.rgb2hex = function (r, g, b) {
return '#' + [r, g, b].map(x => pad(Math.round(x * 255).toString(16))).join('')
}

@ -0,0 +1,572 @@
/*
* Copyright (c) 2010 Tim Baumann
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
// NOTE: Converted to ES6 by MightyPork (2017)
// Modified for ESPTerm
const EventEmitter = require('events')
const {
rgb2hex,
hex2rgb,
hsl2rgb,
rgb2hsl
} = require('./color_utils')
const win = window
const doc = document
const M = Math
const TAU = 2 * M.PI
function times (i, fn) {
for (let j = 0; j < i; j++) {
fn(j)
}
}
function each (obj, fn) {
if (obj.length) {
times(obj.length, function (i) {
fn(obj[i], i)
})
} else {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
fn(obj[key], key)
}
}
}
}
module.exports = class ColorTriangle extends EventEmitter {
/****************
* ColorTriangle *
****************/
// Constructor function:
constructor (color, options) {
super()
this.options = {
size: 150,
padding: 8,
triangleSize: 0.8,
wheelPointerColor1: '#444',
wheelPointerColor2: '#eee',
trianglePointerSize: 16,
// wheelPointerSize: 16,
trianglePointerColor1: '#eee',
trianglePointerColor2: '#444',
background: 'transparent'
}
this.pixelRatio = window.devicePixelRatio
this.setOptions(options)
this.calculateProperties()
this.createContainer()
this.createTriangle()
this.createWheel()
this.createWheelPointer()
this.createTrianglePointer()
this.attachEvents()
color = color || '#f00'
if (typeof color == 'string') {
this.setHEX(color)
}
}
calculateProperties () {
let opts = this.options
this.padding = opts.padding
this.innerSize = opts.size - opts.padding * 2
this.triangleSize = opts.triangleSize * this.innerSize
this.wheelThickness = (this.innerSize - this.triangleSize) / 2
this.wheelPointerSize = opts.wheelPointerSize || this.wheelThickness
this.wheelRadius = (this.innerSize) / 2
this.triangleRadius = (this.triangleSize) / 2
this.triangleSideLength = M.sqrt(3) * this.triangleRadius
}
calculatePositions () {
const r = this.triangleRadius
const hue = this.hue
const third = TAU / 3
const s = this.saturation
const l = this.lightness
// Colored point
const hx = this.hx = M.cos(hue) * r
const hy = this.hy = -M.sin(hue) * r
// Black point
const sx = this.sx = M.cos(hue - third) * r
const sy = this.sy = -M.sin(hue - third) * r
// White point
const vx = this.vx = M.cos(hue + third) * r
const vy = this.vy = -M.sin(hue + third) * r
// Current point
const mx = (sx + vx) / 2
const my = (sy + vy) / 2
const a = (1 - 2 * M.abs(l - 0.5)) * s
this.x = sx + (vx - sx) * l + (hx - mx) * a
this.y = sy + (vy - sy) * l + (hy - my) * a
}
createContainer () {
let c = this.container = doc.createElement('div')
c.className = 'color-triangle'
c.style.display = 'block'
c.style.padding = `${this.padding}px`
c.style.position = 'relative'
c.style.boxShadow = '0 1px 10px black'
c.style.borderRadius = '5px'
c.style.width = c.style.height = `${this.innerSize + 2 * this.padding}px`
c.style.background = this.options.background
}
createWheel () {
let c = this.wheel = doc.createElement('canvas')
c.width = c.height = this.innerSize * this.pixelRatio
c.style.width = c.style.height = `${this.innerSize}px`
c.style.position = 'absolute'
c.style.margin = c.style.padding = '0'
c.style.left = c.style.top = `${this.padding}px`
this.drawWheel(c.getContext('2d'))
this.container.appendChild(c)
}
drawWheel (ctx) {
let s, i
ctx.save()
ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0)
ctx.translate(this.wheelRadius, this.wheelRadius)
s = this.wheelRadius - this.triangleRadius
// Draw a circle for every color
for (i = 0; i < 360; i++) {
ctx.rotate(TAU / -360) // rotate one degree
ctx.beginPath()
ctx.fillStyle = 'hsl(' + i + ', 100%, 50%)'
ctx.arc(this.wheelRadius - (s / 2), 0, s / 2, 0, TAU, true)
ctx.fill()
}
ctx.restore()
}
createTriangle () {
let c = this.triangle = doc.createElement('canvas')
c.width = c.height = this.innerSize * this.pixelRatio
c.style.width = c.style.height = `${this.innerSize}px`
c.style.position = 'absolute'
c.style.margin = c.style.padding = '0'
c.style.left = c.style.top = this.padding + 'px'
this.triangleCtx = c.getContext('2d')
this.container.appendChild(c)
}
drawTriangle () {
const hx = this.hx
const hy = this.hy
const sx = this.sx
const sy = this.sy
const vx = this.vx
const vy = this.vy
const size = this.innerSize
let ctx = this.triangleCtx
// clear
ctx.clearRect(0, 0, size * this.pixelRatio, size * this.pixelRatio)
ctx.save()
ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0)
ctx.translate(this.wheelRadius, this.wheelRadius)
// make a triangle
ctx.beginPath()
ctx.moveTo(hx, hy)
ctx.lineTo(sx, sy)
ctx.lineTo(vx, vy)
ctx.closePath()
ctx.clip()
ctx.fillStyle = '#000'
ctx.fillRect(-this.wheelRadius, -this.wheelRadius, size, size)
// => black triangle
// create gradient from hsl(hue, 1, 1) to transparent
let grad0 = ctx.createLinearGradient(hx, hy, (sx + vx) / 2, (sy + vy) / 2)
const hsla = 'hsla(' + M.round(this.hue * (360 / TAU)) + ', 100%, 50%, '
grad0.addColorStop(0, hsla + '1)')
grad0.addColorStop(1, hsla + '0)')
ctx.fillStyle = grad0
ctx.fillRect(-this.wheelRadius, -this.wheelRadius, size, size)
// => gradient: one side of the triangle is black, the opponent angle is $color
// create color gradient from white to transparent
let grad1 = ctx.createLinearGradient(vx, vy, (hx + sx) / 2, (hy + sy) / 2)
grad1.addColorStop(0, '#fff')
grad1.addColorStop(1, 'rgba(255, 255, 255, 0)')
ctx.globalCompositeOperation = 'lighter'
ctx.fillStyle = grad1
ctx.fillRect(-this.wheelRadius, -this.wheelRadius, size, size)
// => white angle
ctx.restore()
}
// The two pointers
createWheelPointer () {
let c = this.wheelPointer = doc.createElement('canvas')
const size = this.wheelPointerSize
c.width = c.height = size * this.pixelRatio
c.style.width = c.style.height = `${size}px`
c.style.position = 'absolute'
c.style.margin = c.style.padding = '0'
this.drawPointer(c.getContext('2d'), size / 2, this.options.wheelPointerColor1, this.options.wheelPointerColor2)
this.container.appendChild(c)
}
moveWheelPointer () {
const r = this.wheelPointerSize / 2
const s = this.wheelPointer.style
s.top = this.padding + this.wheelRadius - M.sin(this.hue) * (this.triangleRadius + this.wheelThickness / 2) - r + 'px'
s.left = this.padding + this.wheelRadius + M.cos(this.hue) * (this.triangleRadius + this.wheelThickness / 2) - r + 'px'
}
createTrianglePointer () { // create pointer in the triangle
let c = this.trianglePointer = doc.createElement('canvas')
const size = this.options.trianglePointerSize
c.width = c.height = size * this.pixelRatio
c.style.width = c.style.height = `${size}px`
c.style.position = 'absolute'
c.style.margin = c.style.padding = '0'
this.drawPointer(c.getContext('2d'), size / 2, this.options.trianglePointerColor1, this.options.trianglePointerColor2)
this.container.appendChild(c)
}
moveTrianglePointer (x, y) {
const s = this.trianglePointer.style
const r = this.options.trianglePointerSize / 2
s.top = (this.y + this.wheelRadius + this.padding - r) + 'px'
s.left = (this.x + this.wheelRadius + this.padding - r) + 'px'
}
drawPointer (ctx, r, color1, color2) {
ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0)
ctx.fillStyle = color2
ctx.beginPath()
ctx.arc(r, r, r, 0, TAU, true)
ctx.fill() // => black circle
ctx.fillStyle = color1
ctx.beginPath()
ctx.arc(r, r, r - 2, 0, TAU, true)
ctx.fill() // => white circle with 1px black border
ctx.fillStyle = color2
ctx.beginPath()
ctx.arc(r, r, r / 4 + 2, 0, TAU, true)
ctx.fill() // => black circle with big white border and a small black border
ctx.globalCompositeOperation = 'destination-out'
ctx.beginPath()
ctx.arc(r, r, r / 4, 0, TAU, true)
ctx.fill() // => transparent center
}
// The Element and the DOM
inject (parent) {
parent.appendChild(this.container)
}
getRelativeCoordinates (evt) {
let elem = this.triangle
let rect = elem.getBoundingClientRect()
return {
x: evt.clientX - rect.x,
y: evt.clientY - rect.y
}
}
dispose () {
let parent = this.container.parentNode
if (parent) {
parent.removeChild(this.container)
}
}
getElement () {
return this.container
}
// Color accessors
getCSS () {
const h = Math.round(this.hue * (360 / TAU))
const s = Math.round(this.saturation * 100)
const l = Math.round(this.lightness * 100)
return `hsl(${h}, ${s}%, ${l}%)`
}
getHEX () {
return rgb2hex(...this.getRGB())
}
setHEX (hex) {
this.setRGB(...hex2rgb(hex))
}
getRGB () {
return hsl2rgb(...this.getHSL())
}
setRGB (r, g, b) {
this.setHSL(...rgb2hsl(r, g, b))
}
getHSL () {
return [this.hue, this.saturation, this.lightness]
}
setHSL (h, s, l) {
this.hue = h
this.saturation = s
this.lightness = l
this.initColor()
}
initColor () {
this.calculatePositions()
this.moveWheelPointer()
this.drawTriangle()
this.moveTrianglePointer()
}
// Mouse event handling
attachEvents () {
this.down = null
let mousedown = (evt) => {
evt.stopPropagation()
evt.preventDefault()
doc.body.addEventListener('mousemove', mousemove, false)
doc.body.addEventListener('mouseup', mouseup, false)
let xy = this.getRelativeCoordinates(evt)
this.map(xy.x, xy.y)
}
let mousemove = (evt) => {
let xy = this.getRelativeCoordinates(evt)
this.move(xy.x, xy.y)
}
let mouseup = (evt) => {
if (this.down) {
this.down = null
this.emit('dragend')
}
doc.body.removeEventListener('mousemove', mousemove, false)
doc.body.removeEventListener('mouseup', mouseup, false)
}
this.container.addEventListener('mousedown', mousedown, false)
this.container.addEventListener('mousemove', mousemove, false)
}
map (x, y) {
let x0 = x
let y0 = y
x -= this.wheelRadius
y -= this.wheelRadius
const r = M.sqrt(x * x + y * y) // Pythagoras
if (r > this.triangleRadius && r < this.wheelRadius) {
// Wheel
this.down = 'wheel'
this.emit('dragstart')
this.move(x0, y0)
} else if (r < this.triangleRadius) {
// Inner circle
this.down = 'triangle'
this.emit('dragstart')
this.move(x0, y0)
}
}
move (x, y) {
if (!this.down) {
return
}
x -= this.wheelRadius
y -= this.wheelRadius
let rad = M.atan2(-y, x)
if (rad < 0) {
rad += TAU
}
if (this.down === 'wheel') {
this.hue = rad
this.initColor()
this.emit('drag')
} else if (this.down === 'triangle') {
// get radius and max radius
let rad0 = (rad + TAU - this.hue) % TAU
let rad1 = rad0 % (TAU / 3) - (TAU / 6)
let a = 0.5 * this.triangleRadius
let b = M.tan(rad1) * a
let r = M.sqrt(x * x + y * y) // Pythagoras
let maxR = M.sqrt(a * a + b * b) // Pythagoras
if (r > maxR) {
const dx = M.tan(rad1) * r
let rad2 = M.atan(dx / maxR)
if (rad2 > TAU / 6) {
rad2 = TAU / 6
} else if (rad2 < -TAU / 6) {
rad2 = -TAU / 6
}
rad += rad2 - rad1
rad0 = (rad + TAU - this.hue) % TAU
rad1 = rad0 % (TAU / 3) - (TAU / 6)
b = M.tan(rad1) * a
r = maxR = M.sqrt(a * a + b * b) // Pythagoras
}
x = M.round(M.cos(rad) * r)
y = M.round(-M.sin(rad) * r)
const l = this.lightness = ((M.sin(rad0) * r) / this.triangleSideLength) + 0.5
const widthShare = 1 - (M.abs(l - 0.5) * 2)
let s = this.saturation = (((M.cos(rad0) * r) + (this.triangleRadius / 2)) / (1.5 * this.triangleRadius)) / widthShare
s = M.max(0, s) // cannot be lower than 0
s = M.min(1, s) // cannot be greater than 1
this.lightness = l
this.saturation = s
this.x = x
this.y = y
this.moveTrianglePointer()
this.emit('drag')
}
}
/***************
* Init helpers *
***************/
static initInput (input, options) {
options = options || {}
let ct
let openColorTriangle = function () {
let hex = input.value
if (options.parseColor) hex = options.parseColor(hex)
if (!ct) {
options.size = options.size || input.offsetWidth
options.background = win.getComputedStyle(input, null).backgroundColor
options.margin = options.margin || 10
options.event = options.event || 'dragend'
ct = new ColorTriangle(hex, options)
ct.on(options.event, () => {
const hex = ct.getHEX()
input.value = options.uppercase ? hex.toUpperCase() : hex
fireChangeEvent()
})
} else {
ct.setHEX(hex)
}
let top = input.offsetTop
if (win.innerHeight - input.getBoundingClientRect().top > input.offsetHeight + options.margin + options.size) {
top += input.offsetHeight + options.margin // below
} else {
top -= options.margin + options.size // above
}
let el = ct.getElement()
el.style.position = 'absolute'
el.style.left = input.offsetLeft + 'px'
el.style.top = top + 'px'
el.style.zIndex = '1338' // above everything
ct.inject(input.parentNode)
}
let closeColorTriangle = () => {
if (ct) {
ct.dispose()
}
}
let fireChangeEvent = () => {
let evt = doc.createEvent('HTMLEvents')
evt.initEvent('input', true, false) // bubbles = true, cancable = false
input.dispatchEvent(evt) // fire event
}
input.addEventListener('focus', openColorTriangle, false)
input.addEventListener('blur', closeColorTriangle, false)
input.addEventListener('keyup', () => {
const val = input.value
if (val.match(/^#((?:[0-9A-Fa-f]{3})|(?:[0-9A-Fa-f]{6}))$/)) {
openColorTriangle()
fireChangeEvent()
} else {
closeColorTriangle()
}
}, false)
}
/*******************
* Helper functions *
*******************/
setOptions (opts) {
opts = opts || {}
let dflt = this.options
let options = this.options = {}
each(dflt, function (val, key) {
options[key] = (opts.hasOwnProperty(key))
? opts[key]
: val
})
}
}

@ -0,0 +1,63 @@
/*! http://mths.be/fromcodepoint v0.1.0 by @mathias */
if (!String.fromCodePoint) {
(function () {
var defineProperty = (function () {
// IE 8 only supports `Object.defineProperty` on DOM elements
try {
var object = {};
var $defineProperty = Object.defineProperty;
var result = $defineProperty(object, object, object) && $defineProperty;
} catch (error) {
}
return result;
}());
var stringFromCharCode = String.fromCharCode;
var floor = Math.floor;
var fromCodePoint = function () {
var MAX_SIZE = 0x4000;
var codeUnits = [];
var highSurrogate;
var lowSurrogate;
var index = -1;
var length = arguments.length;
if (!length) {
return '';
}
var result = '';
while (++index < length) {
var codePoint = Number(arguments[index]);
if (
!isFinite(codePoint) || // `NaN`, `+Infinity`, or `-Infinity`
codePoint < 0 || // not a valid Unicode code point
codePoint > 0x10FFFF || // not a valid Unicode code point
floor(codePoint) != codePoint // not an integer
) {
throw RangeError('Invalid code point: ' + codePoint);
}
if (codePoint <= 0xFFFF) { // BMP code point
codeUnits.push(codePoint);
} else { // Astral code point; split in surrogate halves
// http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
codePoint -= 0x10000;
highSurrogate = (codePoint >> 10) + 0xD800;
lowSurrogate = (codePoint % 0x400) + 0xDC00;
codeUnits.push(highSurrogate, lowSurrogate);
}
if (index + 1 == length || codeUnits.length > MAX_SIZE) {
result += stringFromCharCode.apply(null, codeUnits);
codeUnits.length = 0;
}
}
return result;
};
if (defineProperty) {
defineProperty(String, 'fromCodePoint', {
'value': fromCodePoint,
'configurable': true,
'writable': true
});
} else {
String.fromCodePoint = fromCodePoint;
}
}());
}

@ -0,0 +1,44 @@
const $ = require('./lib/chibi')
/** Module for toggling a modal overlay */
let modal = {}
let curCloseCb = null
modal.show = function (sel, closeCb) {
let $m = $(sel)
$m.removeClass('hidden visible')
setTimeout(function () {
$m.addClass('visible')
}, 1)
curCloseCb = closeCb
}
modal.hide = function (sel) {
let $m = $(sel)
$m.removeClass('visible')
setTimeout(function () {
$m.addClass('hidden')
if (curCloseCb) curCloseCb()
}, 500) // transition time
}
modal.init = function () {
// close modal by click outside the dialog
$('.Modal').on('click', function () {
if ($(this).hasClass('no-close')) return // this is a no-close modal
modal.hide(this)
})
$('.Dialog').on('click', function (e) {
e.stopImmediatePropagation()
})
// Hide all modals on esc
$(window).on('keydown', function (e) {
if (e.which === 27) {
modal.hide('.Modal')
}
})
}
module.exports = modal

@ -0,0 +1,65 @@
const $ = require('./lib/chibi')
const modal = require('./modal')
let nt = {}
const sel = '#notif'
let $balloon
let timerHideBegin // timeout to start hiding (transition)
let timerHideEnd // timeout to add the hidden class
let canCancel = false
let stopTimeouts = function () {
clearTimeout(timerHideBegin)
clearTimeout(timerHideEnd)
}
nt.show = function (message, timeout, isError) {
$balloon.toggleClass('error', isError === true)
$balloon.html(message)
modal.show($balloon)
stopTimeouts()
if (!timeout || timeout <= 0) {
timeout = 2500
}
timerHideBegin = setTimeout(nt.hide, timeout)
canCancel = false
setTimeout(() => {
canCancel = true
}, 500)
}
nt.hide = function () {
let $m = $(sel)
$m.removeClass('visible')
timerHideEnd = setTimeout(function () {
$m.addClass('hidden')
}, 250) // transition time
}
nt.init = function () {
$balloon = $(sel)
// close by click outside
$(document).on('click', function () {
if (!canCancel) return
nt.hide(this)
})
// click caused by selecting, prevent it from bubbling
$balloon.on('click', function (e) {
e.stopImmediatePropagation()
return false
})
// stop fading if moused
$balloon.on('mouseenter', function () {
stopTimeouts()
$balloon.removeClass('hidden').addClass('visible')
})
}
module.exports = nt

1144
js/td/WebAudio.d.ts vendored

File diff suppressed because it is too large Load Diff

@ -0,0 +1,112 @@
const { getColor } = require('./themes')
const { qs } = require('../utils')
const { rgb2hsl, hex2rgb, rgb2hex, hsl2rgb } = require('../lib/color_utils')
module.exports = function initButtons (input) {
let container = qs('#action-buttons')
// current color palette
let palette = []
// button labels
let labels = []
// button colors
let colors = {}
// button elements
let buttons = []
// add a button element
let pushButton = function pushButton () {
let button = document.createElement('button')
button.classList.add('action-button')
button.setAttribute('data-n', buttons.length)
buttons.push(button)
container.appendChild(button)
button.addEventListener('click', e => {
// might as well use the attribute ¯\_(ツ)_/¯
let index = +button.getAttribute('data-n')
input.sendButton(index)
e.target.blur() // if it keeps focus, spacebar will push it
})
// this prevents button retaining focus after half-click/drag-away
button.addEventListener('mouseleave', e => {
e.target.blur()
})
return button
}
// remove a button element
let popButton = function popButton () {
let button = buttons.pop()
button.parentNode.removeChild(button)
}
// sync with DOM
let update = function updateButtons () {
if (labels.length > buttons.length) {
for (let i = buttons.length; i < labels.length; i++) {
pushButton()
}
} else if (buttons.length > labels.length) {
for (let i = buttons.length; i > labels.length; i--) {
popButton()
}
}
for (let i = 0; i < labels.length; i++) {
let label = labels[i].trim()
let button = buttons[i]
let color = colors[i]
button.textContent = label || '\u00a0' // label or nbsp
if (!label) button.classList.add('inactive')
else button.classList.remove('inactive')
// 0 or undefined can be used to disable custom color
if (Number.isFinite(color) && color !== 0) {
const clr = getColor(color, palette)
button.style.background = clr
// darken the color a bit for the 3D side
const hsl = rgb2hsl(...hex2rgb(clr))
const hex = rgb2hex(...hsl2rgb(hsl[0], hsl[1], hsl[2] * 0.7))
button.style.boxShadow = `0 3px 0 ${hex}`
} else {
button.style.background = null
button.style.boxShadow = null
}
}
}
return {
update,
get labels () {
return labels
},
set labels (value) {
labels = value
update()
},
get colors () {
return colors
},
set colors (value) {
colors = value
update()
},
get palette () {
return palette
},
set palette (value) {
palette = value
update()
}
}
}

@ -0,0 +1,239 @@
const EventEmitter = require('events')
const $ = require('../lib/chibi')
let demo
try { demo = require('./demo') } catch (err) {}
const RECONN_DELAY = 2000
const HEARTBEAT_TIME = 3000
const HTTPS = window.location.protocol.match(/s:/)
/** Handle connections */
module.exports = class TermConnection extends EventEmitter {
constructor (screen) {
super()
this.screen = screen
this.ws = null
this.heartbeatTimeout = null
this.pingInterval = null
this.xoff = false
this.autoXoffTimeout = null
this.reconnTimeout = null
this.forceClosing = false
this.queue = []
try {
this.blobReader = new window.FileReader()
this.blobReader.onload = (evt) => {
this.onDecodedWSMessage(this.blobReader.result)
}
this.blobReader.onerror = (evt) => {
console.error(evt)
}
} catch (e) {
this.blobReader = null
}
this.pageShown = false
this.disconnectTimeout = null
document.addEventListener('visibilitychange', () => {
if (document.hidden === true) {
console.info('Window lost focus, freeing socket')
// Delayed, avoid disconnecting if the background time is short
this.disconnectTimeout = setTimeout(() => {
this.closeSocket()
clearTimeout(this.heartbeatTimeout)
}, 1000)
} else {
clearTimeout(this.disconnectTimeout)
console.info('Window got focus, re-connecting')
this.init()
}
}, false)
}
onWSOpen (evt) {
console.log('CONNECTED')
this.heartbeat()
this.send('i')
this.forceClosing = false
this.emit('connect')
}
onWSClose (evt) {
if (this.forceClosing) {
this.forceClosing = false
return
}
console.warn('SOCKET CLOSED, code ' + evt.code + '. Reconnecting...')
if (evt.code < 1000) {
console.error('Bad code from socket!')
// this sometimes happens for unknown reasons, code < 1000 is invalid
// location.reload()
}
clearTimeout(this.reconnTimeout)
this.reconnTimeout = setTimeout(() => this.init(), RECONN_DELAY)
this.emit('disconnect', evt.code)
}
onDecodedWSMessage (str) {
switch (str.charAt(0)) {
case '.':
// heartbeat, no-op message
break
case '-':
// console.log('xoff');
this.xoff = true
this.autoXoffTimeout = setTimeout(() => {
this.xoff = false
this.flushQueue()
}, 250)
break
case '+':
// console.log('xon');
this.xoff = false
this.flushQueue()
clearTimeout(this.autoXoffTimeout)
break
default:
this.screen.load(str)
if (!this.pageShown) {
window.showPage()
this.pageShown = true
}
break
}
this.heartbeat()
}
onWSMessage (evt) {
if (typeof evt.data === 'string') this.onDecodedWSMessage(evt.data)
else {
if (!this.blobReader) {
console.error('No FileReader!')
return
}
if (this.blobReader.readyState !== 1) {
this.blobReader.readAsText(evt.data)
} else {
setTimeout(() => {
this.onWSMessage(evt)
}, 1)
}
}
}
canSend () {
return !this.xoff
}
send (message) {
if (window._demo) {
if (typeof window.demoInterface !== 'undefined') {
demo.input(message)
} else {
console.log(`TX: ${JSON.stringify(message)}`)
}
return true // Simulate success
}
if (this.xoff) {
console.log("Can't send, flood control. Queueing")
this.queue.push(message)
return false
}
if (!this.ws) return false // for dry testing
if (this.ws.readyState !== 1) {
console.error('Socket not ready')
return false
}
if (typeof message !== 'string') {
message = JSON.stringify(message)
}
this.ws.send(message)
return true
}
flushQueue () {
console.log('Flushing input queue')
for (let message of this.queue) this.send(message)
this.queue = []
}
/** Safely close the socket */
closeSocket () {
if (this.ws) {
this.forceClosing = true
if (this.ws.readyState === 1) this.ws.close()
this.ws = null
}
}
init () {
if (window._demo) {
if (typeof window.demoInterface === 'undefined') {
window.alert('Demoing non-demo build!') // this will catch mistakes when deploying to the website
} else {
demo.init(this.screen)
}
return
}
clearTimeout(this.reconnTimeout)
clearTimeout(this.heartbeatTimeout)
this.closeSocket()
this.ws = new window.WebSocket(`${HTTPS ? 'wss' : 'ws'}://${window._root}/term/update.ws`)
this.ws.addEventListener('open', (...args) => this.onWSOpen(...args))
this.ws.addEventListener('close', (...args) => this.onWSClose(...args))
this.ws.addEventListener('message', (...args) => this.onWSMessage(...args))
console.log('Opening socket.')
this.heartbeat()
this.emit('open')
}
heartbeat () {
this.emit('heartbeat')
clearTimeout(this.heartbeatTimeout)
this.heartbeatTimeout = setTimeout(() => this.onHeartbeatFail(), HEARTBEAT_TIME)
}
sendPing () {
console.log('> ping')
this.emit('ping')
$.get(`${HTTPS ? 'https' : 'http'}://${window._root}/api/v1/ping`, (resp, status) => {
if (status === 200) {
clearInterval(this.pingInterval)
console.info('Server ready, opening socket…')
this.emit('ping-success')
this.init()
// location.reload()
} else this.emit('ping-fail', status)
}, {
timeout: 100,
loader: false // we have loader on-screen
})
}
onHeartbeatFail () {
this.closeSocket()
this.emit('silence')
console.error('Heartbeat lost, probing server...')
clearInterval(this.pingInterval)
this.pingInterval = setInterval(() => { this.sendPing() }, 1000)
// first ping, if this gets through, it'll will reduce delay
setTimeout(() => { this.sendPing() }, 200)
}
}

@ -0,0 +1,539 @@
const { getColor } = require('./themes')
const {
ATTR_FG,
ATTR_BG,
ATTR_BOLD,
ATTR_UNDERLINE,
ATTR_BLINK,
ATTR_ITALIC,
ATTR_STRIKE,
ATTR_OVERLINE,
ATTR_FAINT,
ATTR_FRAKTUR
} = require('./screen_attr_bits')
// debug toolbar, tooltip and screen
module.exports = function attachDebugger (screen, connection) {
// debug screen overlay
const debugCanvas = document.createElement('canvas')
debugCanvas.classList.add('debug-canvas')
const ctx = debugCanvas.getContext('2d')
// debug toolbar
const toolbar = document.createElement('div')
toolbar.classList.add('debug-toolbar')
// debug tooltip
const tooltip = document.createElement('div')
tooltip.classList.add('debug-tooltip')
tooltip.classList.add('hidden')
// update functions, defined somewhere below
let updateTooltip
let updateToolbar
// tooltip cell
let selectedCell = null
// update tooltip cell when mouse moves
const onMouseMove = (e) => {
if (e.target !== screen.layout.canvas) {
selectedCell = null
return
}
selectedCell = screen.layout.screenToGrid(e.offsetX, e.offsetY)
updateTooltip()
}
// hide tooltip when mouse leaves
const onMouseOut = (e) => {
selectedCell = null
tooltip.classList.add('hidden')
}
// updates debug canvas size
const updateCanvasSize = function () {
let { width, height, devicePixelRatio } = screen.layout.window
let cellSize = screen.layout.getCellSize()
let padding = Math.round(screen.layout._padding)
debugCanvas.width = (width * cellSize.width + 2 * padding) * devicePixelRatio
debugCanvas.height = (height * cellSize.height + 2 * padding) * devicePixelRatio
debugCanvas.style.width = `${width * cellSize.width + 2 * screen.layout._padding}px`
debugCanvas.style.height = `${height * cellSize.height + 2 * screen.layout._padding}px`
}
// defined somewhere below
let startDrawLoop
let screenAttached = false
// node to which events were bound (kept here for when they need to be removed)
let eventNode
// attaches/detaches debug screen overlay to/from DOM
const setScreenAttached = function (attached) {
if (attached && !debugCanvas.parentNode) {
screen.layout.canvas.parentNode.appendChild(debugCanvas)
eventNode = debugCanvas.parentNode
eventNode.addEventListener('mousemove', onMouseMove)
eventNode.addEventListener('mouseout', onMouseOut)
screen.layout.on('size-update', updateCanvasSize)
updateCanvasSize()
screenAttached = true
startDrawLoop()
} else if (!attached && debugCanvas.parentNode) {
debugCanvas.parentNode.removeChild(debugCanvas)
eventNode.removeEventListener('mousemove', onMouseMove)
eventNode.removeEventListener('mouseout', onMouseOut)
screen.layout.removeListener('size-update', updateCanvasSize)
screenAttached = false
}
}
// attaches/detaches toolbar and tooltip to/from DOM
const setToolbarAttached = function (attached) {
if (attached && !toolbar.parentNode) {
screen.layout.canvas.parentNode.appendChild(toolbar)
screen.layout.canvas.parentNode.appendChild(tooltip)
updateToolbar()
} else if (!attached && toolbar.parentNode) {
screen.layout.canvas.parentNode.removeChild(toolbar)
screen.layout.canvas.parentNode.removeChild(tooltip)
}
}
// attach/detach toolbar when debug mode is enabled/disabled
screen.on('update-window:debug', enabled => {
setToolbarAttached(enabled)
})
// ditto ^
screen.layout.on('update-window:debug', enabled => {
setScreenAttached(enabled)
})
let drawData = {
// last draw reason
reason: '',
// when true, will show colored cell update overlays
showUpdates: false,
// draw start time in milliseconds
startTime: 0,
// end time
endTime: 0,
// partial update frames
frames: [],
// cell data
cells: new Map(),
// scroll region
scrollRegion: null
}
// debug interface
screen._debug = screen.layout.renderer._debug = {
drawStart (reason) {
drawData.reason = reason
drawData.startTime = window.performance.now()
},
drawEnd () {
drawData.endTime = window.performance.now()
},
setCell (cell, flags) {
drawData.cells.set(cell, [flags, window.performance.now()])
},
pushFrame (frame) {
drawData.frames.push([...frame, window.performance.now()])
}
}
let isDrawing = false
let drawLoop = function () {
// draw while the screen is attached
if (screenAttached) window.requestAnimationFrame(drawLoop)
else isDrawing = false
let now = window.performance.now()
let { width, height, devicePixelRatio } = screen.layout.window
let padding = Math.round(screen.layout._padding)
let cellSize = screen.layout.getCellSize()
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0)
ctx.clearRect(0, 0, width * cellSize.width + 2 * padding, height * cellSize.height + 2 * padding)
ctx.translate(padding, padding)
ctx.lineWidth = 2
ctx.lineJoin = 'round'
if (drawData.showUpdates) {
const cells = drawData.cells
for (let cell = 0; cell < width * height; cell++) {
// cell does not exist or has no flags set
if (!cells.has(cell) || cells.get(cell)[0] === 0) continue
const [flags, timestamp] = cells.get(cell)
let elapsedTime = (now - timestamp) / 1000
if (elapsedTime > 1) {
cells.delete(cell)
continue
}
ctx.globalAlpha = 0.5 * Math.max(0, 1 - elapsedTime)
let x = cell % width
let y = Math.floor(cell / width)
if (flags & 2) {
// updated
ctx.fillStyle = '#0f0'
} else if (flags & 1) {
// redrawn
ctx.fillStyle = '#f0f'
}
if (!(flags & 4)) {
// outside a clipped region
ctx.fillStyle = '#0ff'
}
if (flags & 16) {
// was filled to speed up rendering
ctx.globalAlpha /= 2
}
ctx.fillRect(x * cellSize.width, y * cellSize.height, cellSize.width, cellSize.height)
if (flags & 8) {
// wide cell
ctx.strokeStyle = '#f00'
ctx.beginPath()
ctx.moveTo(x * cellSize.width, (y + 1) * cellSize.height)
ctx.lineTo((x + 1) * cellSize.width, (y + 1) * cellSize.height)
ctx.stroke()
}
}
let framesToDelete = []
for (let frame of drawData.frames) {
let timestamp = frame[4]
let elapsedTime = (now - timestamp) / 1000
if (elapsedTime > 1) framesToDelete.push(frame)
else {
ctx.globalAlpha = 1 - elapsedTime
ctx.strokeStyle = '#ff0'
ctx.strokeRect(frame[0] * cellSize.width, frame[1] * cellSize.height,
frame[2] * cellSize.width, frame[3] * cellSize.height)
}
}
for (let frame of framesToDelete) {
drawData.frames.splice(drawData.frames.indexOf(frame), 1)
}
}
if (selectedCell !== null) {
// draw a dashed outline around the selected cell
let [x, y] = selectedCell
ctx.save()
ctx.globalAlpha = 0.5
ctx.lineWidth = 1
// draw X line
ctx.beginPath()
ctx.moveTo(0, y * cellSize.height)
ctx.lineTo(x * cellSize.width, y * cellSize.height)
ctx.strokeStyle = '#f00'
ctx.setLineDash([cellSize.width])
ctx.stroke()
// draw Y line
ctx.beginPath()
ctx.moveTo(x * cellSize.width, 0)
ctx.lineTo(x * cellSize.width, y * cellSize.height)
ctx.strokeStyle = '#0f0'
ctx.setLineDash([cellSize.height])
ctx.stroke()
ctx.globalAlpha = 1
ctx.lineWidth = 1 + 0.5 * Math.sin((now / 1000) * 10)
ctx.strokeStyle = '#fff'
ctx.lineJoin = 'round'
ctx.setLineDash([2, 2])
ctx.lineDashOffset = (now / 1000) * 10
ctx.strokeRect(x * cellSize.width, y * cellSize.height, cellSize.width, cellSize.height)
ctx.lineDashOffset += 2
ctx.strokeStyle = '#000'
ctx.strokeRect(x * cellSize.width, y * cellSize.height, cellSize.width, cellSize.height)
ctx.restore()
}
if (drawData.scrollRegion !== null) {
// draw two lines marking the scroll region bounds
let [start, end] = drawData.scrollRegion
ctx.save()
ctx.globalAlpha = 1
ctx.strokeStyle = '#00f'
ctx.lineWidth = 2
ctx.setLineDash([2, 2])
ctx.beginPath()
ctx.moveTo(0, start * cellSize.height)
ctx.lineTo(width * cellSize.width, start * cellSize.height)
ctx.stroke()
ctx.beginPath()
ctx.moveTo(0, (end + 1) * cellSize.height)
ctx.lineTo(width * cellSize.width, (end + 1) * cellSize.height)
ctx.stroke()
ctx.restore()
}
}
startDrawLoop = function () {
if (isDrawing) return
isDrawing = true
drawLoop()
}
let pad2 = i => ('00' + i.toString()).substr(-2)
let formatColor = color => color < 256
? color.toString()
: '#' + pad2(color >> 16) + pad2((color >> 8) & 0xFF) + pad2(color & 0xFF)
let makeSpan = (text, styles) => {
let span = document.createElement('span')
span.textContent = text
Object.assign(span.style, styles || {})
return span
}
let formatAttributes = (target, attrs) => {
if (attrs & ATTR_FG) target.appendChild(makeSpan('HasFG'))
if (attrs & ATTR_BG) target.appendChild(makeSpan('HasBG'))
if (attrs & ATTR_BOLD) target.appendChild(makeSpan('Bold', { fontWeight: 'bold' }))
if (attrs & ATTR_UNDERLINE) target.appendChild(makeSpan('Uline', { textDecoration: 'underline' }))
if (attrs & ATTR_BLINK) target.appendChild(makeSpan('Blink'))
if (attrs & ATTR_ITALIC) target.appendChild(makeSpan('Italic', { fontStyle: 'italic' }))
if (attrs & ATTR_STRIKE) target.appendChild(makeSpan('Strike', { textDecoration: 'line-through' }))
if (attrs & ATTR_OVERLINE) target.appendChild(makeSpan('Oline', { textDecoration: 'overline' }))
if (attrs & ATTR_FAINT) target.appendChild(makeSpan('Faint', { opacity: 0.5 }))
if (attrs & ATTR_FRAKTUR) target.appendChild(makeSpan('Fraktur'))
}
updateTooltip = function () {
// TODO: make this not destroy and recreate the same nodes every time
tooltip.classList.remove('hidden')
tooltip.innerHTML = ''
let cell = selectedCell[1] * screen.window.width + selectedCell[0]
if (!screen.screen[cell]) return
let foreground = document.createElement('span')
foreground.textContent = formatColor(screen.screenFG[cell])
let preview = document.createElement('span')
preview.textContent = ' ●'
preview.style.color = getColor(screen.screenFG[cell], screen.layout.renderer.palette)
foreground.appendChild(preview)
let background = document.createElement('span')
background.textContent = formatColor(screen.screenBG[cell])
let bgPreview = document.createElement('span')
bgPreview.textContent = ' ●'
bgPreview.style.color = getColor(screen.screenBG[cell], screen.layout.renderer.palette)
background.appendChild(bgPreview)
let character = screen.screen[cell]
let codePoint = character.codePointAt(0)
let formattedCodePoint = codePoint.toString(16).length <= 4
? `0000${codePoint.toString(16)}`.substr(-4)
: codePoint.toString(16)
let attributes = document.createElement('span')
attributes.classList.add('attributes')
formatAttributes(attributes, screen.screenAttrs[cell])
let data = {
Cell: `col ${selectedCell[0] + 1}, ln ${selectedCell[1] + 1} (${cell})`,
Foreground: foreground,
Background: background,
Character: `U+${formattedCodePoint}`,
Attributes: attributes
}
let table = document.createElement('table')
for (let name in data) {
let row = document.createElement('tr')
let label = document.createElement('td')
label.appendChild(new window.Text(name))
label.classList.add('label')
let value = document.createElement('td')
value.appendChild(typeof data[name] === 'string' ? new window.Text(data[name]) : data[name])
value.classList.add('value')
row.appendChild(label)
row.appendChild(value)
table.appendChild(row)
}
tooltip.appendChild(table)
let cellSize = screen.layout.getCellSize()
// add 3 to the position because for some reason the corner is off
let posX = (selectedCell[0] + 1) * cellSize.width + 3
let posY = (selectedCell[1] + 1) * cellSize.height + 3
tooltip.style.transform = `translate(${posX}px, ${posY}px)`
}
let toolbarData = null
let toolbarNodes = {}
// construct the toolbar if it wasn't already
const initToolbar = function () {
if (toolbarData) return
let showUpdates = document.createElement('input')
showUpdates.type = 'checkbox'
showUpdates.addEventListener('change', e => {
drawData.showUpdates = showUpdates.checked
})
let fancyGraphics = document.createElement('input')
fancyGraphics.type = 'checkbox'
fancyGraphics.value = !!screen.layout.renderer.graphics
fancyGraphics.addEventListener('change', e => {
screen.layout.renderer.graphics = +fancyGraphics.checked
})
toolbarData = {
cursor: {
title: 'Cursor',
Position: '',
Style: '',
Visible: true,
Hanging: false
},
internal: {
Flags: '',
'Cursor Attributes': '',
'Code Page': '',
Heap: 0,
Clients: 0
},
drawing: {
title: 'Drawing',
'Last Update': '',
'Show Updates': showUpdates,
'Fancy Graphics': fancyGraphics,
'Redraw Screen': () => {
screen.layout.renderer.resetDrawn()
screen.layout.renderer.draw('debug-redraw')
}
}
}
for (let i in toolbarData) {
let group = toolbarData[i]
let table = document.createElement('table')
table.classList.add('toolbar-group')
toolbarNodes[i] = {}
for (let key in group) {
let item = document.createElement('tr')
let name = document.createElement('td')
name.classList.add('name')
let value = document.createElement('td')
value.classList.add('value')
toolbarNodes[i][key] = { name, value }
if (key === 'title') {
name.textContent = group[key]
name.classList.add('title')
} else {
name.textContent = key
if (group[key] instanceof Function) {
name.textContent = ''
let button = document.createElement('button')
name.classList.add('has-button')
name.appendChild(button)
button.textContent = key
button.addEventListener('click', e => group[key](e))
} else if (group[key] instanceof window.Node) value.appendChild(group[key])
else value.textContent = group[key]
}
item.appendChild(name)
item.appendChild(value)
table.appendChild(item)
}
toolbar.appendChild(table)
}
let heartbeat = toolbarNodes.heartbeat = document.createElement('div')
heartbeat.classList.add('heartbeat')
heartbeat.textContent = '❤'
toolbar.appendChild(heartbeat)
}
connection.on('heartbeat', () => {
if (screenAttached && toolbarNodes.heartbeat) {
toolbarNodes.heartbeat.classList.remove('beat')
window.requestAnimationFrame(() => {
toolbarNodes.heartbeat.classList.add('beat')
})
}
})
updateToolbar = function () {
initToolbar()
Object.assign(toolbarData.cursor, {
Position: `col ${screen.cursor.x + 1}, ln ${screen.cursor.y + 1}`,
Style: screen.cursor.style + (screen.cursor.blinking ? ', blink' : ''),
Visible: screen.cursor.visible,
Hanging: screen.cursor.hanging
})
let drawTime = Math.round((drawData.endTime - drawData.startTime) * 100) / 100
toolbarData.drawing['Last Update'] = `${drawData.reason} (${drawTime}ms)`
toolbarData.drawing['Fancy Graphics'].checked = !!screen.layout.renderer.graphics
for (let i in toolbarData) {
let group = toolbarData[i]
let nodes = toolbarNodes[i]
for (let key in group) {
if (key === 'title') continue
let value = nodes[key].value
if (!(group[key] instanceof window.Node) && !(group[key] instanceof Function)) {
value.textContent = group[key]
}
}
}
}
screen.on('update', updateToolbar)
screen.on('internal', data => {
if (screenAttached && toolbarData) {
Object.assign(toolbarData.internal, {
Flags: data.flags.toString(2),
'Cursor Attributes': data.cursorAttrs.toString(2),
'Code Page': `${data.charsetGx} (${data.charsetG0}, ${data.charsetG1})`,
Heap: data.freeHeap,
Clients: data.clientCount
})
drawData.scrollRegion = [data.regionStart, data.regionEnd]
updateToolbar()
}
})
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,250 @@
const $ = require('../lib/chibi')
const { qs, mk } = require('../utils')
const localize = require('../lang')
const Notify = require('../notif')
const TermScreen = require('./screen')
const TermConnection = require('./connection')
const TermInput = require('./input')
const TermUpload = require('./upload')
const initSoftKeyboard = require('./soft_keyboard')
const attachDebugger = require('./debug')
const initButtons = require('./buttons')
/** Init the terminal sub-module - called from HTML */
module.exports = function (opts) {
const screen = new TermScreen()
const conn = new TermConnection(screen)
const input = TermInput(conn, screen)
const termUpload = TermUpload(conn, input, screen)
input.termUpload = termUpload
// forward screen input events
screen.on('mousedown', (...args) => input.onMouseDown(...args))
screen.on('mousemove', (...args) => input.onMouseMove(...args))
screen.on('mouseup', (...args) => input.onMouseUp(...args))
screen.on('mousewheel', (...args) => input.onMouseWheel(...args))
screen.on('input-alts', (...args) => input.setAlts(...args))
screen.on('mouse-mode', (...args) => input.setMouseMode(...args))
// touch selection menu (the Copy button)
$.ready(() => {
const touchSelectMenu = qs('#touch-select-menu')
screen.on('show-touch-select-menu', (x, y) => {
let rect = touchSelectMenu.getBoundingClientRect()
x -= rect.width / 2
y -= rect.height / 2
touchSelectMenu.classList.add('open')
touchSelectMenu.style.transform = `translate(${x}px,${y}px)`
})
screen.on('hide-touch-select-menu', () => touchSelectMenu.classList.remove('open'))
const copyButton = qs('#touch-select-copy-btn')
if (copyButton) {
copyButton.addEventListener('click', () => {
screen.copySelectionToClipboard()
})
}
})
// buttons
const buttons = initButtons(input)
screen.on('buttons-update', update => {
buttons.labels = update.labels
buttons.colors = update.colors
})
// TODO: don't access the renderer here
buttons.palette = screen.layout.renderer.palette
screen.layout.renderer.on('palette-update', palette => {
buttons.palette = palette
})
screen.on('full-load', () => {
let scr = qs('#screen')
let errmsg = qs('#load-failed')
if (scr) scr.classList.remove('failed')
if (errmsg) errmsg.parentNode.removeChild(errmsg)
})
let setLinkVisibility = visible => {
let buttons = [...document.querySelectorAll('.x-term-conf-btn')]
if (visible) buttons.forEach(x => x.classList.remove('hidden'))
else buttons.forEach(x => x.classList.add('hidden'))
}
let setButtonVisibility = visible => {
if (visible) qs('#action-buttons').classList.remove('hidden')
else qs('#action-buttons').classList.add('hidden')
}
screen.on('opts-update', () => {
setLinkVisibility(screen.showLinks)
setButtonVisibility(screen.showButtons)
})
screen.on('title-update', text => {
qs('#screen-title').textContent = text
if (!text) text = 'Terminal'
qs('title').textContent = `${text} :: ESPTerm`
})
// connection status
let showSplashTimeout = null
let showSplash = (obj, delay = 250) => {
clearTimeout(showSplashTimeout)
showSplashTimeout = setTimeout(() => {
screen.window.statusScreen = obj
}, delay)
}
conn.on('open', () => {
// console.log('*open')
showSplash({ title: localize('term_conn.connecting'), loading: true })
})
conn.on('connect', () => {
// console.log('*connect')
showSplash({ title: localize('term_conn.waiting_content'), loading: true })
})
screen.on('load', () => {
// console.log('*load')
clearTimeout(showSplashTimeout)
if (screen.window.statusScreen) screen.window.statusScreen = null
})
conn.on('disconnect', () => {
// console.log('*disconnect')
showSplash({ title: localize('term_conn.disconnected') }, 500)
screen.screen = []
screen.screenFG = []
screen.screenBG = []
screen.screenAttrs = []
})
conn.on('silence', () => {
// console.log('*silence')
showSplash({ title: localize('term_conn.waiting_server'), loading: true }, 0)
})
// conn.on('ping-fail', () => { screen.window.statusScreen = { title: 'Disconnected' } })
conn.on('ping-success', () => {
// console.log('*ping-success')
showSplash({ title: localize('term_conn.reconnecting'), loading: true }, 0)
})
conn.init()
input.init(opts)
termUpload.init()
Notify.init()
window.onerror = function (errorMsg, file, line, col) {
Notify.show(`<b>JS ERROR!</b><br>${errorMsg}<br>at ${file}:${line}:${col}`, 10000, true)
return false
}
qs('#screen').appendChild(screen.layout.canvas)
initSoftKeyboard(screen, input)
if (attachDebugger) attachDebugger(screen, conn)
// fullscreen mode
let fullscreenIcon = {} // dummy
let isFullscreen = false
let properFullscreen = false
let fitScreen = false
let screenPadding = screen.layout.window.padding
let fitScreenIfNeeded = function fitScreenIfNeeded () {
if (isFullscreen) {
fullscreenIcon.className = 'icn-resize-small'
if (properFullscreen) {
screen.layout.window.fitIntoWidth = window.screen.width
screen.layout.window.fitIntoHeight = window.screen.height
screen.layout.window.padding = 0
} else {
screen.layout.window.fitIntoWidth = window.innerWidth
if (qs('#term-nav').classList.contains('hidden')) {
screen.layout.window.fitIntoHeight = window.innerHeight
} else {
screen.layout.window.fitIntoHeight = window.innerHeight - 24
}
screen.layout.window.padding = 0
}
} else {
fullscreenIcon.className = 'icn-resize-full'
screen.layout.window.fitIntoWidth = fitScreen ? window.innerWidth - 4 : 0
screen.layout.window.fitIntoHeight = fitScreen ? window.innerHeight : 0
screen.layout.window.padding = screenPadding
}
}
fitScreenIfNeeded()
window.addEventListener('resize', fitScreenIfNeeded)
let toggleFitScreen = function () {
fitScreen = !fitScreen
const resizeButtonIcon = qs('#resize-button-icon')
if (fitScreen) {
resizeButtonIcon.classList.remove('icn-resize-small')
resizeButtonIcon.classList.add('icn-resize-full')
} else {
resizeButtonIcon.classList.remove('icn-resize-full')
resizeButtonIcon.classList.add('icn-resize-small')
}
fitScreenIfNeeded()
}
qs('#term-fit-screen').addEventListener('click', function () {
toggleFitScreen()
return false
})
// add fullscreen mode & button
if (window.Element.prototype.requestFullscreen || window.Element.prototype.webkitRequestFullscreen) {
properFullscreen = true
let checkForFullscreen = function () {
// document.fullscreenElement is not really supported yet, so here's a hack
if (isFullscreen && (window.innerWidth !== window.screen.width || window.innerHeight !== window.screen.height)) {
isFullscreen = false
fitScreenIfNeeded()
}
}
setInterval(checkForFullscreen, 500)
}
// (why are the buttons anchors?)
let button = mk('a')
button.id = 'fullscreen-button'
button.href = '#'
button.addEventListener('click', e => {
e.preventDefault()
if (document.body.classList.contains('pseudo-fullscreen')) {
document.body.classList.remove('pseudo-fullscreen')
isFullscreen = false
fitScreenIfNeeded()
return
}
isFullscreen = true
fitScreenIfNeeded()
screen.layout.updateSize()
if (properFullscreen) {
if (screen.layout.canvas.requestFullscreen) screen.layout.canvas.requestFullscreen()
else screen.layout.canvas.webkitRequestFullscreen()
} else {
document.body.classList.add('pseudo-fullscreen')
}
})
fullscreenIcon = mk('i')
fullscreenIcon.className = 'icn-resize-full'
button.appendChild(fullscreenIcon)
let span = mk('span')
span.textContent = localize('term_nav.fullscreen')
button.appendChild(span)
qs('#term-nav').insertBefore(button, qs('#term-nav').firstChild)
// for debugging
window.termScreen = screen
window.buttons = buttons
window.conn = conn
window.input = input
window.termUpl = termUpload
}

@ -0,0 +1,461 @@
const { encode2B } = require('../utils')
/**
* User input
*
* --- Rx messages: ---
* S - screen content (binary encoding of the entire screen with simple compression)
* T - text labels - Title and buttons, \0x01-separated
* B - beep
* . - heartbeat
*
* --- Tx messages ---
* s - string
* b - action button
* p - mb press
* r - mb release
* m - mouse move
*/
module.exports = function (conn, screen) {
// handle for input object
let input
const KEY_NAMES = {
0x03: 'Cancel',
0x06: 'Help',
0x08: 'Backspace',
0x09: 'Tab',
0x0C: 'Clear',
0x0D: 'Enter',
0x10: 'Shift',
0x11: 'Control',
0x12: 'Alt',
0x13: 'Pause',
0x14: 'CapsLock',
0x1B: 'Escape',
0x20: ' ',
0x21: 'PageUp',
0x22: 'PageDown',
0x23: 'End',
0x24: 'Home',
0x25: 'ArrowLeft',
0x26: 'ArrowUp',
0x27: 'ArrowRight',
0x28: 'ArrowDown',
0x29: 'Select',
0x2A: 'Print',
0x2B: 'Execute',
0x2C: 'PrintScreen',
0x2D: 'Insert',
0x2E: 'Delete',
0x3A: ':',
0x3B: ';',
0x3C: '<',
0x3D: '=',
0x3E: '>',
0x3F: '?',
0x40: '@',
0x5B: 'Meta',
0x5C: 'Meta',
0x5D: 'ContextMenu',
0x6A: 'Numpad*',
0x6B: 'Numpad+',
0x6D: 'Numpad-',
0x6E: 'Numpad.',
0x6F: 'Numpad/',
0x90: 'NumLock',
0x91: 'ScrollLock',
0xA0: '^',
0xA1: '!',
0xA2: '"',
0xA3: '#',
0xA4: '$',
0xA5: '%',
0xA6: '&',
0xA7: '_',
0xA8: '(',
0xA9: ')',
0xAA: '*',
0xAB: '+',
0xAC: '|',
0xAD: '-',
0xAE: '{',
0xAF: '}',
0xB0: '~',
0xBA: ';',
0xBB: '=',
0xBC: 'Numpad,',
0xBD: '-',
0xBE: 'Numpad,',
0xC0: '`',
0xC2: 'Numpad,',
0xDB: '[',
0xDC: '\\',
0xDD: ']',
0xDE: "'",
0xE0: 'Meta'
}
// numbers 0-9
for (let i = 0x30; i <= 0x39; i++) KEY_NAMES[i] = String.fromCharCode(i)
// characters A-Z
for (let i = 0x41; i <= 0x5A; i++) KEY_NAMES[i] = String.fromCharCode(i)
// function F1-F20
for (let i = 0x70; i <= 0x83; i++) KEY_NAMES[i] = `F${i - 0x70 + 1}`
// numpad 0-9
for (let i = 0x60; i <= 0x69; i++) KEY_NAMES[i] = `Numpad${i - 0x60}`
let cfg = {
np_alt: false, // Application Numpad Mode
cu_alt: false, // Application Cursors Mode
fn_alt: false, // SS3 function keys mode
mt_click: false, // Mouse click tracking
mt_move: false, // Mouse move tracking
no_keys: false, // Suppress any key / clipboard event intercepting
crlf_mode: false, // Enter sends CR LF
all_fn: false // Capture also F5, F11 and F12
}
/** Fn alt choice for key message */
const fa = (alt, normal) => cfg.fn_alt ? alt : normal
/** Cursor alt choice for key message */
const ca = (alt, normal) => cfg.cu_alt ? alt : normal
/** Numpad alt choice for key message */
const na = (alt, normal) => cfg.np_alt ? alt : normal
const keymap = {
/* eslint-disable key-spacing */
'Backspace': '\x08',
'Tab': '\x09',
'Enter': () => cfg.crlf_mode ? '\x0d\x0a' : '\x0d',
'Control+Enter': '\x0a',
'Escape': '\x1b',
'ArrowUp': () => ca('\x1bOA', '\x1b[A'),
'ArrowDown': () => ca('\x1bOB', '\x1b[B'),
'ArrowRight': () => ca('\x1bOC', '\x1b[C'),
'ArrowLeft': () => ca('\x1bOD', '\x1b[D'),
'Home': () => ca('\x1bOH', fa('\x1b[H', '\x1b[1~')),
'Insert': '\x1b[2~',
'Delete': '\x1b[3~',
'End': () => ca('\x1bOF', fa('\x1b[F', '\x1b[4~')),
'PageUp': '\x1b[5~',
'PageDown': '\x1b[6~',
'F1': () => fa('\x1bOP', '\x1b[11~'),
'F2': () => fa('\x1bOQ', '\x1b[12~'),
'F3': () => fa('\x1bOR', '\x1b[13~'),
'F4': () => fa('\x1bOS', '\x1b[14~'),
'F5': '\x1b[15~', // note the disconnect
'F6': '\x1b[17~',
'F7': '\x1b[18~',
'F8': '\x1b[19~',
'F9': '\x1b[20~',
'F10': '\x1b[21~', // note the disconnect
'F11': '\x1b[23~',
'F12': '\x1b[24~',
'Shift+F1': () => fa('\x1bO1;2P', '\x1b[25~'),
'Shift+F2': () => fa('\x1bO1;2Q', '\x1b[26~'), // note the disconnect
'Shift+F3': () => fa('\x1bO1;2R', '\x1b[28~'),
'Shift+F4': () => fa('\x1bO1;2S', '\x1b[29~'), // note the disconnect
'Shift+F5': () => fa('\x1b[15;2~', '\x1b[31~'),
'Shift+F6': () => fa('\x1b[17;2~', '\x1b[32~'),
'Shift+F7': () => fa('\x1b[18;2~', '\x1b[33~'),
'Shift+F8': () => fa('\x1b[19;2~', '\x1b[34~'),
'Shift+F9': () => fa('\x1b[20;2~', '\x1b[35~'), // 35-38 are not standard - but what is?
'Shift+F10': () => fa('\x1b[21;2~', '\x1b[36~'),
'Shift+F11': () => fa('\x1b[22;2~', '\x1b[37~'),
'Shift+F12': () => fa('\x1b[23;2~', '\x1b[38~'),
'Numpad0': () => na('\x1bOp', '0'),
'Numpad1': () => na('\x1bOq', '1'),
'Numpad2': () => na('\x1bOr', '2'),
'Numpad3': () => na('\x1bOs', '3'),
'Numpad4': () => na('\x1bOt', '4'),
'Numpad5': () => na('\x1bOu', '5'),
'Numpad6': () => na('\x1bOv', '6'),
'Numpad7': () => na('\x1bOw', '7'),
'Numpad8': () => na('\x1bOx', '8'),
'Numpad9': () => na('\x1bOy', '9'),
'Numpad*': () => na('\x1bOR', '*'),
'Numpad+': () => na('\x1bOl', '+'),
'Numpad-': () => na('\x1bOS', '-'),
'Numpad.': () => na('\x1bOn', '.'),
'Numpad/': () => na('\x1bOQ', '/'),
// we don't implement numlock key (should change in numpad_alt mode,
// but it's even more useless than the rest and also has the side
// effect of changing the user's numlock state)
// shortcuts
'Control+]': '\x1b', // alternate way to enter ESC
'Control+\\': '\x1c',
'Control+[': '\x1d',
'Control+^': '\x1e',
'Control+_': '\x1f',
// extra controls
'Control+ArrowLeft': '\x1f[1;5D',
'Control+ArrowRight': '\x1f[1;5C',
'Control+ArrowUp': '\x1f[1;5A',
'Control+ArrowDown': '\x1f[1;5B',
'Control+Home': '\x1f[1;5H',
'Control+End': '\x1f[1;5F',
// extra shift controls
'Shift+ArrowLeft': '\x1f[1;2D',
'Shift+ArrowRight': '\x1f[1;2C',
'Shift+ArrowUp': '\x1f[1;2A',
'Shift+ArrowDown': '\x1f[1;2B',
'Shift+Home': '\x1f[1;2H',
'Shift+End': '\x1f[1;2F',
// macOS text editing commands
'Alt+ArrowLeft': '\x1bb', // ⌥← to go back a word (^[b)
'Alt+ArrowRight': '\x1bf', // ⌥→ to go forward one word (^[f)
'Meta+ArrowLeft': '\x01', // ⌘← to go to the beginning of a line (^A)
'Meta+ArrowRight': '\x05', // ⌘→ to go to the end of a line (^E)
'Alt+Backspace': '\x17', // ⌥⌫ to delete a word (^W)
'Meta+Backspace': '\x15', // ⌘⌫ to delete to the beginning of a line (^U)
// copy to clipboard
'Control+Shift+C' () {
screen.copySelectionToClipboard()
},
'Control+Insert' () {
screen.copySelectionToClipboard()
},
// toggle debug mode
'Control+F12' () {
screen.window.debug ^= 1
}
/* eslint-enable key-spacing */
}
// ctrl+[A-Z] sent as simple low ASCII codes
for (let i = 1; i <= 26; i++) {
keymap[`Control+${String.fromCharCode(0x40 + i)}`] = String.fromCharCode(i)
}
/** Send a literal message */
function sendString (str) {
return conn.send('s' + str)
}
/** Send a button event */
function sendButton (index) {
conn.send('b' + String.fromCharCode(index + 1))
}
const shouldAcceptEvent = function () {
if (cfg.no_keys) return false
if (document.activeElement instanceof window.HTMLTextAreaElement) return false
return true
}
const keyBlacklist = [
'F5', 'F11', 'F12', 'Shift+F5'
]
let softModifiers = {
alt: false,
ctrl: false,
meta: false,
shift: false
}
const handleKeyDown = function (e) {
if (!shouldAcceptEvent()) return
let modifiers = []
// sorted alphabetically
if (e.altKey || softModifiers.alt) modifiers.push('Alt')
if (e.ctrlKey || softModifiers.ctrl) modifiers.push('Control')
if (e.metaKey || softModifiers.meta) modifiers.push('Meta')
if (e.shiftKey || softModifiers.shift) modifiers.push('Shift')
let key = KEY_NAMES[e.which] || e.key
// ignore clipboard events
if ((e.ctrlKey || e.metaKey) && key === 'V') return
let binding = null
for (let name in keymap) {
let itemModifiers = name.split('+')
let itemKey = itemModifiers.pop()
if (itemKey === key && itemModifiers.sort().join() === modifiers.join()) {
if (keyBlacklist.includes(name) && !cfg.all_fn) continue
binding = keymap[name]
break
}
}
if (binding) {
if (binding instanceof Function) binding = binding()
e.preventDefault()
if (typeof binding === 'string') {
sendString(binding)
}
}
}
/** Bind/rebind key messages */
function initKeys ({ allFn }) {
// This takes care of text characters typed
window.addEventListener('keypress', function (evt) {
if (!shouldAcceptEvent()) return
if (evt.ctrlKey || evt.metaKey) return
let str = ''
if (evt.key && evt.key.length === 1) str = evt.key
else if (evt.which && evt.which !== 229) str = String.fromCodePoint(evt.which)
if (str.length > 0 && str.charCodeAt(0) >= 32) {
// prevent space from scrolling
if (evt.which === 32) evt.preventDefault()
sendString(str)
}
})
window.addEventListener('keydown', handleKeyDown)
window.addEventListener('copy', e => {
if (!shouldAcceptEvent()) return
let selectedText = screen.getSelectedText()
if (selectedText) {
e.preventDefault()
e.clipboardData.setData('text/plain', selectedText)
}
})
window.addEventListener('paste', e => {
if (!shouldAcceptEvent()) return
e.preventDefault()
let string = e.clipboardData.getData('text/plain')
if (string.includes('\n') || string.length > 90) {
if (!input.termUpload) console.error('input.termUpload is undefined')
input.termUpload.setContent(string)
input.termUpload.open()
} else {
// simple string, just paste it
if (screen.bracketedPaste) string = `\x1b[200~${string}\x1b[201~`
sendString(string)
}
})
cfg.all_fn = allFn
}
// mouse button states
let mb1 = 0
let mb2 = 0
let mb3 = 0
/** Init the Input module */
function init (opts) {
initKeys(opts)
// global mouse state tracking - for motion reporting
window.addEventListener('mousedown', evt => {
if (evt.button === 0) mb1 = 1
if (evt.button === 1) mb2 = 1
if (evt.button === 2) mb3 = 1
})
window.addEventListener('mouseup', evt => {
if (evt.button === 0) mb1 = 0
if (evt.button === 1) mb2 = 0
if (evt.button === 2) mb3 = 0
})
}
// record modifier keys
// bits: Meta, Alt, Shift, Ctrl
let modifiers = 0b0000
window.addEventListener('keydown', e => {
if (e.ctrlKey) modifiers |= 1
if (e.shiftKey) modifiers |= 2
if (e.altKey) modifiers |= 4
if (e.metaKey) modifiers |= 8
})
window.addEventListener('keyup', e => {
modifiers = 0
if (e.ctrlKey) modifiers |= 1
if (e.shiftKey) modifiers |= 2
if (e.altKey) modifiers |= 4
if (e.metaKey) modifiers |= 8
})
/** Prepare modifiers byte for mouse message */
function packModifiersForMouse () {
return modifiers
}
input = {
/** Init the Input module */
init,
/** Send a literal string message */
sendString,
sendButton,
/** Enable alternate key modes (cursors, numpad, fn) */
setAlts: function (cu, np, fn, crlf) {
if (cfg.cu_alt !== cu || cfg.np_alt !== np || cfg.fn_alt !== fn || cfg.crlf_mode !== crlf) {
cfg.cu_alt = cu
cfg.np_alt = np
cfg.fn_alt = fn
cfg.crlf_mode = crlf
}
},
setMouseMode (click, move) {
cfg.mt_click = click
cfg.mt_move = move
},
// Mouse events
onMouseMove (x, y) {
if (!cfg.mt_move) return
const b = mb1 ? 1 : mb2 ? 2 : mb3 ? 3 : 0
const m = packModifiersForMouse()
conn.send('m' + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m))
},
onMouseDown (x, y, b) {
if (!cfg.mt_click) return
if (b > 3 || b < 1) return
const m = packModifiersForMouse()
conn.send('p' + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m))
},
onMouseUp (x, y, b) {
if (!cfg.mt_click) return
if (b > 3 || b < 1) return
const m = packModifiersForMouse()
conn.send('r' + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m))
// console.log("B ",b," M ",m);
},
onMouseWheel (x, y, dir) {
if (!cfg.mt_click) return
// -1 ... btn 4 (away from user)
// +1 ... btn 5 (towards user)
const m = packModifiersForMouse()
const b = (dir < 0 ? 4 : 5)
conn.send('p' + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m))
// console.log("B ",b," M ",m);
},
/**
* Prevent capturing keys. This is used for text input
* modals on the terminal screen
*/
blockKeys (yes) {
cfg.no_keys = yes
},
handleKeyDown,
softModifiers
}
return input
}

@ -0,0 +1,590 @@
const EventEmitter = require('events')
const { mk } = require('../utils')
const notify = require('../notif')
const ScreenParser = require('./screen_parser')
const ScreenLayout = require('./screen_layout')
const { ATTR_BLINK } = require('./screen_attr_bits')
/**
* A terminal screen.
*/
module.exports = class TermScreen extends EventEmitter {
constructor () {
super()
this.parser = new ScreenParser()
this.layout = new ScreenLayout()
// debug screen handle
this._debug = null
if ('AudioContext' in window || 'webkitAudioContext' in window) {
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)()
} else {
console.warn('No AudioContext!')
}
this._window = {
width: 0,
height: 0,
// two bits. LSB: debug enabled by user, MSB: debug enabled by server
debug: 0,
statusScreen: null
}
// make writing to window update size and draw
this.window = new Proxy(this._window, {
set (target, key, value) {
if (target[key] !== value) {
target[key] = value
self.updateLayout()
self.renderScreen(`window:${key}=${value}`)
self.emit(`update-window:${key}`, value)
}
return true
}
})
this.on('update-window:debug', debug => { this.layout.window.debug = !!debug })
this.cursor = {
x: 0,
y: 0,
blinking: true,
visible: true,
hanging: false,
style: 'block'
}
const self = this
// current selection
this.selection = {
// when false, this will prevent selection in favor of mouse events,
// though alt can be held to override it
selectable: null,
// selection start and end (x, y) tuples
start: [0, 0],
end: [0, 0],
setSelectable (value) {
if (value !== this.selectable) {
this.selectable = self.layout.selectable = value
}
}
}
// mouse features
this.mouseMode = { clicks: false, movement: false }
this.showLinks = false
this.showButtons = false
this.title = ''
this.bracketedPaste = false
this.blinkingCellCount = 0
this.reverseVideo = false
this.screen = []
this.screenFG = []
this.screenBG = []
this.screenAttrs = []
this.screenLines = []
// For testing TODO remove
// this.screenLines[0] = 0b001
// this.screenLines[1] = 0b010
// this.screenLines[2] = 0b100
// this.screenLines[3] = 0b011
// this.screenLines[4] = 0b101
let selecting = false
let selectStart = (x, y) => {
if (selecting) return
selecting = true
this.selection.start = this.selection.end = this.layout.screenToGrid(x, y, true)
this.renderScreen('select-start')
}
let selectMove = (x, y) => {
if (!selecting) return
this.selection.end = this.layout.screenToGrid(x, y, true)
this.renderScreen('select-move')
}
let selectEnd = (x, y) => {
if (!selecting) return
selecting = false
this.selection.end = this.layout.screenToGrid(x, y, true)
this.renderScreen('select-end')
Object.assign(this.selection, this.getNormalizedSelection())
}
// bind event listeners
this.layout.on('mousedown', e => {
this.emit('hide-touch-select-menu')
if ((this.selection.selectable || e.altKey) && e.button === 0) {
selectStart(e.offsetX, e.offsetY)
} else {
this.emit('mousedown', ...this.layout.screenToGrid(e.offsetX, e.offsetY), e.button + 1)
}
})
window.addEventListener('mousemove', e => {
selectMove(e.offsetX, e.offsetY)
})
window.addEventListener('mouseup', e => {
selectEnd(e.offsetX, e.offsetY)
})
// touch event listeners
let touchPosition = null
let touchDownTime = 0
let touchSelectMinTime = 500
let touchDidMove = false
let getTouchPositionOffset = touch => {
let rect = this.layout.canvas.getBoundingClientRect()
return [touch.clientX - rect.left, touch.clientY - rect.top]
}
this.layout.on('touchstart', e => {
touchPosition = getTouchPositionOffset(e.touches[0])
touchDidMove = false
touchDownTime = Date.now()
if (this.mouseMode.clicks) {
this.emit('mousedown', ...this.layout.screenToGrid(...touchPosition), 1)
e.preventDefault()
}
})
this.layout.on('touchmove', e => {
touchPosition = getTouchPositionOffset(e.touches[0])
if (!selecting && touchDidMove === false) {
if (touchDownTime < Date.now() - touchSelectMinTime) {
selectStart(...touchPosition)
}
} else if (selecting) {
e.preventDefault()
selectMove(...touchPosition)
} else if (this.mouseMode.movement && !selecting) {
this.emit('mousemove', ...this.layout.screenToGrid(...touchPosition))
e.preventDefault()
}
touchDidMove = true
})
this.layout.on('touchend', e => {
if (e.touches[0]) {
touchPosition = getTouchPositionOffset(e.touches[0])
}
if (selecting) {
e.preventDefault()
selectEnd(...touchPosition)
// selection ended; show touch select menu
// use middle position for x and one line above for y
let selectionPos = this.layout.gridToScreen(
(this.selection.start[0] + this.selection.end[0]) / 2,
this.selection.start[1] - 1
)
this.emit('show-touch-select-menu', selectionPos[0], selectionPos[1])
} else if (this.mouseMode.clicks) {
this.emit('mouseup', ...this.layout.screenToGrid(...touchPosition), 1)
e.preventDefault()
}
if (!touchDidMove && !this.mouseMode.clicks) {
this.emit('tap', Object.assign(e, {
x: touchPosition[0],
y: touchPosition[1]
}))
} else if (!touchDidMove) this.resetSelection()
touchPosition = null
})
this.on('tap', e => {
if (this.selection.start[0] !== this.selection.end[0] ||
this.selection.start[1] !== this.selection.end[1]) {
// selection is not empty
this.resetSelection()
} else {
e.preventDefault()
this.emit('open-soft-keyboard')
}
})
this.layout.on('mousemove', e => {
if (!selecting) {
this.emit('mousemove', ...this.layout.screenToGrid(e.offsetX, e.offsetY))
}
})
this.layout.on('mouseup', e => {
if (!selecting) {
this.emit('mouseup', ...this.layout.screenToGrid(e.offsetX, e.offsetY),
e.button + 1)
}
})
let aggregateWheelDelta = 0
this.layout.on('wheel', e => {
if (this.mouseMode.clicks) {
if (Math.abs(e.wheelDeltaY) === 120) {
// mouse wheel scrolling
this.emit('mousewheel', ...this.layout.screenToGrid(e.offsetX, e.offsetY), e.deltaY > 0 ? 1 : -1)
} else {
// smooth scrolling
aggregateWheelDelta -= e.wheelDeltaY
if (Math.abs(aggregateWheelDelta) >= 40) {
this.emit('mousewheel', ...this.layout.screenToGrid(e.offsetX, e.offsetY), aggregateWheelDelta > 0 ? 1 : -1)
aggregateWheelDelta = 0
}
}
// prevent page scrolling
e.preventDefault()
}
})
this.layout.on('contextmenu', e => {
if (this.mouseMode.clicks) {
// prevent mouse keys getting stuck
e.preventDefault()
}
selectEnd(e.offsetX, e.offsetY)
})
}
resetScreen () {
const { width, height } = this.window
this.blinkingCellCount = 0
this.screen.screen = new Array(width * height).fill(' ')
this.screen.screenFG = new Array(width * height).fill(0)
this.screen.screenBG = new Array(width * height).fill(0)
this.screen.screenAttrs = new Array(width * height).fill(0)
this.screen.screenLines = new Array(height).fill(0)
}
updateLayout () {
this.layout.window.width = this.window.width
this.layout.window.height = this.window.height
}
renderScreen (reason) {
let selection = []
for (let cell = 0; cell < this.screen.length; cell++) {
selection.push(this.isInSelection(cell % this.window.width, Math.floor(cell / this.window.width)))
}
this.layout.render(reason, {
width: this.window.width,
height: this.window.height,
screen: this.screen,
screenFG: this.screenFG,
screenBG: this.screenBG,
screenSelection: selection,
screenAttrs: this.screenAttrs,
screenLines: this.screenLines,
cursor: this.cursor,
statusScreen: this.window.statusScreen,
reverseVideo: this.reverseVideo,
hasBlinkingCells: !!this.blinkingCellCount
})
}
resetSelection () {
this.selection.start = this.selection.end = [0, 0]
this.emit('hide-touch-select-menu')
this.renderScreen('select-reset')
}
/**
* Returns a normalized version of the current selection, such that `start`
* is always before `end`.
* @returns {Object} the normalized selection, with `start` and `end`
*/
getNormalizedSelection () {
let { start, end } = this.selection
// if the start line is after the end line, or if they're both on the same
// line but the start column comes after the end column, swap
if (start[1] > end[1] || (start[1] === end[1] && start[0] > end[0])) {
[start, end] = [end, start]
}
return { start, end }
}
/**
* Returns whether or not a given cell is in the current selection.
* @param {number} col - the column (x)
* @param {number} line - the line (y)
* @returns {boolean}
*/
isInSelection (col, line) {
let { start, end } = this.getNormalizedSelection()
let colAfterStart = start[0] <= col
let colBeforeEnd = col < end[0]
let onStartLine = line === start[1]
let onEndLine = line === end[1]
if (onStartLine && onEndLine) return colAfterStart && colBeforeEnd
else if (onStartLine) return colAfterStart
else if (onEndLine) return colBeforeEnd
else return start[1] < line && line < end[1]
}
/**
* Sweeps for selected cells and joins them in a multiline string.
* @returns {string} the selection
*/
getSelectedText () {
const screenLength = this.window.width * this.window.height
let lines = []
let previousLineIndex = -1
for (let cell = 0; cell < screenLength; cell++) {
let x = cell % this.window.width
let y = Math.floor(cell / this.window.width)
if (this.isInSelection(x, y)) {
if (previousLineIndex !== y) {
previousLineIndex = y
lines.push('')
}
lines[lines.length - 1] += this.screen[cell]
}
}
return lines.join('\n')
}
/**
* Copies the selection to clipboard and creates a notification balloon.
*/
copySelectionToClipboard () {
let selectedText = this.getSelectedText()
// don't copy anything if nothing is selected
if (!selectedText) return
let textarea = mk('textarea')
document.body.appendChild(textarea)
textarea.value = selectedText
textarea.select()
if (document.execCommand('copy')) {
notify.show('Copied to clipboard')
} else {
notify.show('Failed to copy')
}
document.body.removeChild(textarea)
}
/**
* Shows an actual notification (if possible) or a notification balloon.
* @param {string} text - the notification content
*/
showNotification (text) {
console.info(`Notification: ${text}`)
if (window.Notification && window.Notification.permission === 'granted') {
let notification = new window.Notification('ESPTerm', {
body: text
})
notification.addEventListener('click', () => window.focus())
} else {
if (window.Notification && window.Notification.permission !== 'denied') {
window.Notification.requestPermission()
} else {
// Fallback using the built-in notification balloon
notify.show(text)
}
}
}
/**
* Creates a beep sound.
*/
beep () {
const audioCtx = this.audioCtx
if (!audioCtx) return
// prevent screeching
if (this._lastBeep && this._lastBeep > Date.now() - 50) return
this._lastBeep = Date.now()
if (!this._convolver) {
this._convolver = audioCtx.createConvolver()
let impulseLength = audioCtx.sampleRate * 0.8
let impulse = audioCtx.createBuffer(2, impulseLength, audioCtx.sampleRate)
for (let i = 0; i < impulseLength; i++) {
impulse.getChannelData(0)[i] = (1 - i / impulseLength) ** (7 + Math.random())
impulse.getChannelData(1)[i] = (1 - i / impulseLength) ** (7 + Math.random())
}
this._convolver.buffer = impulse
this._convolver.connect(audioCtx.destination)
}
// main beep
const mainOsc = audioCtx.createOscillator()
const mainGain = audioCtx.createGain()
mainOsc.connect(mainGain)
mainGain.gain.value = 4
mainOsc.frequency.value = 750
mainOsc.type = 'sine'
// surrogate beep (making it sound like 'oops')
const surrOsc = audioCtx.createOscillator()
const surrGain = audioCtx.createGain()
surrOsc.connect(surrGain)
surrGain.gain.value = 2
surrOsc.frequency.value = 400
surrOsc.type = 'sine'
mainGain.connect(this._convolver)
surrGain.connect(this._convolver)
let startTime = audioCtx.currentTime
mainOsc.start()
mainOsc.stop(startTime + 0.5)
surrOsc.start(startTime + 0.05)
surrOsc.stop(startTime + 0.8)
let loop = function () {
if (audioCtx.currentTime < startTime + 0.8) window.requestAnimationFrame(loop)
mainGain.gain.value *= 0.8
surrGain.gain.value *= 0.8
}
loop()
}
load (...args) {
const updates = this.parser.parse(...args)
for (let update of updates) {
switch (update.topic) {
case 'screen-opts':
if (update.width !== this.window.width || update.height !== this.window.height) {
this.window.width = update.width
this.window.height = update.height
this.resetScreen()
}
this.layout.renderer.loadTheme(update.theme)
this.layout.renderer.setDefaultColors(update.defFG, update.defBG)
this.cursor.visible = update.cursorVisible
this.emit('input-alts', ...update.inputAlts)
this.mouseMode.clicks = update.trackMouseClicks
this.mouseMode.movement = update.trackMouseMovement
this.emit('mouse-mode', update.trackMouseClicks, update.trackMouseMovement)
this.selection.setSelectable(!update.trackMouseClicks && !update.trackMouseMovement)
if (this.cursor.blinking !== update.cursorBlinking) {
this.cursor.blinking = update.cursorBlinking
this.layout.renderer.resetCursorBlink()
}
this.cursor.style = update.cursorStyle
this.bracketedPaste = update.bracketedPaste
this.reverseVideo = update.reverseVideo
this.window.debug &= 0b01
this.window.debug |= (+update.debugEnabled << 1)
this.showLinks = update.showConfigLinks
this.showButtons = update.showButtons
this.emit('opts-update')
break
case 'double-lines':
this.screenLines = update.lines
this.renderScreen('double-lines')
break
case 'static-opts':
this.layout.window.fontFamily = update.fontStack || null
this.layout.window.fontSize = update.fontSize
break
case 'cursor':
if (this.cursor.x !== update.x || this.cursor.y !== update.y || this.cursor.hanging !== update.hanging) {
this.cursor.x = update.x
this.cursor.y = update.y
this.cursor.hanging = update.hanging
this.layout.renderer.resetCursorBlink()
this.emit('cursor-moved')
this.renderScreen('cursor-moved')
}
break
case 'title':
this.emit('title-update', this.title = update.title)
break
case 'buttons-update':
this.emit('buttons-update', update)
break
case 'backdrop':
this.backgroundImage = update.image
break
case 'bell':
this.beep()
break
case 'internal':
this.emit('internal', update)
break
case 'content':
const { frameX, frameY, frameWidth, frameHeight, cells } = update
if (this._debug && this.window.debug) {
this._debug.pushFrame([frameX, frameY, frameWidth, frameHeight])
}
for (let cell = 0; cell < cells.length; cell++) {
let data = cells[cell]
let cellXInFrame = cell % frameWidth
let cellYInFrame = Math.floor(cell / frameWidth)
let index = (frameY + cellYInFrame) * this.window.width + frameX + cellXInFrame
if ((this.screenAttrs[index] & ATTR_BLINK) !== (data[3] & ATTR_BLINK)) {
if (data[3] & ATTR_BLINK) this.blinkingCellCount++
else this.blinkingCellCount--
}
this.screen[index] = data[0]
this.screenFG[index] = data[1]
this.screenBG[index] = data[2]
this.screenAttrs[index] = data[3]
}
if (this.window.debug) console.log(`Blinking cells: ${this.blinkingCellCount}`)
this.renderScreen('load')
this.emit('load')
break
case 'full-load-complete':
this.emit('full-load')
break
case 'notification':
this.showNotification(update.content)
break
default:
console.warn('Unhandled update', update)
}
}
this.emit('update')
}
}

@ -0,0 +1,15 @@
// Bits in the cell attribs word
/* eslint-disable no-multi-spaces */
exports.ATTR_FG = (1 << 0) // 1 if not using default background color (ignore cell bg) - color extension bit
exports.ATTR_BG = (1 << 1) // 1 if not using default foreground color (ignore cell fg) - color extension bit
exports.ATTR_BOLD = (1 << 2) // Bold font
exports.ATTR_UNDERLINE = (1 << 3) // Underline decoration
exports.ATTR_INVERSE = (1 << 4) // Invert colors - this is useful so we can clear then with SGR manipulation commands
exports.ATTR_BLINK = (1 << 5) // Blinking
exports.ATTR_ITALIC = (1 << 6) // Italic font
exports.ATTR_STRIKE = (1 << 7) // Strike-through decoration
exports.ATTR_OVERLINE = (1 << 8) // Over-line decoration
exports.ATTR_FAINT = (1 << 9) // Faint foreground color (reduced alpha)
exports.ATTR_FRAKTUR = (1 << 10) // Fraktur font (unicode substitution)
/* eslint-enable no-multi-spaces */

@ -0,0 +1,285 @@
const EventEmitter = require('events')
const CanvasRenderer = require('./screen_renderer')
const DEFAULT_FONT = '"DejaVu Sans Mono", "Liberation Mono", "Inconsolata", "Menlo", monospace'
/**
* Manages terminal screen layout and sizing
*/
module.exports = class ScreenLayout extends EventEmitter {
constructor () {
super()
this.canvas = document.createElement('canvas')
this.renderer = new CanvasRenderer(this.canvas)
this._window = {
width: 0,
height: 0,
devicePixelRatio: 1,
fontFamily: DEFAULT_FONT,
fontSize: 20,
padding: 6,
gridScaleX: 1.0,
gridScaleY: 1.2,
fitIntoWidth: 0,
fitIntoHeight: 0,
debug: false
}
// scaling caused by fitIntoWidth/fitIntoHeight
this._windowScale = 1
// actual padding, as it may be disabled by fullscreen mode etc.
this._padding = 0
// properties of this.window that require updating size and redrawing
this.windowState = {
width: 0,
height: 0,
devicePixelRatio: 0,
padding: 0,
gridScaleX: 0,
gridScaleY: 0,
fontFamily: '',
fontSize: 0,
fitIntoWidth: 0,
fitIntoHeight: 0
}
this.charSize = { width: 0, height: 0 }
const self = this
// make writing to window update size and draw
this.window = new Proxy(this._window, {
set (target, key, value) {
if (target[key] !== value) {
target[key] = value
self.scheduleSizeUpdate()
self.renderer.scheduleDraw(`window:${key}=${value}`)
self.emit(`update-window:${key}`, value)
}
return true
}
})
this.on('update-window:debug', debug => { this.renderer.debug = debug })
this.canvas.addEventListener('mousedown', e => this.emit('mousedown', e))
this.canvas.addEventListener('mousemove', e => this.emit('mousemove', e))
this.canvas.addEventListener('mouseup', e => this.emit('mouseup', e))
this.canvas.addEventListener('touchstart', e => this.emit('touchstart', e))
this.canvas.addEventListener('touchmove', e => this.emit('touchmove', e))
this.canvas.addEventListener('touchend', e => this.emit('touchend', e))
this.canvas.addEventListener('wheel', e => this.emit('wheel', e))
this.canvas.addEventListener('contextmenu', e => this.emit('contextmenu', e))
}
/**
* Schedule a size update in the next millisecond
*/
scheduleSizeUpdate () {
clearTimeout(this._scheduledSizeUpdate)
this._scheduledSizeUpdate = setTimeout(() => this.updateSize(), 1)
}
get backgroundImage () {
return this.canvas.style.backgroundImage
}
set backgroundImage (value) {
this.canvas.style.backgroundImage = value ? `url(${value})` : ''
if (this.renderer.backgroundImage !== !!value) {
this.renderer.backgroundImage = !!value
this.renderer.resetDrawn()
this.renderer.scheduleDraw('background-image')
}
}
get selectable () {
return this.canvas.classList.contains('selectable')
}
set selectable (selectable) {
if (selectable) this.canvas.classList.add('selectable')
else this.canvas.classList.remove('selectable')
}
/**
* Returns a CSS font string with the current font settings and the
* specified modifiers.
* @param {Object} modifiers
* @param {string} [modifiers.style] - the font style
* @param {string} [modifiers.weight] - the font weight
* @returns {string} a CSS font string
*/
getFont (modifiers = {}) {
let fontStyle = modifiers.style || 'normal'
let fontWeight = modifiers.weight || 'normal'
let fontFamily = this.window.fontFamily || ''
if (fontFamily.length > 0) fontFamily += ','
fontFamily += DEFAULT_FONT
return `${fontStyle} normal ${fontWeight} ${this.window.fontSize}px ${fontFamily}`
}
/**
* Converts screen coordinates to grid coordinates.
* @param {number} x - x in pixels
* @param {number} y - y in pixels
* @param {boolean} rounded - whether to round the coord, used for select highlighting
* @returns {number[]} a tuple of (x, y) in cells
*/
screenToGrid (x, y, rounded = false) {
let cellSize = this.getCellSize()
x = x / this._windowScale - this._padding
y = y / this._windowScale - this._padding
y = Math.floor(y / cellSize.height)
if (this.renderer.drawnScreenLines[y]) x /= 2 // double size
x = Math.floor((x + (rounded ? cellSize.width / 2 : 0)) / cellSize.width)
x = Math.max(0, Math.min(this.window.width - 1, x))
y = Math.max(0, Math.min(this.window.height - 1, y))
return [x, y]
}
/**
* Converts grid coordinates to screen coordinates.
* @param {number} x - x in cells
* @param {number} y - y in cells
* @param {boolean} [withScale] - when true, will apply window scale
* @returns {number[]} a tuple of (x, y) in pixels
*/
gridToScreen (x, y, withScale = false) {
let cellSize = this.getCellSize()
if (this.renderer.drawnScreenLines[y]) x *= 2 // double size
return [x * cellSize.width, y * cellSize.height].map(v => this._padding + (withScale ? v * this._windowScale : v))
}
/**
* Update the character size, used for calculating the cell size.
* The space character is used for measuring.
* @returns {Object} the character size with `width` and `height` in pixels
*/
updateCharSize () {
this.charSize = {
width: this.renderer.getCharWidthFor(this.getFont()),
height: this.window.fontSize
}
return this.charSize
}
/**
* The cell size, which is the character size multiplied by the grid scale.
* @returns {Object} the cell size with `width` and `height` in pixels
*/
getCellSize () {
if (!this.charSize.height && this.window.fontSize) this.updateCharSize()
return {
width: Math.ceil(this.charSize.width * this.window.gridScaleX),
height: Math.ceil(this.charSize.height * this.window.gridScaleY)
}
}
/**
* Updates the canvas size if it changed
*/
updateSize () {
// see below (this is just updating it)
this._window.devicePixelRatio = Math.ceil(this._windowScale * (window.devicePixelRatio || 1))
let didChange = false
for (let key in this.windowState) {
if (this.windowState.hasOwnProperty(key) && this.windowState[key] !== this.window[key]) {
didChange = true
this.windowState[key] = this.window[key]
}
}
if (didChange) {
const {
width,
height,
fitIntoWidth,
fitIntoHeight,
padding
} = this.window
this.updateCharSize()
const cellSize = this.getCellSize()
// real height of the canvas element in pixels
let realWidth = width * cellSize.width
let realHeight = height * cellSize.height
let originalWidth = realWidth
if (fitIntoWidth && fitIntoHeight) {
let terminalAspect = realWidth / realHeight
let fitAspect = fitIntoWidth / fitIntoHeight
if (terminalAspect < fitAspect) {
// align heights
realHeight = fitIntoHeight - 2 * padding
realWidth = realHeight * terminalAspect
} else {
// align widths
realWidth = fitIntoWidth - 2 * padding
realHeight = realWidth / terminalAspect
}
}
// store new window scale
this._windowScale = realWidth / originalWidth
realWidth += 2 * padding
realHeight += 2 * padding
// store padding
this._padding = padding * (originalWidth / realWidth)
// the DPR must be rounded to a very nice value to prevent gaps between cells
let devicePixelRatio = this._window.devicePixelRatio = Math.ceil(this._windowScale * (window.devicePixelRatio || 1))
this.canvas.width = (width * cellSize.width + 2 * Math.round(this._padding)) * devicePixelRatio
this.canvas.style.width = `${realWidth}px`
this.canvas.height = (height * cellSize.height + 2 * Math.round(this._padding)) * devicePixelRatio
this.canvas.style.height = `${realHeight}px`
// the screen has been cleared (by changing canvas width)
this.renderer.resetDrawn()
this.renderer.render('update-size', this.serializeRenderData())
this.emit('size-update')
}
}
serializeRenderData () {
return {
padding: Math.round(this._padding),
devicePixelRatio: this.window.devicePixelRatio,
charSize: this.charSize,
cellSize: this.getCellSize(),
fonts: [
this.getFont(),
this.getFont({ weight: 'bold' }),
this.getFont({ style: 'italic' }),
this.getFont({ weight: 'bold', style: 'italic' })
]
}
}
render (reason, data) {
this.window.width = data.width
this.window.height = data.height
Object.assign(data, this.serializeRenderData())
this.renderer.render(reason, data)
}
}

@ -0,0 +1,416 @@
const {
ATTR_FG,
ATTR_BG,
ATTR_BOLD,
ATTR_UNDERLINE,
ATTR_BLINK,
ATTR_STRIKE,
ATTR_OVERLINE,
ATTR_FAINT
} = require('./screen_attr_bits')
// constants for decoding the update blob
const SEQ_SKIP = 1
const SEQ_REPEAT = 2
const SEQ_SET_COLORS = 3
const SEQ_SET_ATTRS = 4
const SEQ_SET_FG = 5
const SEQ_SET_BG = 6
const SEQ_SET_ATTR_0 = 7
// decode a number encoded as a unicode code point
function du (str) {
if (!str) return NaN
let num = str.codePointAt(0)
if (num > 0xDFFF) num -= 0x800
return num - 1
}
/* eslint-disable no-multi-spaces */
// mnemonic
const TOPIC_SCREEN_OPTS = 'O' // O-ptions
const TOPIC_STATIC_OPTS = 'P' // P-arams
const TOPIC_CONTENT = 'S' // S-creen
const TOPIC_TITLE = 'T' // T-itle
const TOPIC_BUTTONS = 'B' // B-uttons
const TOPIC_CURSOR = 'C' // C-ursor
const TOPIC_INTERNAL = 'D' // D-ebug
const TOPIC_BELL = '!' // !!!
const TOPIC_BACKDROP = 'W' // W-allpaper
const TOPIC_DOUBLE_LINES = 'H' // H-uge
const OPT_CURSOR_VISIBLE = (1 << 0)
const OPT_DEBUGBAR = (1 << 1)
const OPT_CURSORS_ALT_MODE = (1 << 2)
const OPT_NUMPAD_ALT_MODE = (1 << 3)
const OPT_FN_ALT_MODE = (1 << 4)
const OPT_CLICK_TRACKING = (1 << 5)
const OPT_MOVE_TRACKING = (1 << 6)
const OPT_SHOW_BUTTONS = (1 << 7)
const OPT_SHOW_CONFIG_LINKS = (1 << 8)
// const OPT_CURSOR_SHAPE = (7 << 9)
const OPT_CRLF_MODE = (1 << 12)
const OPT_BRACKETED_PASTE = (1 << 13)
const OPT_REVERSE_VIDEO = (1 << 14)
/* eslint-enable no-multi-spaces */
/**
* A parser for screen update messages
*/
module.exports = class ScreenParser {
constructor () {
// true if full content was loaded
this.contentLoaded = false
}
parseUpdate (str) {
// console.log(`update ${str}`)
// current index
let ci = 0
let strArray = Array.from ? Array.from(str) : str.split('')
let text
const topics = du(strArray[ci++])
let collectOneTerminatedString = () => {
// TODO optimize this
text = ''
while (ci < strArray.length) {
let c = strArray[ci++]
if (c !== '\x01') {
text += c
} else {
break
}
}
return text
}
let collectColor = () => {
let c = du(strArray[ci++])
if (c & 0x10000) { // support for trueColor
c &= 0xFFF
c |= (du(strArray[ci++]) & 0xFFF) << 12
c += 256
}
return c
}
const updates = []
while (ci < strArray.length) {
const topic = strArray[ci++]
if (topic === TOPIC_SCREEN_OPTS) {
const height = du(strArray[ci++])
const width = du(strArray[ci++])
const theme = du(strArray[ci++])
const defFG = collectColor()
const defBG = collectColor()
// process attributes
const attributes = du(strArray[ci++])
const cursorVisible = !!(attributes & OPT_CURSOR_VISIBLE)
// HACK: input alts are formatted as arguments for Input#setAlts
const inputAlts = [
!!(attributes & OPT_CURSORS_ALT_MODE),
!!(attributes & OPT_NUMPAD_ALT_MODE),
!!(attributes & OPT_FN_ALT_MODE),
!!(attributes & OPT_CRLF_MODE)
]
const trackMouseClicks = !!(attributes & OPT_CLICK_TRACKING)
const trackMouseMovement = !!(attributes & OPT_MOVE_TRACKING)
// 0 - Block blink 2 - Block steady (1 is unused)
// 3 - Underline blink 4 - Underline steady
// 5 - I-bar blink 6 - I-bar steady
let cursorShape = (attributes >> 9) & 0x07
// if it's not zero, decrement such that the two most significant bits
// are the type and the least significant bit is the blink state
if (cursorShape > 0) cursorShape--
let cursorStyle = cursorShape >> 1
const cursorBlinking = !(cursorShape & 1)
if (cursorStyle === 0) cursorStyle = 'block'
else if (cursorStyle === 1) cursorStyle = 'line'
else cursorStyle = 'bar'
const showButtons = !!(attributes & OPT_SHOW_BUTTONS)
const showConfigLinks = !!(attributes & OPT_SHOW_CONFIG_LINKS)
const bracketedPaste = !!(attributes & OPT_BRACKETED_PASTE)
const reverseVideo = !!(attributes & OPT_REVERSE_VIDEO)
const debugEnabled = !!(attributes & OPT_DEBUGBAR)
updates.push({
topic: 'screen-opts',
width,
height,
theme,
defFG,
defBG,
cursorVisible,
cursorBlinking,
cursorStyle,
inputAlts,
trackMouseClicks,
trackMouseMovement,
showButtons,
showConfigLinks,
bracketedPaste,
reverseVideo,
debugEnabled
})
} else if (topic === TOPIC_CURSOR) {
// cursor position
const y = du(strArray[ci++])
const x = du(strArray[ci++])
const hanging = !!du(strArray[ci++])
updates.push({
topic: 'cursor',
x,
y,
hanging
})
} else if (topic === TOPIC_STATIC_OPTS) {
const fontStack = collectOneTerminatedString()
const fontSize = du(strArray[ci++])
updates.push({
topic: 'static-opts',
fontStack,
fontSize
})
} else if (topic === TOPIC_DOUBLE_LINES) {
let lines = []
const count = du(strArray[ci++])
for (let i = 0; i < count; i++) {
// format: INDEX<<3 | (dbl-h-bot : dbl-h-top : dbl-w)
let n = du(strArray[ci++])
lines[n >> 3] = n & 0b111
}
updates.push({ topic: 'double-lines', lines: lines })
} else if (topic === TOPIC_TITLE) {
updates.push({ topic: 'title', title: collectOneTerminatedString() })
} else if (topic === TOPIC_BUTTONS) {
const count = du(strArray[ci++])
let labels = []
let colors = []
for (let j = 0; j < count; j++) {
colors.push(collectColor())
labels.push(collectOneTerminatedString())
}
updates.push({
topic: 'buttons-update',
labels,
colors
})
} else if (topic === TOPIC_BACKDROP) {
updates.push({ topic: 'backdrop', image: collectOneTerminatedString() })
} else if (topic === TOPIC_BELL) {
updates.push({ topic: 'bell' })
} else if (topic === TOPIC_INTERNAL) {
// debug info
const flags = du(strArray[ci++])
const cursorAttrs = du(strArray[ci++])
const regionStart = du(strArray[ci++])
const regionEnd = du(strArray[ci++])
const charsetGx = du(strArray[ci++])
const charsetG0 = strArray[ci++]
const charsetG1 = strArray[ci++]
let cursorFg = collectColor()
let cursorBg = collectColor()
const freeHeap = du(strArray[ci++])
const clientCount = du(strArray[ci++])
updates.push({
topic: 'internal',
flags,
cursorAttrs,
regionStart,
regionEnd,
charsetGx,
charsetG0,
charsetG1,
cursorFg,
cursorBg,
freeHeap,
clientCount
})
} else if (topic === TOPIC_CONTENT) {
// set screen content
const frameY = du(strArray[ci++])
const frameX = du(strArray[ci++])
const frameHeight = du(strArray[ci++])
const frameWidth = du(strArray[ci++])
// content
let fg = 7
let bg = 0
let attrs = 0
let cell = 0 // cell index
let lastChar = ' '
let frameLength = frameWidth * frameHeight
const MASK_LINE_ATTR = ATTR_UNDERLINE | ATTR_OVERLINE | ATTR_STRIKE
const MASK_BLINK = ATTR_BLINK
const cells = []
let pushCell = () => {
let hasFG = attrs & ATTR_FG
let hasBG = attrs & ATTR_BG
let cellFG = fg
let cellBG = bg
let cellAttrs = attrs
// use 0,0 if no fg/bg. this is to match back-end implementation
// and allow leaving out fg/bg setting for cells with none
if (!hasFG) cellFG = 0
if (!hasBG) cellBG = 0
// Remove blink attribute if it wouldn't have any effect
if ((cellAttrs & MASK_BLINK) &&
((lastChar === ' ' && ((cellAttrs & MASK_LINE_ATTR) === 0)) || // no line styles
(fg === bg && hasFG && hasBG) // invisible text
)
) {
cellAttrs ^= MASK_BLINK
}
// 8 dark system colors turn bright when bold
if ((cellAttrs & ATTR_BOLD) && !(cellAttrs & ATTR_FAINT) && hasFG && cellFG < 8) {
cellFG += 8
}
cells.push([lastChar, cellFG, cellBG, cellAttrs])
}
while (ci < strArray.length && cell < frameLength) {
let character = strArray[ci++]
let charCode = character.codePointAt(0)
let data, count
switch (charCode) {
case SEQ_REPEAT:
count = du(strArray[ci++])
for (let j = 0; j < count; j++) {
pushCell()
if (++cell > frameLength) break
}
break
case SEQ_SKIP:
cell += du(strArray[ci++])
break
case SEQ_SET_COLORS:
data = du(strArray[ci++])
fg = data & 0xFF
bg = (data >> 8) & 0xFF
break
case SEQ_SET_ATTRS:
data = du(strArray[ci++])
attrs = data & 0xFFFF
break
case SEQ_SET_ATTR_0:
attrs = 0
break
case SEQ_SET_FG:
data = du(strArray[ci++])
if (data & 0x10000) {
data &= 0xFFF
data |= (du(strArray[ci++]) & 0xFFF) << 12
data += 256
}
fg = data
break
case SEQ_SET_BG:
data = du(strArray[ci++])
if (data & 0x10000) {
data &= 0xFFF
data |= (du(strArray[ci++]) & 0xFFF) << 12
data += 256
}
bg = data
break
default:
if (charCode < 32) character = '\ufffd'
lastChar = character
pushCell()
cell++
}
}
updates.push({
topic: 'content',
frameX,
frameY,
frameWidth,
frameHeight,
cells
})
}
if (topics & 0x3B && !this.contentLoaded) {
updates.push({ topic: 'full-load-complete' })
this.contentLoaded = true
}
}
return updates
}
/**
* Parses a message from the server
* @param {string} message - the message
*/
parse (message) {
const content = message.substr(1)
const updates = []
// This is a good place for debugging the message
// console.log(message)
switch (message[0]) {
case 'U':
updates.push(...this.parseUpdate(content))
break
case 'G':
return [{
topic: 'notification',
content
}]
default:
console.warn(`Bad data message type; ignoring.\n${JSON.stringify(message)}`)
}
return updates
}
}

@ -0,0 +1,936 @@
const EventEmitter = require('events')
const {
themes,
getColor
} = require('./themes')
const {
ATTR_FG,
ATTR_BG,
ATTR_BOLD,
ATTR_UNDERLINE,
ATTR_INVERSE,
ATTR_BLINK,
ATTR_ITALIC,
ATTR_STRIKE,
ATTR_OVERLINE,
ATTR_FAINT,
ATTR_FRAKTUR
} = require('./screen_attr_bits')
// Some non-bold Fraktur symbols are outside the contiguous block
const frakturExceptions = {
'C': '\u212d',
'H': '\u210c',
'I': '\u2111',
'R': '\u211c',
'Z': '\u2128'
}
/**
* A terminal screen renderer, using canvas 2D
*/
module.exports = class CanvasRenderer extends EventEmitter {
constructor (canvas) {
super()
this.canvas = canvas
this.ctx = this.canvas.getContext('2d')
this._palette = null // colors 0-15
this.defaultBG = 0
this.defaultFG = 7
this.debug = false
this._debug = null
this.graphics = 0
this.statusFont = "-apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif"
// screen data, considered immutable
this.width = 0
this.height = 0
this.padding = 0
this.charSize = { width: 0, height: 0 }
this.cellSize = { width: 0, height: 0 }
this.fonts = ['', '', '', ''] // normal, bold, italic, bold-italic
this.screen = []
this.screenFG = []
this.screenBG = []
this.screenAttrs = []
this.screenSelection = []
this.screenLines = []
this.cursor = {}
this.reverseVideo = false
this.hasBlinkingCells = false
this.statusScreen = null
this.resetDrawn()
this.blinkStyleOn = false
this.blinkInterval = null
this.cursorBlinkOn = false
this.cursorBlinkInterval = null
// start blink timers
this.resetBlink()
this.resetCursorBlink()
}
render (reason, data) {
if ('hasBlinkingCells' in data && data.hasBlinkingCells !== this.hasBlinkingCells) {
if (data.hasBlinkingCells) this.resetBlink()
else clearInterval(this.blinkInterval)
}
Object.assign(this, data)
this.scheduleDraw(reason)
}
resetDrawn () {
// used to determine if a cell should be redrawn; storing the current state
// as it is on screen
if (this.debug) console.log('Resetting drawn screen')
this.drawnScreen = []
this.drawnScreenFG = []
this.drawnScreenBG = []
this.drawnScreenAttrs = []
this.drawnScreenLines = []
this.drawnCursor = [-1, -1, '', false]
}
/**
* The color palette. Should define 16 colors in an array.
* @type {string[]}
*/
get palette () {
return this._palette || themes[0]
}
/** @type {string[]} */
set palette (palette) {
if (this._palette !== palette) {
this._palette = palette
this.resetDrawn()
this.emit('palette-update', palette)
this.scheduleDraw('palette')
}
}
getCharWidthFor (font) {
this.ctx.font = font
return Math.floor(this.ctx.measureText(' ').width)
}
loadTheme (i) {
if (i in themes) this.palette = themes[i]
}
setDefaultColors (fg, bg) {
if (fg !== this.defaultFG || bg !== this.defaultBG) {
this.resetDrawn()
this.defaultFG = fg
this.defaultBG = bg
this.scheduleDraw('default-colors')
// full bg with default color (goes behind the image)
this.canvas.style.backgroundColor = this.getColor(bg)
}
}
/**
* Schedule a draw in the next millisecond
* @param {string} why - the reason why the draw occured (for debugging)
* @param {number} [aggregateTime] - time to wait for more scheduleDraw calls
* to occur. 1 ms by default.
*/
scheduleDraw (why, aggregateTime = 1) {
clearTimeout(this._scheduledDraw)
this._scheduledDraw = setTimeout(() => this.draw(why), aggregateTime)
}
/**
* Returns the specified color. If `i` is in the palette, it will return the
* palette color. If `i` is between 16 and 255, it will return the 256color
* value. If `i` is larger than 255, it will return an RGB color value. If `i`
* is -1 (foreground) or -2 (background), it will return the selection colors.
* @param {number} i - the color
* @returns {string} the CSS color
*/
getColor (i) {
return getColor(i, this.palette)
}
/**
* Resets the cursor blink to on and restarts the timer
*/
resetCursorBlink () {
this.cursorBlinkOn = true
clearInterval(this.cursorBlinkInterval)
this.cursorBlinkInterval = setInterval(() => {
this.cursorBlinkOn = this.cursor.blinking ? !this.cursorBlinkOn : true
if (this.cursor.blinking) this.scheduleDraw('cursor-blink')
}, 500)
}
/**
* Resets the blink style to on and restarts the timer
*/
resetBlink () {
this.blinkStyleOn = true
clearInterval(this.blinkInterval)
let intervals = 0
this.blinkInterval = setInterval(() => {
if (this.blinkingCellCount <= 0) return
intervals++
if (intervals >= 4 && this.blinkStyleOn) {
this.blinkStyleOn = false
intervals = 0
this.scheduleDraw('blink-style')
} else if (intervals >= 1 && !this.blinkStyleOn) {
this.blinkStyleOn = true
intervals = 0
this.scheduleDraw('blink-style')
}
}, 200)
}
/**
* Draws a cell's background with the given parameters.
* @param {Object} options
* @param {number} options.x - x in cells
* @param {number} options.y - y in cells
* @param {number} options.cellWidth - cell width in pixels
* @param {number} options.cellHeight - cell height in pixels
* @param {number} options.bg - the background color
* @param {number} options.isDefaultBG - if true, will draw image background if available
*/
drawBackground ({ x, y, cellWidth, cellHeight, bg, isDefaultBG }) {
const { ctx, width, height, padding } = this
// is a double-width/double-height line
if (this.screenLines[y] & 0b001) cellWidth *= 2
ctx.fillStyle = this.getColor(bg)
let screenX = x * cellWidth + padding
let screenY = y * cellHeight + padding
let isBorderCell = x === 0 || y === 0 || x === width - 1 || y === height - 1
let fillX, fillY, fillWidth, fillHeight
if (isBorderCell) {
let left = screenX
let top = screenY
let right = screenX + cellWidth
let bottom = screenY + cellHeight
if (x === 0) left -= padding
else if (x === width - 1) right += padding
if (y === 0) top -= padding
else if (y === height - 1) bottom += padding
fillX = left
fillY = top
fillWidth = right - left
fillHeight = bottom - top
} else {
fillX = screenX
fillY = screenY
fillWidth = cellWidth
fillHeight = cellHeight
}
ctx.clearRect(fillX, fillY, fillWidth, fillHeight)
if (!isDefaultBG || bg < 0 || !this.backgroundImage) {
ctx.fillRect(fillX, fillY, fillWidth, fillHeight)
}
}
/**
* Draws a cell's character with the given parameters. Won't do anything if
* text is an empty string.
* @param {Object} options
* @param {number} options.x - x in cells
* @param {number} options.y - y in cells
* @param {Object} options.charSize - the character size, an object with
* `width` and `height` in pixels
* @param {number} options.cellWidth - cell width in pixels
* @param {number} options.cellHeight - cell height in pixels
* @param {string} options.text - the cell content
* @param {number} options.fg - the foreground color
* @param {number} options.attrs - the cell's attributes
*/
drawCharacter ({ x, y, charSize, cellWidth, cellHeight, text, fg, attrs }) {
if (!text) return
const { ctx, padding } = this
let underline = false
let strike = false
let overline = false
if (attrs & ATTR_FAINT) ctx.globalAlpha = 0.5
if (attrs & ATTR_UNDERLINE) underline = true
if (attrs & ATTR_FRAKTUR) text = CanvasRenderer.alphaToFraktur(text)
if (attrs & ATTR_STRIKE) strike = true
if (attrs & ATTR_OVERLINE) overline = true
ctx.fillStyle = this.getColor(fg)
let screenX = x * cellWidth + padding
let screenY = y * cellHeight + padding
const dblWidth = this.screenLines[y] & 0b001
const dblHeightTop = this.screenLines[y] & 0b010
const dblHeightBot = this.screenLines[y] & 0b100
if (this.screenLines[y]) {
// is a double-width/double-height line
if (dblWidth) cellWidth *= 2
ctx.save()
ctx.translate(padding, screenY + 0.5 * cellHeight)
if (dblWidth) ctx.scale(2, 1)
if (dblHeightTop) {
// top half
ctx.scale(1, 2)
ctx.translate(0, cellHeight / 4)
} else if (dblHeightBot) {
// bottom half
ctx.scale(1, 2)
ctx.translate(0, -cellHeight / 4)
}
ctx.translate(-padding, -screenY - 0.5 * cellHeight)
if (dblWidth) ctx.translate(-cellWidth / 4, 0)
if (dblHeightBot || dblHeightTop) {
// characters overflow -- needs clipping
// TODO: clipping is really expensive
ctx.beginPath()
if (dblHeightTop) ctx.rect(screenX, screenY, cellWidth, cellHeight / 2)
else ctx.rect(screenX, screenY + cellHeight / 2, cellWidth, cellHeight / 2)
ctx.clip()
}
}
let codePoint = text.codePointAt(0)
if (codePoint >= 0x2580 && codePoint <= 0x259F) {
// block elements
ctx.beginPath()
const left = screenX
const top = screenY
const cw = cellWidth
const ch = cellHeight
const c2w = cellWidth / 2
const c2h = cellHeight / 2
// http://www.fileformat.info/info/unicode/block/block_elements/utf8test.htm
// 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F
// 0x2580 ▀ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▉ ▊ ▋ ▌ ▍ ▎ ▏
// 0x2590 ▐ ░ ▒ ▓ ▔ ▕ ▖ ▗ ▘ ▙ ▚ ▛ ▜ ▝ ▞ ▟
if (codePoint === 0x2580) {
// upper half block >▀<
ctx.rect(left, top, cw, c2h)
} else if (codePoint <= 0x2588) {
// lower n eighth block (increasing) >▁< to >█<
let offset = (1 - (codePoint - 0x2580) / 8) * ch
ctx.rect(left, top + offset, cw, ch - offset)
} else if (codePoint <= 0x258F) {
// left n eighth block (decreasing) >▉< to >▏<
let offset = (codePoint - 0x2588) / 8 * cw
ctx.rect(left, top, cw - offset, ch)
} else if (codePoint === 0x2590) {
// right half block >▐<
ctx.rect(left + c2w, top, c2w, ch)
} else if (codePoint <= 0x2593) {
// shading >░< >▒< >▓<
// dot spacing by dividing cell size by a constant. This could be
// reworked to always return a whole number, but that would require
// prime factorization, and doing that without a loop would let you
// take over the world, which is not within the scope of this project.
let dotSpacingX, dotSpacingY, dotSize
if (codePoint === 0x2591) {
dotSpacingX = cw / 4
dotSpacingY = ch / 10
dotSize = 1
} else if (codePoint === 0x2592) {
dotSpacingX = cw / 6
dotSpacingY = cw / 10
dotSize = 1
} else if (codePoint === 0x2593) {
dotSpacingX = cw / 4
dotSpacingY = cw / 7
dotSize = 2
}
let alignRight = false
for (let dy = 0; dy < ch; dy += dotSpacingY) {
for (let dx = 0; dx < cw; dx += dotSpacingX) {
// prevent overflow
let dotSizeY = Math.min(dotSize, ch - dy)
ctx.rect(left + (alignRight ? cw - dx - dotSize : dx), top + dy, dotSize, dotSizeY)
}
alignRight = !alignRight
}
} else if (codePoint === 0x2594) {
// upper one eighth block >▔<
ctx.rect(left, top, cw, ch / 8)
} else if (codePoint === 0x2595) {
// right one eighth block >▕<
ctx.rect(left + (7 / 8) * cw, top, cw / 8, ch)
} else if (codePoint === 0x2596) {
// left bottom quadrant >▖<
ctx.rect(left, top + c2h, c2w, c2h)
} else if (codePoint === 0x2597) {
// right bottom quadrant >▗<
ctx.rect(left + c2w, top + c2h, c2w, c2h)
} else if (codePoint === 0x2598) {
// left top quadrant >▘<
ctx.rect(left, top, c2w, c2h)
} else if (codePoint === 0x2599) {
// left chair >▙<
ctx.rect(left, top, c2w, ch)
ctx.rect(left + c2w, top + c2h, c2w, c2h)
} else if (codePoint === 0x259A) {
// quadrants lt rb >▚<
ctx.rect(left, top, c2w, c2h)
ctx.rect(left + c2w, top + c2h, c2w, c2h)
} else if (codePoint === 0x259B) {
// left chair upside down >▛<
ctx.rect(left, top, c2w, ch)
ctx.rect(left + c2w, top, c2w, c2h)
} else if (codePoint === 0x259C) {
// right chair upside down >▜<
ctx.rect(left, top, cw, c2h)
ctx.rect(left + c2w, top + c2h, c2w, c2h)
} else if (codePoint === 0x259D) {
// right top quadrant >▝<
ctx.rect(left + c2w, top, c2w, c2h)
} else if (codePoint === 0x259E) {
// quadrants lb rt >▞<
ctx.rect(left, top + c2h, c2w, c2h)
ctx.rect(left + c2w, top, c2w, c2h)
} else if (codePoint === 0x259F) {
// right chair upside down >▟<
ctx.rect(left, top + c2h, c2w, c2h)
ctx.rect(left + c2w, top, c2w, ch)
}
ctx.fill()
} else if (codePoint >= 0xE0B0 && codePoint <= 0xE0B3) {
// powerline symbols, except branch, line, and lock. Basically, just the triangles
ctx.beginPath()
if (codePoint === 0xE0B0 || codePoint === 0xE0B1) {
// right-pointing triangle
ctx.moveTo(screenX, screenY)
ctx.lineTo(screenX + cellWidth, screenY + cellHeight / 2)
ctx.lineTo(screenX, screenY + cellHeight)
} else if (codePoint === 0xE0B2 || codePoint === 0xE0B3) {
// left-pointing triangle
ctx.moveTo(screenX + cellWidth, screenY)
ctx.lineTo(screenX, screenY + cellHeight / 2)
ctx.lineTo(screenX + cellWidth, screenY + cellHeight)
}
if (codePoint % 2 === 0) {
// triangle
ctx.fill()
} else {
// chevron
ctx.strokeStyle = ctx.fillStyle
ctx.stroke()
}
} else {
// Draw other characters using the text renderer
ctx.fillText(text, screenX + 0.5 * cellWidth, screenY + 0.5 * cellHeight)
}
// -- line drawing - a reference for a possible future rect/line implementation ---
// http://www.fileformat.info/info/unicode/block/box_drawing/utf8test.htm
// 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F
// 0x2500 ─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏
// 0x2510 ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟
// 0x2520 ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯
// 0x2530 ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿
// 0x2540 ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏
// 0x2550 ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟
// 0x2560 ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯
// 0x2570 ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿
if (underline || strike || overline) {
ctx.strokeStyle = this.getColor(fg)
ctx.lineWidth = 1
ctx.lineCap = 'round'
ctx.beginPath()
if (underline) {
let lineY = Math.round(screenY + charSize.height) + 0.5
ctx.moveTo(screenX, lineY)
ctx.lineTo(screenX + cellWidth, lineY)
}
if (strike) {
let lineY = Math.round(screenY + 0.5 * cellHeight) + 0.5
ctx.moveTo(screenX, lineY)
ctx.lineTo(screenX + cellWidth, lineY)
}
if (overline) {
let lineY = Math.round(screenY) + 0.5
ctx.moveTo(screenX, lineY)
ctx.lineTo(screenX + cellWidth, lineY)
}
ctx.stroke()
}
if (this.screenLines[y]) ctx.restore()
ctx.globalAlpha = 1
}
/**
* Returns all adjacent cell indices given a radius.
* @param {number} cell - the center cell index
* @param {number} [radius] - the radius. 1 by default
* @returns {number[]} an array of cell indices
*/
getAdjacentCells (cell, radius = 1) {
const { width, height } = this
const screenLength = width * height
let cells = []
for (let x = -radius; x <= radius; x++) {
for (let y = -radius; y <= radius; y++) {
if (x === 0 && y === 0) continue
cells.push(cell + x + y * width)
}
}
return cells.filter(cell => cell >= 0 && cell < screenLength)
}
/**
* Updates the screen.
* @param {string} why - the draw reason (for debugging)
*/
draw (why) {
const ctx = this.ctx
const {
width,
height,
devicePixelRatio,
statusScreen
} = this
if (statusScreen) {
// draw status screen instead
this.drawStatus(statusScreen)
this.startDrawLoop()
return
} else this.stopDrawLoop()
const charSize = this.charSize
const { width: cellWidth, height: cellHeight } = this.cellSize
const screenLength = width * height
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0)
if (this.debug && this._debug) this._debug.drawStart(why)
ctx.font = this.fonts[0]
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
// bits in the attr value that affect the font
const FONT_MASK = ATTR_BOLD | ATTR_ITALIC
// Map of (attrs & FONT_MASK) -> Array of cell indices
let fontGroups = new Map()
// Map of (cell index) -> boolean, whether or not a cell has updated
let updateMap = new Map()
for (let cell = 0; cell < screenLength; cell++) {
let x = cell % width
let y = Math.floor(cell / width)
let isCursor = this.cursorBlinkOn &&
this.cursor.x === x &&
this.cursor.y === y &&
this.cursor.visible
let wasCursor = x === this.drawnCursor[0] && y === this.drawnCursor[1]
let text = this.screen[cell]
let fg = this.screenFG[cell] | 0
let bg = this.screenBG[cell] | 0
let attrs = this.screenAttrs[cell] | 0
let inSelection = this.screenSelection[cell]
let isDefaultBG = false
if (!(attrs & ATTR_FG)) fg = this.defaultFG
if (!(attrs & ATTR_BG)) {
bg = this.defaultBG
isDefaultBG = true
}
if (attrs & ATTR_INVERSE) [fg, bg] = [bg, fg] // swap - reversed character colors
if (this.reverseVideo) [fg, bg] = [bg, fg] // swap - reversed all screen
if (attrs & ATTR_BLINK && !this.blinkStyleOn) {
// blinking is enabled and blink style is off
// set text to nothing so drawCharacter only draws decoration
text = ' '
}
if (inSelection) {
fg = -1
bg = -2
}
let didUpdate = text !== this.drawnScreen[cell] || // text updated
fg !== this.drawnScreenFG[cell] || // foreground updated, and this cell has text
bg !== this.drawnScreenBG[cell] || // background updated
attrs !== this.drawnScreenAttrs[cell] || // attributes updated
this.screenLines[y] !== this.drawnScreenLines[y] || // line updated
// TODO: fix artifacts or keep this hack:
isCursor || wasCursor || // cursor blink/position updated
(isCursor && this.cursor.style !== this.drawnCursor[2]) || // cursor style updated
(isCursor && this.cursor.hanging !== this.drawnCursor[3]) // cursor hanging updated
let font = attrs & FONT_MASK
if (!fontGroups.has(font)) fontGroups.set(font, [])
fontGroups.get(font).push({ cell, x, y, text, fg, bg, attrs, isCursor, inSelection, isDefaultBG })
updateMap.set(cell, didUpdate)
}
// set drawn screen lines
this.drawnScreenLines = this.screenLines.slice()
let debugFilledUpdates = []
if (this.graphics >= 1) {
// fancy graphics gets really slow when there's a lot of masks
// so here's an algorithm that fills in holes in the update map
for (let cell of updateMap.keys()) {
if (updateMap.get(cell)) continue
let previous = updateMap.get(cell - 1) || false
let next = updateMap.get(cell + 1) || false
if (previous && next) {
// set cell to true of horizontally adjacent updated
updateMap.set(cell, true)
if (this.debug && this._debug) debugFilledUpdates.push(cell)
}
}
}
// Map of (cell index) -> boolean, whether or not a cell should be redrawn
const redrawMap = new Map()
const maskedCells = new Map()
let isTextWide = text =>
text !== ' ' && ctx.measureText(text).width >= (cellWidth + 0.05)
// decide for each cell if it should be redrawn
for (let cell of updateMap.keys()) {
let shouldUpdate = updateMap.get(cell) || redrawMap.get(cell) || false
// TODO: fonts (necessary?)
let text = this.screen[cell]
let isWideCell = isTextWide(text)
let checkRadius = isWideCell ? 2 : 1
if (!shouldUpdate) {
// check adjacent cells
let adjacentDidUpdate = false
for (let adjacentCell of this.getAdjacentCells(cell, checkRadius)) {
// update this cell if:
// - the adjacent cell updated (For now, this'll always be true because characters can be slightly larger than they say they are)
// - the adjacent cell updated and this cell or the adjacent cell is wide
// - this or the adjacent cell is not double-sized
if (updateMap.get(adjacentCell) &&
(this.graphics < 2 || isWideCell || isTextWide(this.screen[adjacentCell])) &&
(!this.screenLines[Math.floor(cell / this.width)] && !this.screenLines[Math.floor(adjacentCell / this.width)])) {
adjacentDidUpdate = true
if (this.getAdjacentCells(cell, 1).includes(adjacentCell)) {
// this is within a radius of 1, therefore this cell should be included in the mask as well
maskedCells.set(cell, true)
}
break
}
}
if (adjacentDidUpdate) shouldUpdate = true
}
if (updateMap.get(cell)) {
// this was updated, it should definitely be included in the mask
maskedCells.set(cell, true)
}
redrawMap.set(cell, shouldUpdate)
}
// mask to masked regions only
if (this.graphics >= 1) {
// TODO: include padding in border cells
const padding = this.padding
let regions = []
for (let y = 0; y < height; y++) {
let regionStart = null
for (let x = 0; x < width; x++) {
let cell = y * width + x
let masked = maskedCells.get(cell)
if (masked && regionStart === null) regionStart = x
if (!masked && regionStart !== null) {
regions.push([regionStart, y, x, y + 1])
regionStart = null
}
}
if (regionStart !== null) {
regions.push([regionStart, y, width, y + 1])
}
}
// join regions if possible (O(n^2-1), sorry)
let i = 0
while (i < regions.length) {
let region = regions[i]
let j = 0
while (j < regions.length) {
let other = regions[j]
if (other === region) {
j++
continue
}
if (other[0] === region[0] && other[2] === region[2] && other[3] === region[1]) {
region[1] = other[1]
regions.splice(j, 1)
if (i > j) i--
j--
}
j++
}
i++
}
ctx.save()
ctx.beginPath()
for (let region of regions) {
let [regionStart, y, endX, endY] = region
let rectX = padding + regionStart * cellWidth
let rectY = padding + y * cellHeight
let rectWidth = (endX - regionStart) * cellWidth
let rectHeight = (endY - y) * cellHeight
// compensate for padding
if (regionStart === 0) {
rectX -= padding
rectWidth += padding
}
if (y === 0) {
rectY -= padding
rectHeight += padding
}
if (endX === width - 1) rectWidth += padding
if (y === height - 1) rectHeight += padding
ctx.rect(rectX, rectY, rectWidth, rectHeight)
}
ctx.clip()
}
// pass 1: backgrounds
for (let font of fontGroups.keys()) {
for (let data of fontGroups.get(font)) {
let { cell, x, y, text, bg, isDefaultBG } = data
if (redrawMap.get(cell)) {
this.drawBackground({ x, y, cellWidth, cellHeight, bg, isDefaultBG })
if (this.debug) {
// set cell flags
let flags = (+redrawMap.get(cell))
flags |= (+updateMap.get(cell)) << 1
flags |= (+maskedCells.get(cell)) << 2
flags |= (+isTextWide(text)) << 3
flags |= (+debugFilledUpdates.includes(cell)) << 4
this._debug.setCell(cell, flags)
}
}
}
}
// reset drawn cursor
this.drawnCursor = [-1, -1, '', false]
// pass 2: characters
for (let font of fontGroups.keys()) {
// set font once because in Firefox, this is a really slow action for some
// reason
let fontIndex = 0
if (font & ATTR_BOLD) fontIndex |= 1
if (font & ATTR_ITALIC) fontIndex |= 2
ctx.font = this.fonts[fontIndex]
for (let data of fontGroups.get(font)) {
let { cell, x, y, text, fg, bg, attrs, isCursor, inSelection } = data
if (redrawMap.get(cell)) {
this.drawCharacter({
x, y, charSize, cellWidth, cellHeight, text, fg, attrs
})
this.drawnScreen[cell] = text
this.drawnScreenFG[cell] = fg
this.drawnScreenBG[cell] = bg
this.drawnScreenAttrs[cell] = attrs
if (isCursor) this.drawnCursor = [x, y, this.cursor.style, this.cursor.hanging]
// draw cursor
if (isCursor && !inSelection) {
ctx.save()
ctx.beginPath()
let cursorX = x
let cursorY = y
let cursorWidth = cellWidth // JS doesn't allow same-name assignment
if (this.cursor.hanging) {
// draw hanging cursor in the margin
cursorX += 1
}
// double-width lines
if (this.screenLines[cursorY] & 0b001) cursorWidth *= 2
let screenX = cursorX * cursorWidth + this.padding
let screenY = cursorY * cellHeight + this.padding
if (this.cursor.style === 'block') {
// block
ctx.rect(screenX, screenY, cursorWidth, cellHeight)
} else if (this.cursor.style === 'bar') {
// vertical bar
let barWidth = 2
ctx.rect(screenX, screenY, barWidth, cellHeight)
} else if (this.cursor.style === 'line') {
// underline
let lineHeight = 2
ctx.rect(screenX, screenY + charSize.height, cursorWidth, lineHeight)
}
ctx.clip()
// swap foreground/background
;[fg, bg] = [bg, fg]
// HACK: ensure cursor is visible
if (fg === bg) bg = fg === 0 ? 7 : 0
this.drawBackground({ x: cursorX, y: cursorY, cellWidth, cellHeight, bg })
this.drawCharacter({
x: cursorX, y: cursorY, charSize, cellWidth, cellHeight, text, fg, attrs
})
ctx.restore()
}
}
}
}
if (this.graphics >= 1) ctx.restore()
if (this.debug && this._debug) this._debug.drawEnd()
this.emit('draw', why)
}
drawStatus (statusScreen) {
const { ctx, width, height, devicePixelRatio } = this
// reset drawnScreen to force redraw when statusScreen is disabled
this.drawnScreen = []
const cellSize = this.cellSize
const screenWidth = width * cellSize.width + 2 * this.padding
const screenHeight = height * cellSize.height + 2 * this.padding
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0)
ctx.fillStyle = this.getColor(this.defaultBG)
ctx.fillRect(0, 0, screenWidth, screenHeight)
ctx.font = `24px ${this.statusFont}`
ctx.fillStyle = this.getColor(this.defaultFG)
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(statusScreen.title || '', screenWidth / 2, screenHeight / 2 - 50)
if (statusScreen.loading) {
// show loading spinner
ctx.save()
ctx.translate(screenWidth / 2, screenHeight / 2 + 20)
ctx.strokeStyle = this.getColor(this.defaultFG)
ctx.lineWidth = 5
ctx.lineCap = 'round'
let t = Date.now() / 1000
for (let i = 0; i < 12; i++) {
ctx.rotate(Math.PI / 6)
let offset = ((t * 12) - i) % 12
ctx.globalAlpha = Math.max(0.2, 1 - offset / 3)
ctx.beginPath()
ctx.moveTo(0, 15)
ctx.lineTo(0, 30)
ctx.stroke()
}
ctx.restore()
}
}
startDrawLoop () {
if (this._drawTimerThread) return
let threadID = Math.random().toString(36)
this._drawTimerThread = threadID
this.drawTimerLoop(threadID)
}
stopDrawLoop () {
this._drawTimerThread = null
}
drawTimerLoop (threadID) {
if (!threadID || threadID !== this._drawTimerThread) return
window.requestAnimationFrame(() => this.drawTimerLoop(threadID))
this.draw('draw-loop')
}
/**
* Converts an alphabetic character to its fraktur variant.
* @param {string} character - the character
* @returns {string} the converted character
*/
static alphaToFraktur (character) {
if (character >= 'a' && character <= 'z') {
character = String.fromCodePoint(0x1d51e - 0x61 + character.charCodeAt(0))
} else if (character >= 'A' && character <= 'Z') {
character = frakturExceptions[character] || String.fromCodePoint(0x1d504 - 0x41 + character.charCodeAt(0))
}
return character
}
}

@ -0,0 +1,167 @@
const { qs } = require('../utils')
module.exports = function (screen, input) {
const keyInput = qs('#softkb-input')
if (!keyInput) return // abort, we're not on the terminal page
const shortcutBar = document.createElement('div')
shortcutBar.id = 'keyboard-shortcut-bar'
if (navigator.userAgent.match(/iPad|iPhone|iPod/)) {
qs('#screen').appendChild(shortcutBar)
}
let keyboardOpen = false
// moves the input to where the cursor is on the canvas.
// this is because most browsers will always scroll to wherever the focused
// input is
let updateInputPosition = function () {
if (!keyboardOpen) return
let [x, y] = screen.layout.gridToScreen(screen.cursor.x, screen.cursor.y, true)
keyInput.style.transform = `translate(${x}px, ${y}px)`
}
keyInput.addEventListener('focus', () => {
keyboardOpen = true
updateInputPosition()
shortcutBar.classList.add('open')
})
keyInput.addEventListener('blur', () => {
keyboardOpen = false
shortcutBar.classList.remove('open')
})
screen.on('cursor-moved', updateInputPosition)
qs('#term-kb-open').addEventListener('click', e => {
e.preventDefault()
keyInput.focus()
})
// Chrome for Android doesn't send proper keydown/keypress events with
// real key values instead of 229 “Unidentified,” so here's a workaround
// that deals with the input composition events.
let lastCompositionString = ''
// sends the difference between the last and the new composition string
let sendInputDelta = function (newValue) {
if (newValue === null) newValue = '' // this sometimes happens, why?
let resend = false
if (newValue.length > lastCompositionString.length) {
if (newValue.startsWith(lastCompositionString)) {
// characters have been added at the end
input.sendString(newValue.substr(lastCompositionString.length))
} else resend = true
} else if (newValue.length < lastCompositionString.length) {
if (lastCompositionString.startsWith(newValue)) {
// characters have been removed at the end
input.sendString('\b'.repeat(lastCompositionString.length -
newValue.length))
} else resend = true
} else if (newValue !== lastCompositionString) resend = true
if (resend) {
// the entire string changed; resend everything
input.sendString('\b'.repeat(lastCompositionString.length) +
newValue)
}
lastCompositionString = newValue
}
keyInput.addEventListener('keydown', e => {
if (e.key === 'Unidentified') return
keyInput.value = ''
e.stopPropagation()
input.handleKeyDown(e)
})
keyInput.addEventListener('keypress', e => {
// prevent key duplication on iOS (because Safari *does* send proper events)
e.stopPropagation()
})
keyInput.addEventListener('input', e => {
e.stopPropagation()
if (e.isComposing && 'data' in e) {
sendInputDelta(e.data)
} else if (e.isComposing) {
// Firefox Mobile doesn't support InputEvent#data, so here's a hack
// that just takes the input value and uses that
sendInputDelta(keyInput.value)
} else {
if (e.inputType === 'insertCompositionText') input.sendString(e.data)
else if (e.inputType === 'deleteContentBackward') {
lastCompositionString = ''
sendInputDelta('')
} else if (e.inputType === 'insertText') {
input.sendString(e.data)
}
}
})
keyInput.addEventListener('compositionstart', e => {
lastCompositionString = ''
})
keyInput.addEventListener('compositionend', e => {
lastCompositionString = ''
keyInput.value = ''
})
screen.on('open-soft-keyboard', () => keyInput.focus())
// shortcut bar
const shortcuts = {
Control: 'ctrl',
Esc: 0x1b,
Tab: 0x09,
'←': 0x25,
'↓': 0x28,
'↑': 0x26,
'→': 0x27
}
let touchMoved = false
for (const shortcut in shortcuts) {
const button = document.createElement('button')
button.classList.add('shortcut-button')
button.textContent = shortcut
shortcutBar.appendChild(button)
const key = shortcuts[shortcut]
if (typeof key === 'string') button.classList.add('modifier')
button.addEventListener('touchstart', e => {
touchMoved = false
if (typeof key === 'string') {
// modifier button
input.softModifiers[key] = true
button.classList.add('enabled')
// prevent default. This prevents scrolling, but also prevents the
// selection popup
e.preventDefault()
}
})
window.addEventListener('touchmove', e => {
touchMoved = true
})
button.addEventListener('touchend', e => {
e.preventDefault()
if (typeof key === 'number') {
if (touchMoved) return
let fakeEvent = { which: key, preventDefault: () => {} }
input.handleKeyDown(fakeEvent)
} else if (typeof key === 'string') {
button.classList.remove('enabled')
input.softModifiers[key] = false
}
})
}
}

@ -0,0 +1,151 @@
const $ = require('../lib/chibi')
const { rgb255ToHex } = require('../lib/color_utils')
const themes = exports.themes = [
[ // 0 - Tango - terminator
'#111213', '#CC0000', '#4E9A06', '#C4A000', '#3465A4', '#75507B', '#06989A', '#D3D7CF',
'#555753', '#EF2929', '#8AE234', '#FCE94F', '#729FCF', '#AD7FA8', '#34E2E2', '#EEEEEC'
],
[ // 1 - Linux (CGA) - terminator
'#000000', '#aa0000', '#00aa00', '#aa5500', '#0000aa', '#aa00aa', '#00aaaa', '#aaaaaa',
'#555555', '#ff5555', '#55ff55', '#ffff55', '#5555ff', '#ff55ff', '#55ffff', '#ffffff'
],
[ // 2 - xterm - terminator
'#000000', '#cd0000', '#00cd00', '#cdcd00', '#0000ee', '#cd00cd', '#00cdcd', '#e5e5e5',
'#7f7f7f', '#ff0000', '#00ff00', '#ffff00', '#5c5cff', '#ff00ff', '#00ffff', '#ffffff'
],
[ // 3 - rxvt - terminator
'#000000', '#cd0000', '#00cd00', '#cdcd00', '#0000cd', '#cd00cd', '#00cdcd', '#faebd7',
'#404040', '#ff0000', '#00ff00', '#ffff00', '#0000ff', '#ff00ff', '#00ffff', '#ffffff'
],
[ // 4 - Ambience - terminator
'#2e3436', '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', '#d3d7cf',
'#555753', '#ef2929', '#8ae234', '#fce94f', '#729fcf', '#ad7fa8', '#34e2e2', '#eeeeec'
],
[ // 5 - Solarized Dark - terminator
'#073642', '#dc322f', '#859900', '#b58900', '#268bd2', '#d33682', '#2aa198', '#eee8d5',
'#002b36', '#cb4b16', '#586e75', '#657b83', '#839496', '#6c71c4', '#93a1a1', '#fdf6e3'
],
[ // 6 - CGA NTSC - wikipedia
'#000000', '#69001A', '#117800', '#769100', '#1A00A6', '#8019AB', '#289E76', '#A4A4A4',
'#484848', '#C54E76', '#6DD441', '#D2ED46', '#765BFF', '#DC75FF', '#84FAD2', '#FFFFFF'
],
[ // 7 - ZX Spectrum - wikipedia
'#000000', '#aa0000', '#00aa00', '#aaaa00', '#0000aa', '#aa00aa', '#00aaaa', '#aaaaaa',
'#000000', '#ff0000', '#00FF00', '#ffff00', '#0000ff', '#ff00ff', '#00ffff', '#ffffff'
],
[ // 8 - Apple II - wikipedia
'#000000', '#722640', '#0E5940', '#808080', '#40337F', '#E434FE', '#1B9AFE', '#BFB3FF',
'#404C00', '#E46501', '#1BCB01', '#BFCC80', '#808080', '#F1A6BF', '#8DD9BF', '#ffffff'
],
[ // 9 - Commodore - wikipedia
'#000000', '#8D3E37', '#55A049', '#AAB95D', '#40318D', '#80348B', '#72C1C8', '#D59F74',
'#8B5429', '#B86962', '#94E089', '#FFFFB2', '#8071CC', '#AA5FB6', '#87D6DD', '#ffffff'
],
[ // 10 - Solarized Light - https://github.com/sgerrand/xfce4-terminal-colors-solarized
'#eee8d5', '#dc322f', '#859900', '#b58900', '#268bd2', '#d33682', '#2aa198', '#073642',
'#fdf6e3', '#cb4b16', '#93a1a1', '#839496', '#657b83', '#6c71c4', '#586e75', '#002b36'
],
[ // 11 - Solarized Dark High contrast - https://github.com/sgerrand/xfce4-terminal-colors-solarized
'#073642', '#dc322f', '#859900', '#b58900', '#268bd2', '#d33682', '#2aa198', '#fdf6e3',
'#002b36', '#cb4b16', '#657b83', '#839496', '#93a1a1', '#6c71c4', '#eee8d5', '#fdf6e3'
]
]
exports.fgbgThemes = [
['#AAAAAA', '#000000', 'Lnx', 'Linux'],
['#FFFFFF', '#000000', 'W+K', 'White on Black'],
['#00FF00', '#000000', 'Lim', 'Lime'],
['#E53C00', '#000000', 'Nix', 'Nixie'],
['#EFF0F1', '#31363B', 'Brz', 'Breeze'],
['#FFFFFF', '#300A24', 'Amb', 'Ambiance'],
['#839496', '#002B36', 'SoD', 'Solarized Dark'],
['#93a1a1', '#002b36', 'SoH', 'Solarized Dark (High Contrast)'],
['#657B83', '#FDF6E3', 'SoL', 'Solarized Light'],
['#000000', '#FFD75F', 'Wsp', 'Wasp'],
['#000000', '#FFFFDD', 'K+Y', 'Black on Yellow'],
['#000000', '#FFFFFF', 'K+W', 'Black on White']
]
let colorTable256 = null
exports.buildColorTable = function () {
if (colorTable256 !== null) return colorTable256
// 256color lookup table
// should not be used to look up 0-15
colorTable256 = new Array(16).fill('#000000')
// fill color table
// colors 16-231 are a 6x6x6 color cube
for (let red = 0; red < 6; red++) {
for (let green = 0; green < 6; green++) {
for (let blue = 0; blue < 6; blue++) {
let redValue = red * 40 + (red ? 55 : 0)
let greenValue = green * 40 + (green ? 55 : 0)
let blueValue = blue * 40 + (blue ? 55 : 0)
colorTable256.push(rgb255ToHex(redValue, greenValue, blueValue))
}
}
}
// colors 232-255 are a grayscale ramp, sans black and white
for (let gray = 0; gray < 24; gray++) {
let value = gray * 10 + 8
colorTable256.push(rgb255ToHex(value, value, value))
}
return colorTable256
}
exports.SELECTION_FG = '#333'
exports.SELECTION_BG = '#b2d7fe'
exports.themePreview = function (themeN) {
$('[data-fg]').forEach((elem) => {
let shade = elem.dataset.fg
if (/^\d+$/.test(shade)) shade = exports.toHex(shade, themeN)
elem.style.color = shade
})
$('[data-bg]').forEach((elem) => {
let shade = elem.dataset.bg
if (/^\d+$/.test(shade)) shade = exports.toHex(shade, themeN)
elem.style.backgroundColor = shade
})
}
exports.colorTable256 = null
exports.ensureColorTable256 = function () {
if (!exports.colorTable256) exports.colorTable256 = exports.buildColorTable()
}
exports.getColor = function (i, palette = []) {
// return palette color if it exists
if (i < 16 && i in palette) return palette[i]
// -1 for selection foreground, -2 for selection background
if (i === -1) return exports.SELECTION_FG
if (i === -2) return exports.SELECTION_BG
// 256 color
if (i > 15 && i < 256) {
exports.ensureColorTable256()
return exports.colorTable256[i]
}
// 24-bit color, encoded as (hex) + 256 (such that #000000 == 256)
if (i > 255) {
i -= 256
return '#' + `000000${i.toString(16)}`.substr(-6)
}
// return error color
return Math.floor(Date.now() / 1000) % 2 === 0 ? '#ff0ff' : '#00ff00'
}
exports.toHex = function (shade, themeN) {
if (/^\d+$/.test(shade)) {
shade = +shade
return exports.getColor(shade, themes[themeN])
}
return shade
}

@ -0,0 +1,182 @@
const $ = require('../lib/chibi')
const { qs } = require('../utils')
const modal = require('../modal')
/** File upload utility */
module.exports = function (conn, input, screen) {
let lines, // array of lines without newlines
line_i, // current line index
fuTout, // timeout handle for line sending
send_delay_ms, // delay between lines (ms)
nl_str, // newline string to use
curLine, // current line (when using fuOil)
inline_pos // Offset in line (for long lines)
// lines longer than this are split to chunks
// sending a super-ling string through the socket is not a good idea
const MAX_LINE_LEN = 128
function openUploadDialog () {
updateStatus('Ready...')
modal.show('#fu_modal', onDialogClose)
$('#fu_form').toggleClass('busy', false)
input.blockKeys(true)
}
function onDialogClose () {
console.log('Upload modal closed.')
clearTimeout(fuTout)
line_i = 0
input.blockKeys(false)
}
function updateStatus (msg) {
qs('#fu_prog').textContent = msg
}
function startUpload () {
let v = qs('#fu_text').value
if (!v.length) {
fuClose()
return
}
lines = v.split('\n')
line_i = 0
inline_pos = 0 // offset in line
send_delay_ms = +qs('#fu_delay').value
// sanitize - 0 causes overflows
if (send_delay_ms < 0) {
send_delay_ms = 0
qs('#fu_delay').value = send_delay_ms
}
nl_str = {
'CR': '\r',
'LF': '\n',
'CRLF': '\r\n'
}[qs('#fu_crlf').value]
$('#fu_form').toggleClass('busy', true)
updateStatus('Starting...')
uploadLine()
}
function uploadLine () {
if (!$('#fu_modal').hasClass('visible')) {
// Modal is closed, cancel
return
}
if (!conn.canSend()) {
// postpone
fuTout = setTimeout(uploadLine, 1)
return
}
if (inline_pos === 0) {
curLine = ''
if (line_i === 0) {
if (screen.bracketedPaste) {
curLine = '\x1b[200~'
}
}
curLine += lines[line_i++] + nl_str
if (line_i === lines.length) {
if (screen.bracketedPaste) {
curLine += '\x1b[201~'
}
}
}
let maxChunk = +qs('#fu_chunk').value
if (maxChunk === 0 || maxChunk > MAX_LINE_LEN) {
maxChunk = MAX_LINE_LEN
}
let chunk
if ((curLine.length - inline_pos) <= maxChunk) {
chunk = curLine.substr(inline_pos, maxChunk)
inline_pos = 0
} else {
chunk = curLine.substr(inline_pos, maxChunk)
inline_pos += maxChunk
}
if (!input.sendString(chunk)) {
updateStatus('FAILED!')
return
}
let pt = Math.round((line_i / lines.length) * 1000) / 10
updateStatus(`${line_i} / ${lines.length} (${pt}%)`)
if (lines.length > line_i || inline_pos > 0) {
fuTout = setTimeout(uploadLine, send_delay_ms)
} else {
closeWhenReady()
}
}
function closeWhenReady () {
if (!conn.canSend()) {
// stuck in XOFF still, wait to process...
updateStatus('Waiting for Tx buffer...')
setTimeout(closeWhenReady, 100)
} else {
updateStatus('Done.')
// delay to show it
fuClose()
}
}
function fuClose () {
modal.hide('#fu_modal')
}
return {
init: function () {
qs('#fu_file').addEventListener('change', function (evt) {
let reader = new window.FileReader()
let file = evt.target.files[0]
let ftype = file.type || 'application/octet-stream'
console.log('Selected file type: ' + ftype)
if (!ftype.match(/text\/.*|application\/(json|csv|.*xml.*|.*script.*|x-php)/)) {
// Deny load of blobs like img - can crash browser and will get corrupted anyway
if (!window.confirm(`This does not look like a text file: ${ftype}\nReally load?`)) {
qs('#fu_file').value = ''
return
}
}
reader.onload = function (e) {
const txt = e.target.result.replace(/[\r\n]+/, '\n')
qs('#fu_text').value = txt
}
console.log('Loading file...')
reader.readAsText(file)
}, false)
qs('#term-fu-open').addEventListener('click', e => {
e.preventDefault()
openUploadDialog()
})
qs('#term-fu-start').addEventListener('click', e => {
e.preventDefault()
startUpload()
})
qs('#term-fu-close').addEventListener('click', e => {
e.preventDefault()
fuClose()
})
},
open: openUploadDialog,
setContent (content) {
qs('#fu_text').value = content
}
}
}

@ -0,0 +1,106 @@
const ColorTriangle = require('./lib/colortriangle')
const $ = require('./lib/chibi')
const themes = require('./term/themes')
const { qs } = require('./utils')
function selectedTheme () {
return +$('#theme').val()
}
exports.init = function () {
$('#theme').on('change', showColor)
$('#default_fg,#default_bg').on('input', showColor)
let opts = {
padding: 10,
event: 'drag',
uppercase: true,
trianglePointerSize: 20,
// wheelPointerSize: 12,
size: 200,
parseColor: (color) => {
return themes.toHex(color, selectedTheme())
}
}
ColorTriangle.initInput(qs('#default_fg'), opts)
ColorTriangle.initInput(qs('#default_bg'), opts)
for (let i = 1; i <= 5; i++) {
ColorTriangle.initInput(qs(`#bc${i}`), opts)
}
$('.colorprev.bg span').on('click', function () {
const bg = this.dataset.bg
if (typeof bg != 'undefined') $('#default_bg').val(bg)
showColor()
})
$('.colorprev.fg span').on('click', function () {
const fg = this.dataset.fg
if (typeof fg != 'undefined') $('#default_fg').val(fg)
showColor()
})
let $presets = $('#fgbg_presets')
for (let i = 0; i < themes.fgbgThemes.length; i++) {
const thm = themes.fgbgThemes[i]
const fg = thm[0]
const bg = thm[1]
const lbl = thm[2]
const tit = thm[3]
$presets.htmlAppend(
'<span class="preset" ' +
'data-xfg="' + fg + '" data-xbg="' + bg + '" ' +
'style="color:' + fg + ';background:' + bg + '" title="' + tit + '">&nbsp;' + lbl + '&nbsp;</span>')
if ((i + 1) % 5 === 0) $presets.htmlAppend('<br>')
}
$('.preset').on('click', function () {
$('#default_fg').val(this.dataset.xfg)
$('#default_bg').val(this.dataset.xbg)
showColor()
})
showColor()
}
function showColor () {
let ex = qs('.color-example')
let fg = $('#default_fg').val()
let bg = $('#default_bg').val()
if (/^\d+$/.test(fg)) {
fg = +fg
} else if (!/^#[\da-f]{6}$/i.test(fg)) {
fg = 'black'
}
if (/^\d+$/.test(bg)) {
bg = +bg
} else if (!/^#[\da-f]{6}$/i.test(bg)) {
bg = 'black'
}
const themeN = selectedTheme()
ex.dataset.fg = fg
ex.dataset.bg = bg
themes.themePreview(themeN)
$('.colorprev.fg span').css('background', themes.toHex(bg, themeN))
}
exports.nextTheme = () => {
let sel = qs('#theme')
let i = sel.selectedIndex
sel.options[++i % sel.options.length].selected = true
showColor()
}
exports.prevTheme = () => {
let sel = qs('#theme')
let i = sel.selectedIndex
sel.options[(sel.options.length + (--i)) % sel.options.length].selected = true
showColor()
}

@ -0,0 +1,60 @@
/** Make a node */
exports.mk = function mk (e) {
return document.createElement(e)
}
/** Find one by query */
exports.qs = function qs (s) {
return document.querySelector(s)
}
/** Find all by query */
exports.qsa = function qsa (s) {
return document.querySelectorAll(s)
}
/**
* Filter 'spacebar' and 'return' from keypress handler,
* and when they're pressed, fire the callback.
* use $(...).on('keypress', cr(handler))
*/
exports.cr = function cr (hdl) {
return function (e) {
if (e.which === 10 || e.which === 13 || e.which === 32) {
hdl()
}
}
}
/** Decode number from 2B encoding */
exports.parse2B = function parse2B (s, i = 0) {
return (s.charCodeAt(i++) - 1) + (s.charCodeAt(i) - 1) * 127
}
/** Decode number from 3B encoding */
exports.parse3B = function parse3B (s, i = 0) {
return (s.charCodeAt(i) - 1) + (s.charCodeAt(i + 1) - 1) * 127 + (s.charCodeAt(i + 2) - 1) * 127 * 127
}
/** Encode using 2B encoding, returns string. */
exports.encode2B = function encode2B (n) {
let lsb, msb
lsb = (n % 127)
n = ((n - lsb) / 127)
lsb += 1
msb = (n + 1)
return String.fromCharCode(lsb) + String.fromCharCode(msb)
}
/** Encode using 3B encoding, returns string. */
exports.encode3B = function encode3B (n) {
let lsb, msb, xsb
lsb = (n % 127)
n = (n - lsb) / 127
lsb += 1
msb = (n % 127)
n = (n - msb) / 127
msb += 1
xsb = (n + 1)
return String.fromCharCode(lsb) + String.fromCharCode(msb) + String.fromCharCode(xsb)
}

@ -0,0 +1,173 @@
const $ = require('./lib/chibi')
const { mk } = require('./utils')
const tr = require('./lang')
const HTTPS = window.location.protocol.match(/s:/)
{
const w = window.WiFi = {}
const authTypes = ['Open', 'WEP', 'WPA', 'WPA2', 'WPA/WPA2']
let curSSID
// Get XX % for a slider input
function calc_dBm (inp) {
return `+${(inp.value * 0.25).toFixed(2)} dBm`
}
// Display selected STA SSID etc
function selectSta (name, password, ip) {
$('#sta_ssid').val(name)
$('#sta_password').val(password)
$('#sta-nw').toggleClass('hidden', name.length === 0)
$('#sta-nw-nil').toggleClass('hidden', name.length > 0)
$('#sta-nw .essid').html($.htmlEscape(name))
const hasPassword = !!password
// (the following is kind of confusing with the double-double negations,
// but it works)
$('#sta-nw .passwd').toggleClass('hidden', !hasPassword)
$('#sta-nw .nopasswd').toggleClass('hidden', hasPassword)
$('#sta-nw .ip').html(ip.length > 0 ? tr('wifi.connected_ip_is') + ip : tr('wifi.not_conn'))
}
/** Update display for received response */
function onScan (resp, status) {
// var ap_json = {
// "result": {
// "inProgress": "0",
// "APs": [
// {"essid": "Chlivek", "bssid": "88:f7:c7:52:b3:99", "rssi": "204", "enc": "4", "channel": "1"},
// {"essid": "TyNikdy", "bssid": "5c:f4:ab:0d:f1:1b", "rssi": "164", "enc": "3", "channel": "1"},
// ]
// }
// };
if (status !== 200) {
// bad response
rescan(5000) // wait 5s then retry
return
}
try {
resp = JSON.parse(resp)
} catch (e) {
console.log(e)
rescan(5000)
return
}
const done = !resp.result.inProgress && resp.result.APs.length > 0
rescan(done ? 15000 : 1000)
if (!done) return // no redraw yet
// clear the AP list
let $list = $('#ap-list')
// remove old APs
$('#ap-list .AP').remove()
$list.toggleClass('hidden', !done)
$('#ap-loader').toggleClass('hidden', done)
// scan done
resp.result.APs.sort((a, b) => b.rssi - a.rssi).forEach(function (ap) {
ap.enc = parseInt(ap.enc)
if (ap.enc > 4) return // hide unsupported auths
let item = mk('div')
let $item = $(item)
.data('ssid', ap.essid)
.data('pwd', ap.enc)
.attr('tabindex', 0)
.addClass('AP')
// mark current SSID
if (ap.essid === curSSID) {
$item.addClass('selected')
}
let inner = mk('div')
let escapedSSID = $.htmlEscape(ap.essid)
$(inner).addClass('inner')
.htmlAppend(`<div class="rssi">${ap.rssi_perc}</div>`)
.htmlAppend(`<div class="essid" title="${escapedSSID}">${escapedSSID}</div>`)
.htmlAppend(`<div class="auth">${authTypes[ap.enc]}</div>`)
$item.on('click', function () {
let $th = $(this)
const conn_ssid = $th.data('ssid')
let conn_pass = ''
if (+$th.data('pwd')) {
// this AP needs a password
conn_pass = window.prompt(tr('wifi.enter_passwd').replace(':ssid:', conn_ssid))
if (!conn_pass) return
}
$('#sta_password').val(conn_pass)
$('#sta_ssid').val(conn_ssid)
selectSta(conn_ssid, conn_pass, '')
})
item.appendChild(inner)
$list[0].appendChild(item)
})
}
function startScanning () {
$('#ap-loader').removeClass('hidden')
$('#ap-scan').addClass('hidden')
$('#ap-loader .anim-dots').html('.')
scanAPs()
}
/** Ask the CGI what APs are visible (async) */
function scanAPs () {
if (window._demo) {
onScan(window._demo_aps, 200)
} else {
$.get(`${HTTPS ? 'https' : 'http'}://${window._root}/cfg/wifi/scan`, onScan)
}
}
function rescan (time) {
setTimeout(scanAPs, time)
}
/** Set up the WiFi page */
function wifiInit (cfg) {
// Update slider value displays
$('.Row.range').forEach(function (x) {
let inp = x.querySelector('input')
let disp1 = x.querySelector('.x-disp1')
let disp2 = x.querySelector('.x-disp2')
let t = calc_dBm(inp)
$(disp1).html(t)
$(disp2).html(t)
$(inp).on('input', function () {
t = calc_dBm(inp)
$(disp1).html(t)
$(disp2).html(t)
})
})
// Forget STA credentials
$('#forget-sta').on('click', () => {
selectSta('', '', '')
return false
})
selectSta(cfg.sta_ssid, cfg.sta_password, cfg.sta_active_ip)
curSSID = cfg.sta_active_ssid
}
w.init = wifiInit
w.startScanning = startScanning
}

@ -1,189 +0,0 @@
/** Global generic init */
$.ready(function () {
// Checkbox UI (checkbox CSS and hidden input with int value)
$('.Row.checkbox').forEach(function(x) {
var inp = x.querySelector('input');
var box = x.querySelector('.box');
$(box).toggleClass('checked', inp.value);
var hdl = function() {
inp.value = 1 - inp.value;
$(box).toggleClass('checked', inp.value)
};
$(x).on('click', hdl).on('keypress', cr(hdl));
});
// Expanding boxes on mobile
$('.Box.mobcol,.Box.fold').forEach(function(x) {
var h = x.querySelector('h2');
var hdl = function() {
$(x).toggleClass('expanded');
};
$(h).on('click', hdl).on('keypress', cr(hdl));
});
$('form').forEach(function(x) {
$(x).on('keypress', function(e) {
if ((e.keyCode == 10 || e.keyCode == 13) && e.ctrlKey) {
x.submit();
}
})
});
// loader dots...
setInterval(function () {
$('.anim-dots').each(function (x) {
var $x = $(x);
var dots = $x.html() + '.';
if (dots.length == 5) dots = '.';
$x.html(dots);
});
}, 1000);
// flipping number boxes with the mouse wheel
$('input[type=number]').on('mousewheel', function(e) {
var $this = $(this);
var val = +$this.val();
if (isNaN(val)) val = 1;
var step = +($this.attr('step') || 1);
var min = +$this.attr('min');
var max = +$this.attr('max');
if(e.wheelDelta > 0) {
val += step;
} else {
val -= step;
}
if (typeof min != 'undefined') val = Math.max(val, +min);
if (typeof max != 'undefined') val = Math.min(val, +max);
$this.val(val);
if ("createEvent" in document) {
var evt = document.createEvent("HTMLEvents");
evt.initEvent("change", false, true);
$this[0].dispatchEvent(evt);
} else {
$this[0].fireEvent("onchange");
}
e.preventDefault();
});
var errAt = location.search.indexOf('err=');
if (errAt !== -1 && qs('.Box.errors')) {
var errs = location.search.substr(errAt+4).split(',');
var hres = [];
errs.forEach(function(er) {
var lbl = qs('label[for="'+er+'"]');
if (lbl) {
lbl.classList.add('error');
hres.push(lbl.childNodes[0].textContent.trim().replace(/: ?$/, ''));
} else {
hres.push(er);
}
});
qs('.Box.errors .list').innerHTML = hres.join(', ');
qs('.Box.errors').classList.remove('hidden');
}
Modal.init();
Notify.init();
// remove tabindixes from h2 if wide
if (window.innerWidth > 550) {
$('.Box h2').forEach(function (x) {
x.removeAttribute('tabindex');
});
// brand works as a link back to term in widescreen mode
var br = qs('#brand');
br && br.addEventListener('click', function() {
location.href='/'; // go to terminal
});
}
});
$._loader = function(vis) {
$('#loader').toggleClass('show', vis);
};
function showPage() {
$('#content').addClass('load');
}
$.ready(function() {
if (window.noAutoShow !== true) {
setTimeout(function () {
showPage();
}, 1);
}
});
/*! http://mths.be/fromcodepoint v0.1.0 by @mathias */
if (!String.fromCodePoint) {
(function() {
var defineProperty = (function() {
// IE 8 only supports `Object.defineProperty` on DOM elements
try {
var object = {};
var $defineProperty = Object.defineProperty;
var result = $defineProperty(object, object, object) && $defineProperty;
} catch(error) {}
return result;
}());
var stringFromCharCode = String.fromCharCode;
var floor = Math.floor;
var fromCodePoint = function() {
var MAX_SIZE = 0x4000;
var codeUnits = [];
var highSurrogate;
var lowSurrogate;
var index = -1;
var length = arguments.length;
if (!length) {
return '';
}
var result = '';
while (++index < length) {
var codePoint = Number(arguments[index]);
if (
!isFinite(codePoint) || // `NaN`, `+Infinity`, or `-Infinity`
codePoint < 0 || // not a valid Unicode code point
codePoint > 0x10FFFF || // not a valid Unicode code point
floor(codePoint) != codePoint // not an integer
) {
throw RangeError('Invalid code point: ' + codePoint);
}
if (codePoint <= 0xFFFF) { // BMP code point
codeUnits.push(codePoint);
} else { // Astral code point; split in surrogate halves
// http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
codePoint -= 0x10000;
highSurrogate = (codePoint >> 10) + 0xD800;
lowSurrogate = (codePoint % 0x400) + 0xDC00;
codeUnits.push(highSurrogate, lowSurrogate);
}
if (index + 1 == length || codeUnits.length > MAX_SIZE) {
result += stringFromCharCode.apply(null, codeUnits);
codeUnits.length = 0;
}
}
return result;
};
if (defineProperty) {
defineProperty(String, 'fromCodePoint', {
'value': fromCodePoint,
'configurable': true,
'writable': true
});
} else {
String.fromCodePoint = fromCodePoint;
}
}());
}

@ -1,310 +0,0 @@
// keymaster.js
// (c) 2011-2013 Thomas Fuchs
// keymaster.js may be freely distributed under the MIT license.
;(function(global){
var k,
_handlers = {},
_mods = { 16: false, 18: false, 17: false, 91: false },
_scope = 'all',
// modifier keys
_MODIFIERS = {
'⇧': 16, shift: 16,
'⌥': 18, alt: 18, option: 18,
'⌃': 17, ctrl: 17, control: 17,
'⌘': 91, command: 91
},
// special keys
_MAP = {
backspace: 8, tab: 9, clear: 12,
enter: 13, 'return': 13,
esc: 27, escape: 27, space: 32,
left: 37, up: 38,
right: 39, down: 40,
del: 46, 'delete': 46,
home: 36, end: 35,
pageup: 33, pagedown: 34,
',': 188, '.': 190, '/': 191,
'`': 192, '-': 189, '=': 187,
';': 186, '\'': 222,
'[': 219, ']': 221, '\\': 220,
// added:
insert: 45,
np_0: 96, np_1: 97, np_2: 98, np_3: 99, np_4: 100, np_5: 101,
np_6: 102, np_7: 103, np_8: 104, np_9: 105, np_mul: 106,
np_add: 107, np_sub: 109, np_point: 110, np_div: 111, numlock: 144,
},
code = function(x){
return _MAP[x] || x.toUpperCase().charCodeAt(0);
},
_downKeys = [];
for(k=1;k<20;k++) _MAP['f'+k] = 111+k;
// IE doesn't support Array#indexOf, so have a simple replacement
function index(array, item){
var i = array.length;
while(i--) if(array[i]===item) return i;
return -1;
}
// for comparing mods before unassignment
function compareArray(a1, a2) {
if (a1.length != a2.length) return false;
for (var i = 0; i < a1.length; i++) {
if (a1[i] !== a2[i]) return false;
}
return true;
}
var modifierMap = {
16:'shiftKey',
18:'altKey',
17:'ctrlKey',
91:'metaKey'
};
function updateModifierKey(event) {
for(k in _mods) _mods[k] = event[modifierMap[k]];
};
function isModifierPressed(mod) {
if (mod=='control'||mod=='ctrl') return _mods[17];
if (mod=='shift') return _mods[16];
if (mod=='meta') return _mods[91];
if (mod=='alt') return _mods[18];
return false;
}
// handle keydown event
function dispatch(event) {
var key, handler, k, i, modifiersMatch, scope;
key = event.keyCode;
if (index(_downKeys, key) == -1) {
_downKeys.push(key);
}
// if a modifier key, set the key.<modifierkeyname> property to true and return
if(key == 93 || key == 224) key = 91; // right command on webkit, command on Gecko
if(key in _mods) {
_mods[key] = true;
// 'assignKey' from inside this closure is exported to window.key
for(k in _MODIFIERS) if(_MODIFIERS[k] == key) assignKey[k] = true;
return;
}
updateModifierKey(event);
// see if we need to ignore the keypress (filter() can can be overridden)
// by default ignore key presses if a select, textarea, or input is focused
if(!assignKey.filter.call(this, event)) return;
// abort if no potentially matching shortcuts found
if (!(key in _handlers)) return;
scope = getScope();
// for each potential shortcut
for (i = 0; i < _handlers[key].length; i++) {
handler = _handlers[key][i];
// see if it's in the current scope
if(handler.scope == scope || handler.scope == 'all'){
// check if modifiers match if any
modifiersMatch = handler.mods.length > 0;
for(k in _mods)
if((!_mods[k] && index(handler.mods, +k) > -1) ||
(_mods[k] && index(handler.mods, +k) == -1)) modifiersMatch = false;
// call the handler and stop the event if neccessary
if((handler.mods.length == 0 && !_mods[16] && !_mods[18] && !_mods[17] && !_mods[91]) || modifiersMatch){
if(handler.method(event, handler)===false){
if(event.preventDefault) event.preventDefault();
else event.returnValue = false;
if(event.stopPropagation) event.stopPropagation();
if(event.cancelBubble) event.cancelBubble = true;
}
}
}
}
};
// unset modifier keys on keyup
function clearModifier(event){
var key = event.keyCode, k,
i = index(_downKeys, key);
// remove key from _downKeys
if (i >= 0) {
_downKeys.splice(i, 1);
}
if(key == 93 || key == 224) key = 91;
if(key in _mods) {
_mods[key] = false;
for(k in _MODIFIERS) if(_MODIFIERS[k] == key) assignKey[k] = false;
}
};
function resetModifiers() {
for(k in _mods) _mods[k] = false;
for(k in _MODIFIERS) assignKey[k] = false;
};
// parse and assign shortcut
function assignKey(key, scope, method){
var keys, mods;
keys = getKeys(key);
if (method === undefined) {
method = scope;
scope = 'all';
}
// for each shortcut
for (var i = 0; i < keys.length; i++) {
// set modifier keys if any
mods = [];
key = keys[i].split('+');
if (key.length > 1){
mods = getMods(key);
key = [key[key.length-1]];
}
// convert to keycode and...
key = key[0]
key = code(key);
// ...store handler
if (!(key in _handlers)) _handlers[key] = [];
_handlers[key].push({ shortcut: keys[i], scope: scope, method: method, key: keys[i], mods: mods });
}
};
// unbind all handlers for given key in current scope
function unbindKey(key, scope) {
var multipleKeys, keys,
mods = [],
i, j, obj;
multipleKeys = getKeys(key);
for (j = 0; j < multipleKeys.length; j++) {
keys = multipleKeys[j].split('+');
if (keys.length > 1) {
mods = getMods(keys);
}
key = keys[keys.length - 1];
key = code(key);
if (scope === undefined) {
scope = getScope();
}
if (!_handlers[key]) {
return;
}
for (i = 0; i < _handlers[key].length; i++) {
obj = _handlers[key][i];
// only clear handlers if correct scope and mods match
if (obj.scope === scope && compareArray(obj.mods, mods)) {
_handlers[key][i] = {};
}
}
}
};
// Returns true if the key with code 'keyCode' is currently down
// Converts strings into key codes.
function isPressed(keyCode) {
if (typeof(keyCode)=='string') {
keyCode = code(keyCode);
}
return index(_downKeys, keyCode) != -1;
}
function getPressedKeyCodes() {
return _downKeys.slice(0);
}
function filter(event){
var tagName = (event.target || event.srcElement).tagName;
// ignore keypressed in any elements that support keyboard data input
return !(tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA');
}
// initialize key.<modifier> to false
for(k in _MODIFIERS) assignKey[k] = false;
// set current scope (default 'all')
function setScope(scope){ _scope = scope || 'all' };
function getScope(){ return _scope || 'all' };
// delete all handlers for a given scope
function deleteScope(scope){
var key, handlers, i;
for (key in _handlers) {
handlers = _handlers[key];
for (i = 0; i < handlers.length; ) {
if (handlers[i].scope === scope) handlers.splice(i, 1);
else i++;
}
}
};
// abstract key logic for assign and unassign
function getKeys(key) {
var keys;
key = key.replace(/\s/g, '');
keys = key.split(',');
if ((keys[keys.length - 1]) == '') {
keys[keys.length - 2] += ',';
}
return keys;
}
// abstract mods logic for assign and unassign
function getMods(key) {
var mods = key.slice(0, key.length - 1);
for (var mi = 0; mi < mods.length; mi++)
mods[mi] = _MODIFIERS[mods[mi]];
return mods;
}
// cross-browser events
function addEvent(object, event, method) {
if (object.addEventListener)
object.addEventListener(event, method, false);
else if(object.attachEvent)
object.attachEvent('on'+event, function(){ method(window.event) });
};
// set the handlers globally on document
addEvent(document, 'keydown', function(event) { dispatch(event) }); // Passing _scope to a callback to ensure it remains the same by execution. Fixes #48
addEvent(document, 'keyup', clearModifier);
// reset modifiers to false whenever the window is (re)focused.
addEvent(window, 'focus', resetModifiers);
// store previously defined key
var previousKey = global.key;
// restore previously defined key and return reference to our key object
function noConflict() {
var k = global.key;
global.key = previousKey;
return k;
}
// set window.key and window.key.set/get/deleteScope, and the default filter
global.key = assignKey;
global.key.setScope = setScope;
global.key.getScope = getScope;
global.key.deleteScope = deleteScope;
global.key.filter = filter;
global.key.isPressed = isPressed;
global.key.isModifier = isModifierPressed;
global.key.getPressedKeyCodes = getPressedKeyCodes;
global.key.noConflict = noConflict;
global.key.unbind = unbindKey;
if(typeof module !== 'undefined') module.exports = assignKey;
})(this);

@ -1,8 +0,0 @@
// Generated from PHP locale file
var _tr = {
"wifi.connected_ip_is": "Connected, IP is ",
"wifi.not_conn": "Not connected.",
"wifi.enter_passwd": "Enter password for \":ssid:\""
};
function tr(key) { return _tr[key] || '?'+key+'?'; }

@ -1,44 +0,0 @@
/** Module for toggling a modal overlay */
(function () {
var modal = {};
var curCloseCb = null;
modal.show = function (sel, closeCb) {
var $m = $(sel);
$m.removeClass('hidden visible');
setTimeout(function () {
$m.addClass('visible');
}, 1);
curCloseCb = closeCb;
};
modal.hide = function (sel) {
var $m = $(sel);
$m.removeClass('visible');
setTimeout(function () {
$m.addClass('hidden');
if (curCloseCb) curCloseCb();
}, 500); // transition time
};
modal.init = function () {
// close modal by click outside the dialog
$('.Modal').on('click', function () {
if ($(this).hasClass('no-close')) return; // this is a no-close modal
modal.hide(this);
});
$('.Dialog').on('click', function (e) {
e.stopImmediatePropagation();
});
// Hide all modals on esc
$(window).on('keydown', function (e) {
if (e.which == 27) {
modal.hide('.Modal');
}
});
};
window.Modal = modal;
})();

@ -1,32 +0,0 @@
(function (nt) {
var sel = '#notif';
var hideTmeo1; // timeout to start hiding (transition)
var hideTmeo2; // timeout to add the hidden class
nt.show = function (message, timeout) {
$(sel).html(message);
Modal.show(sel);
clearTimeout(hideTmeo1);
clearTimeout(hideTmeo2);
if (undef(timeout)) timeout = 2500;
hideTmeo1 = setTimeout(nt.hide, timeout);
};
nt.hide = function () {
var $m = $(sel);
$m.removeClass('visible');
hideTmeo2 = setTimeout(function () {
$m.addClass('hidden');
}, 250); // transition time
};
nt.init = function() {
$(sel).on('click', function() {
nt.hide(this);
});
};
})(window.Notify = {});

@ -1,6 +0,0 @@
/** Init the terminal sub-module - called from HTML */
window.termInit = function () {
Conn.init();
Input.init();
TermUpl.init();
};

@ -1,134 +0,0 @@
/** Handle connections */
var Conn = (function () {
var ws;
var heartbeatTout;
var pingIv;
var xoff = false;
var autoXoffTout;
var reconTout;
var pageShown = false;
function onOpen(evt) {
console.log("CONNECTED");
doSend("i");
}
function onClose(evt) {
console.warn("SOCKET CLOSED, code " + evt.code + ". Reconnecting...");
clearTimeout(reconTout);
reconTout = setTimeout(function () {
init();
}, 2000);
// this happens when the buffer gets fucked up via invalid unicode.
// we basically use polling instead of socket then
}
function onMessage(evt) {
try {
// . = heartbeat
switch (evt.data.charAt(0)) {
case 'B':
case 'T':
case 'S':
Screen.load(evt.data);
if(!pageShown) {
showPage();
pageShown = true;
}
break;
case '-':
//console.log('xoff');
xoff = true;
autoXoffTout = setTimeout(function () {
xoff = false;
}, 250);
break;
case '+':
//console.log('xon');
xoff = false;
clearTimeout(autoXoffTout);
break;
}
heartbeat();
} catch (e) {
console.error(e);
}
}
function canSend() {
return !xoff;
}
function doSend(message) {
if (_demo) {
console.log("TX: ", message);
return true; // Simulate success
}
if (xoff) {
// TODO queue
console.log("Can't send, flood control.");
return false;
}
if (!ws) return false; // for dry testing
if (ws.readyState != 1) {
console.error("Socket not ready");
return false;
}
if (typeof message != "string") {
message = JSON.stringify(message);
}
ws.send(message);
return true;
}
function init() {
if (_demo) {
console.log("Demo mode!");
Screen.load(_demo_screen);
showPage();
return;
}
clearTimeout(reconTout);
clearTimeout(heartbeatTout);
ws = new WebSocket("ws://" + _root + "/term/update.ws");
ws.onopen = onOpen;
ws.onclose = onClose;
ws.onmessage = onMessage;
console.log("Opening socket.");
heartbeat();
}
function heartbeat() {
clearTimeout(heartbeatTout);
heartbeatTout = setTimeout(heartbeatFail, 2000);
}
function heartbeatFail() {
console.error("Heartbeat lost, probing server...");
pingIv = setInterval(function () {
console.log("> ping");
$.get('http://' + _root + '/system/ping', function (resp, status) {
if (status == 200) {
clearInterval(pingIv);
console.info("Server ready, reloading page...");
location.reload();
}
}, {
timeout: 100,
});
}, 1000);
}
return {
ws: null,
init: init,
send: doSend,
canSend: canSend, // check flood control
};
})();

@ -1,264 +0,0 @@
/**
* User input
*
* --- Rx messages: ---
* S - screen content (binary encoding of the entire screen with simple compression)
* T - text labels - Title and buttons, \0x01-separated
* B - beep
* . - heartbeat
*
* --- Tx messages ---
* s - string
* b - action button
* p - mb press
* r - mb release
* m - mouse move
*/
var Input = (function() {
var opts = {
np_alt: false,
cu_alt: false,
fn_alt: false,
mt_click: false,
mt_move: false,
no_keys: false,
crlf_mode: false,
};
/** Send a literal message */
function sendStrMsg(str) {
return Conn.send("s"+str);
}
/** Send a button event */
function sendBtnMsg(n) {
Conn.send("b"+Chr(n));
}
/** Fn alt choice for key message */
function fa(alt, normal) {
return opts.fn_alt ? alt : normal;
}
/** Cursor alt choice for key message */
function ca(alt, normal) {
return opts.cu_alt ? alt : normal;
}
/** Numpad alt choice for key message */
function na(alt, normal) {
return opts.np_alt ? alt : normal;
}
function _bindFnKeys() {
var keymap = {
'tab': '\x09',
'backspace': '\x08',
'enter': opts.crlf_mode ? '\x0d\x0a' : '\x0d',
'ctrl+enter': '\x0a',
'esc': '\x1b',
'up': ca('\x1bOA', '\x1b[A'),
'down': ca('\x1bOB', '\x1b[B'),
'right': ca('\x1bOC', '\x1b[C'),
'left': ca('\x1bOD', '\x1b[D'),
'home': ca('\x1bOH', fa('\x1b[H', '\x1b[1~')),
'insert': '\x1b[2~',
'delete': '\x1b[3~',
'end': ca('\x1bOF', fa('\x1b[F', '\x1b[4~')),
'pageup': '\x1b[5~',
'pagedown': '\x1b[6~',
'f1': fa('\x1bOP', '\x1b[11~'),
'f2': fa('\x1bOQ', '\x1b[12~'),
'f3': fa('\x1bOR', '\x1b[13~'),
'f4': fa('\x1bOS', '\x1b[14~'),
'f5': '\x1b[15~', // note the disconnect
'f6': '\x1b[17~',
'f7': '\x1b[18~',
'f8': '\x1b[19~',
'f9': '\x1b[20~',
'f10': '\x1b[21~', // note the disconnect
'f11': '\x1b[23~',
'f12': '\x1b[24~',
'shift+f1': fa('\x1bO1;2P', '\x1b[25~'),
'shift+f2': fa('\x1bO1;2Q', '\x1b[26~'), // note the disconnect
'shift+f3': fa('\x1bO1;2R', '\x1b[28~'),
'shift+f4': fa('\x1bO1;2S', '\x1b[29~'), // note the disconnect
'shift+f5': fa('\x1b[15;2~', '\x1b[31~'),
'shift+f6': fa('\x1b[17;2~', '\x1b[32~'),
'shift+f7': fa('\x1b[18;2~', '\x1b[33~'),
'shift+f8': fa('\x1b[19;2~', '\x1b[34~'),
'shift+f9': fa('\x1b[20;2~', '\x1b[35~'), // 35-38 are not standard - but what is?
'shift+f10': fa('\x1b[21;2~', '\x1b[36~'),
'shift+f11': fa('\x1b[22;2~', '\x1b[37~'),
'shift+f12': fa('\x1b[23;2~', '\x1b[38~'),
'np_0': na('\x1bOp', '0'),
'np_1': na('\x1bOq', '1'),
'np_2': na('\x1bOr', '2'),
'np_3': na('\x1bOs', '3'),
'np_4': na('\x1bOt', '4'),
'np_5': na('\x1bOu', '5'),
'np_6': na('\x1bOv', '6'),
'np_7': na('\x1bOw', '7'),
'np_8': na('\x1bOx', '8'),
'np_9': na('\x1bOy', '9'),
'np_mul': na('\x1bOR', '*'),
'np_add': na('\x1bOl', '+'),
'np_sub': na('\x1bOS', '-'),
'np_point': na('\x1bOn', '.'),
'np_div': na('\x1bOQ', '/'),
// we don't implement numlock key (should change in numpad_alt mode, but it's even more useless than the rest)
};
for (var k in keymap) {
if (keymap.hasOwnProperty(k)) {
bind(k, keymap[k]);
}
}
}
/** Bind a keystroke to message */
function bind(combo, str) {
// mac fix - allow also cmd
if (combo.indexOf('ctrl+') !== -1) {
combo += ',' + combo.replace('ctrl', 'command');
}
// unbind possible old binding
key.unbind(combo);
key(combo, function (e) {
if (opts.no_keys) return;
e.preventDefault();
sendStrMsg(str)
});
}
/** Bind/rebind key messages */
function _initKeys() {
// This takes care of text characters typed
window.addEventListener('keypress', function(evt) {
if (opts.no_keys) return;
var str = '';
if (evt.key) str = evt.key;
else if (evt.which) str = String.fromCodePoint(evt.which);
if (str.length>0 && str.charCodeAt(0) >= 32) {
// console.log("Typed ", str);
sendStrMsg(str);
}
});
// ctrl-letter codes are sent as simple low ASCII codes
for (var i = 1; i<=26;i++) {
bind('ctrl+' + String.fromCharCode(96+i), String.fromCharCode(i));
}
bind('ctrl+]', '\x1b'); // alternate way to enter ESC
bind('ctrl+\\', '\x1c');
bind('ctrl+[', '\x1d');
bind('ctrl+^', '\x1e');
bind('ctrl+_', '\x1f');
_bindFnKeys();
}
// mouse button states
var mb1 = 0;
var mb2 = 0;
var mb3 = 0;
/** Init the Input module */
function init() {
_initKeys();
// Button presses
$('#action-buttons button').forEach(function(s) {
s.addEventListener('click', function() {
sendBtnMsg(+this.dataset['n']);
});
});
// global mouse state tracking - for motion reporting
window.addEventListener('mousedown', function(evt) {
if (evt.button == 0) mb1 = 1;
if (evt.button == 1) mb2 = 1;
if (evt.button == 2) mb3 = 1;
});
window.addEventListener('mouseup', function(evt) {
if (evt.button == 0) mb1 = 0;
if (evt.button == 1) mb2 = 0;
if (evt.button == 2) mb3 = 0;
});
}
/** Prepare modifiers byte for mouse message */
function packModifiersForMouse() {
return (key.isModifier('ctrl')?1:0) |
(key.isModifier('shift')?2:0) |
(key.isModifier('alt')?4:0) |
(key.isModifier('meta')?8:0);
}
return {
/** Init the Input module */
init: init,
/** Send a literal string message */
sendString: sendStrMsg,
/** Enable alternate key modes (cursors, numpad, fn) */
setAlts: function(cu, np, fn, crlf) {
if (opts.cu_alt != cu || opts.np_alt != np || opts.fn_alt != fn || opts.crlf_mode != crlf) {
opts.cu_alt = cu;
opts.np_alt = np;
opts.fn_alt = fn;
opts.crlf_mode = crlf;
// rebind keys - codes have changed
_bindFnKeys();
}
},
setMouseMode: function(click, move) {
opts.mt_click = click;
opts.mt_move = move;
},
// Mouse events
onMouseMove: function (x, y) {
if (!opts.mt_move) return;
var b = mb1 ? 1 : mb2 ? 2 : mb3 ? 3 : 0;
var m = packModifiersForMouse();
Conn.send("m" + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m));
},
onMouseDown: function (x, y, b) {
if (!opts.mt_click) return;
if (b > 3 || b < 1) return;
var m = packModifiersForMouse();
Conn.send("p" + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m));
// console.log("B ",b," M ",m);
},
onMouseUp: function (x, y, b) {
if (!opts.mt_click) return;
if (b > 3 || b < 1) return;
var m = packModifiersForMouse();
Conn.send("r" + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m));
// console.log("B ",b," M ",m);
},
onMouseWheel: function (x, y, dir) {
if (!opts.mt_click) return;
// -1 ... btn 4 (away from user)
// +1 ... btn 5 (towards user)
var m = packModifiersForMouse();
var b = (dir < 0 ? 4 : 5);
Conn.send("p" + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m));
// console.log("B ",b," M ",m);
},
mouseTracksClicks: function() {
return opts.mt_click;
},
blockKeys: function(yes) {
opts.no_keys = yes;
}
};
})();

@ -1,380 +0,0 @@
var Screen = (function () {
var W = 0, H = 0; // dimensions
var inited = false;
var cursor = {
a: false, // active (blink state)
x: 0, // 0-based coordinates
y: 0,
fg: 7, // colors 0-15
bg: 0,
attrs: 0,
suppress: false, // do not turn on in blink interval (for safe moving)
forceOn: false, // force on unless hanging: used to keep cursor visible during move
hidden: false, // do not show (DEC opt)
hanging: false, // cursor at column "W+1" - not visible
};
var screen = [];
var blinkIval;
var cursorFlashStartIval;
// Some non-bold Fraktur symbols are outside the contiguous block
var frakturExceptions = {
'C': '\u212d',
'H': '\u210c',
'I': '\u2111',
'R': '\u211c',
'Z': '\u2128',
};
// for BEL
var audioCtx = null;
try {
audioCtx = new (window.AudioContext || window.audioContext || window.webkitAudioContext)();
} catch (er) {
console.error("No AudioContext!", er);
}
/** Get cell under cursor */
function _curCell() {
return screen[cursor.y*W + cursor.x];
}
/** Safely move cursor */
function cursorSet(y, x) {
// Hide and prevent from showing up during the move
cursor.suppress = true;
_draw(_curCell(), false);
cursor.x = x;
cursor.y = y;
// Show again
cursor.suppress = false;
_draw(_curCell());
}
function alpha2fraktur(t) {
// perform substitution
if (t >= 'a' && t <= 'z') {
t = String.fromCodePoint(0x1d51e - 97 + t.charCodeAt(0));
}
else if (t >= 'A' && t <= 'Z') {
// this set is incomplete, some exceptions are needed
if (frakturExceptions.hasOwnProperty(t)) {
t = frakturExceptions[t];
} else {
t = String.fromCodePoint(0x1d504 - 65 + t.charCodeAt(0));
}
}
return t;
}
/** Update cell on display. inv = invert (for cursor) */
function _draw(cell, inv) {
if (!cell) return;
if (typeof inv == 'undefined') {
inv = cursor.a && cursor.x == cell.x && cursor.y == cell.y;
}
var fg, bg, cn, t;
fg = inv ? cell.bg : cell.fg;
bg = inv ? cell.fg : cell.bg;
t = cell.t;
if (!t.length) t = ' ';
cn = 'fg' + fg + ' bg' + bg;
if (cell.attrs & (1<<0)) cn += ' bold';
if (cell.attrs & (1<<1)) cn += ' faint';
if (cell.attrs & (1<<2)) cn += ' italic';
if (cell.attrs & (1<<3)) cn += ' under';
if (cell.attrs & (1<<4)) cn += ' blink';
if (cell.attrs & (1<<5)) {
cn += ' fraktur';
t = alpha2fraktur(t);
}
if (cell.attrs & (1<<6)) cn += ' strike';
cell.slot.textContent = t;
cell.elem.className = cn;
}
/** Show entire screen */
function _drawAll() {
for (var i = W*H-1; i>=0; i--) {
_draw(screen[i]);
}
}
function _rebuild(rows, cols) {
W = cols;
H = rows;
/* Build screen & show */
var cOuter, cInner, cell, screenDiv = qs('#screen');
// Empty the screen node
while (screenDiv.firstChild) screenDiv.removeChild(screenDiv.firstChild);
screen = [];
for(var i = 0; i < W*H; i++) {
cOuter = mk('span');
cInner = mk('span');
/* Mouse tracking */
(function() {
var x = i % W;
var y = Math.floor(i / W);
cOuter.addEventListener('mouseenter', function (evt) {
Input.onMouseMove(x, y);
});
cOuter.addEventListener('mousedown', function (evt) {
Input.onMouseDown(x, y, evt.button+1);
});
cOuter.addEventListener('mouseup', function (evt) {
Input.onMouseUp(x, y, evt.button+1);
});
cOuter.addEventListener('contextmenu', function (evt) {
if (Input.mouseTracksClicks()) {
evt.preventDefault();
}
});
cOuter.addEventListener('mousewheel', function (evt) {
Input.onMouseWheel(x, y, evt.deltaY>0?1:-1);
return false;
});
})();
/* End of line */
if ((i > 0) && (i % W == 0)) {
screenDiv.appendChild(mk('br'));
}
/* The cell */
cOuter.appendChild(cInner);
screenDiv.appendChild(cOuter);
cell = {
t: ' ',
fg: 7,
bg: 0, // the colors will be replaced immediately as we receive data (user won't see this)
attrs: 0,
elem: cOuter,
slot: cInner,
x: i % W,
y: Math.floor(i / W),
};
screen.push(cell);
_draw(cell);
}
}
/** Init the terminal */
function _init() {
/* Cursor blinking */
clearInterval(blinkIval);
blinkIval = setInterval(function () {
cursor.a = !cursor.a;
if (cursor.hidden || cursor.hanging) {
cursor.a = false;
}
if (!cursor.suppress) {
_draw(_curCell(), cursor.forceOn || cursor.a);
}
}, 500);
/* blink attribute animation */
setInterval(function () {
$('#screen').removeClass('blink-hide');
setTimeout(function () {
$('#screen').addClass('blink-hide');
}, 800); // 200 ms ON
}, 1000);
inited = true;
}
// constants for decoding the update blob
var SEQ_SET_COLOR_ATTR = 1;
var SEQ_REPEAT = 2;
var SEQ_SET_COLOR = 3;
var SEQ_SET_ATTR = 4;
/** Parse received screen update object (leading S removed already) */
function _load_content(str) {
var i = 0, ci = 0, j, jc, num, num2, t = ' ', fg, bg, attrs, cell;
if (!inited) _init();
var cursorMoved;
// Set size
num = parse2B(str, i); i += 2; // height
num2 = parse2B(str, i); i += 2; // width
if (num != H || num2 != W) {
_rebuild(num, num2);
}
// console.log("Size ",num, num2);
// Cursor position
num = parse2B(str, i); i += 2; // row
num2 = parse2B(str, i); i += 2; // col
cursorMoved = (cursor.x != num2 || cursor.y != num);
cursorSet(num, num2);
// console.log("Cursor at ",num, num2);
// Attributes
num = parse3B(str, i); i += 3;
cursor.hidden = !(num & (1<<0)); // DEC opt "visible"
cursor.hanging = !!(num & (1<<1));
Input.setAlts(
!!(num & (1<<2)), // cursors alt
!!(num & (1<<3)), // numpad alt
!!(num & (1<<4)), // fn keys alt
!!(num & (1<<12)) // crlf mode
);
var mt_click = !!(num & (1<<5));
var mt_move = !!(num & (1<<6));
Input.setMouseMode(
mt_click,
mt_move
);
$('#screen').toggleClass('noselect', mt_move);
var show_buttons = !!(num & (1<<7));
var show_config_links = !!(num & (1<<8));
$('.x-term-conf-btn').toggleClass('hidden', !show_config_links);
$('#action-buttons').toggleClass('hidden', !show_buttons);
// bits 9-11 are cursor shape (not implemented)
fg = 7;
bg = 0;
attrs = 0;
// Here come the content
while(i < str.length && ci<W*H) {
j = str[i++];
jc = j.charCodeAt(0);
if (jc == SEQ_SET_COLOR_ATTR) {
num = parse3B(str, i); i += 3;
fg = num & 0x0F;
bg = (num & 0xF0) >> 4;
attrs = (num & 0xFF00)>>8;
}
else if (jc == SEQ_SET_COLOR) {
num = parse2B(str, i); i += 2;
fg = num & 0x0F;
bg = (num & 0xF0) >> 4;
}
else if (jc == SEQ_SET_ATTR) {
num = parse2B(str, i); i += 2;
attrs = num & 0xFF;
}
else if (jc == SEQ_REPEAT) {
num = parse2B(str, i); i += 2;
// console.log("Repeat x ",num);
for (; num>0 && ci<W*H; num--) {
cell = screen[ci++];
cell.fg = fg;
cell.bg = bg;
cell.t = t;
cell.attrs = attrs;
}
}
else {
cell = screen[ci++];
// Unique cell character
t = cell.t = j;
cell.fg = fg;
cell.bg = bg;
cell.attrs = attrs;
// console.log("Symbol ", j);
}
}
_drawAll();
// if (!cursor.hidden || cursor.hanging || !cursor.suppress) {
// // hide cursor asap
// _draw(_curCell(), false);
// }
if (cursorMoved) {
cursor.forceOn = true;
cursorFlashStartIval = setTimeout(function() {
cursor.forceOn = false;
}, 1200);
_draw(_curCell(), true);
}
}
/** Apply labels to buttons and screen title (leading T removed already) */
function _load_labels(str) {
var pieces = str.split('\x01');
qs('h1').textContent = pieces[0];
$('#action-buttons button').forEach(function(x, i) {
var s = pieces[i+1].trim();
// if empty string, use the "dim" effect and put nbsp instead to stretch the btn vertically
x.innerHTML = s.length > 0 ? e(s) : "&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 = {});

@ -0,0 +1,14 @@
#! /usr/bin/env php
<?php
require_once __DIR__ . '/../base.php';
$selected = array_slice($argv, 1);
$output = [];
foreach ($selected as $key) {
$output[$key] = tr($key);
}
fwrite(STDOUT, json_encode($output, JSON_UNESCAPED_UNICODE));

@ -0,0 +1,54 @@
/*
* This is a Webpack loader that loads the language data by running
* dump_selected.php.
*/
const { spawnSync } = require('child_process')
const path = require('path')
const selectedKeys = require('./js-keys')
module.exports = function (source) {
let child = spawnSync(path.resolve(__dirname, '_js-dump.php'), selectedKeys, {
timeout: 1000
})
let data
try {
data = JSON.parse(child.stdout.toString().trim())
} catch (err) {
console.error(`\x1b[31;1m[lang-loader] Failed to parse JSON:`)
console.error(child.stdout.toString().trim())
console.error(`\x1b[m`)
if (err) throw err
}
// adapted from webpack/loader-utils
let remainingRequest = this.remainingRequest
if (!remainingRequest) {
remainingRequest = this.loaders.slice(this.loaderIndex + 1)
.map(obj => obj.request)
.concat([this.resource]).join('!')
}
let currentRequest = this.currentRequest
if (!currentRequest) {
remainingRequest = this.loaders.slice(this.loaderIndex)
.map(obj => obj.request)
.concat([this.resource]).join('!')
}
let map = {
version: 3,
file: currentRequest,
sourceRoot: '',
sources: [remainingRequest],
sourcesContent: [source],
names: [],
mappings: 'AAAA;AAAA'
}
this.callback(null,
`/* Generated language file */\n` +
`module.exports=${JSON.stringify(data)}\n`, map)
}

@ -0,0 +1,21 @@
<?php
return [
'appname' => 'ESPTerm',
'appname_demo' => 'ESPTerm<sup> DEMO</sup>',
// not used - api etc. Added to suppress warnings
'menu.term_set' => '',
'menu.wifi_connstatus' => '',
'menu.wifi_set' => '',
'menu.wifi_scan' => '',
'menu.network_set' => '',
'menu.system_set' => '',
'menu.write_defaults' => '',
'menu.restore_defaults' => '',
'menu.restore_hard' => '',
'menu.reset_screen' => '',
'menu.index' => '',
'menu.ini_export' => '',
'menu.ini_import' => '',
];

@ -0,0 +1,291 @@
<?php
return [
'menu.cfg_wifi' => 'Nastavení WiFi',
'menu.cfg_network' => 'Nastavení sítě',
'menu.cfg_term' => 'Nastavení terminalu',
'menu.about' => 'O programu',
'menu.help' => 'Nápověda',
'menu.term' => 'Zpět k terminálu',
'menu.cfg_system' => 'Nastavení systému',
'menu.cfg_wifi_conn' => 'Připojování',
'menu.settings' => 'Nastavení',
// Terminal page
'title.term' => 'Terminál', // page title of the terminal page
'term_nav.fullscreen' => 'Celá obr.',
'term_nav.config' => 'Nastavení',
'term_nav.wifi' => 'WiFi',
'term_nav.help' => 'Nápověda',
'term_nav.about' => 'About',
'term_nav.paste' => 'Vložit',
'term_nav.upload' => 'Nahrát',
'term_nav.keybd' => 'Klávesnice',
'term_nav.paste_prompt' => 'Vložte text k~odeslání:',
'term_conn.connecting' => 'Připojuji se',
'term_conn.waiting_content' => 'Čekám na data',
'term_conn.disconnected' => 'Odpojen',
'term_conn.waiting_server' => 'Čekám na server',
'term_conn.reconnecting' => 'Obnova spojení',
// Terminal settings page
'term.defaults' => 'Výchozí nastavení',
'term.expert' => 'Pokročilé volby',
'term.explain_initials' => '
Tato nastavení jsou použita po spuštění a při resetu obrazovky
(příkaz RIS, <code>\ec</code>). Tyto volby lze měnit za běhu
pomocí řídicích sekvencí.
',
'term.explain_expert' => '
Interní parametry terminálu. Změnou časování lze dosáhnout kratší
latence a~rychlejšího překreslování, hodnoty záleží na konkrétní
aplikaci. Timeout parseru je čas do automatického zrušení započaté
řídicí sekvence.',
'term.example' => 'Náhled výchozích barev',
'term.explain_scheme' => '
Výchozí barvu textu a pozadí vyberete kliknutím na barvy v~paletě.
Dále lze použít ANSI barvy 0-255 a hex ve formátu #FFFFFF.
',
'term.fgbg_presets' => 'Předvolby výchozích<br>barev textu a pozadí',
'term.color_scheme' => 'Barevné schéma',
'term.reset_screen' => 'Resetovat obrazovku a parser',
'term.term_title' => 'Nadpis',
'term.term_width' => 'Šířka',
'term.term_height' => 'Výška',
'term.buttons' => 'Text tlačítek',
'term.theme' => 'Barevná paleta',
'term.cursor_shape' => 'Styl kurzoru',
'term.parser_tout_ms' => 'Timeout parseru',
'term.display_tout_ms' => 'Prodleva překreslení',
'term.display_cooldown_ms' => 'Min. čas překreslení',
'term.allow_decopt_12' => 'Povolit \e?12h/l',
'term.fn_alt_mode' => 'SS3 Fn klávesy',
'term.show_config_links' => 'Menu pod obrazovkou',
'term.show_buttons' => 'Zobrazit tlačítka',
'term.loopback' => 'Lokální echo (<span style="text-decoration:overline">SRM</span>)',
'term.crlf_mode' => 'Enter = CR+LF (LNM)',
'term.want_all_fn' => 'Zachytávat F5, F11, F12',
'term.button_msgs' => 'Reporty tlačítek<br>(dek. ASCII CSV)',
'term.color_fg' => 'Výchozí text',
'term.color_bg' => 'Výchozí pozadí',
'term.color_fg_prev' => 'Barva textu',
'term.color_bg_prev' => 'Barva pozadí',
'term.colors_preview' => '',
'term.debugbar' => 'Rozšířené ladění',
'term.ascii_debug' => 'Ladění vstupních dat',
'term.backdrop' => 'URL obrázku na pozadí',
'term.button_count' => 'Počet tlačítek',
'term.button_colors' => 'Barvy tlačítek',
'term.font_stack' => 'Font',
'term.font_size' => 'Velikost písma',
'cursor.block_blink' => 'Blok, blikající',
'cursor.block_steady' => 'Blok, stálý',
'cursor.underline_blink' => 'Podtržítko, blikající',
'cursor.underline_steady' => 'Podtržítko, stálé',
'cursor.bar_blink' => 'Svislice, blikající',
'cursor.bar_steady' => 'Svislice, stálá',
// Text upload dialog
'upload.title' => 'Upload textu',
'upload.prompt' => 'Načíst ze souboru:',
'upload.endings' => 'Konce řádku:',
'upload.endings.cr' => 'CR (klávesa Enter)',
'upload.endings.crlf' => 'CR LF (Windows)',
'upload.endings.lf' => 'LF (Linux)',
'upload.chunk_delay' => 'Prodleva (ms):',
'upload.chunk_size' => 'Délka úseku (0=řádek):',
'upload.progress' => 'Proběh:',
// Network config page
'net.explain_sta' => '
Odškrtněte "Použít dynamickou IP" pro nastavení statické IP adresy.',
'net.explain_ap' => '
Tato nastavení ovlivňují interní DHCP server v AP režimu (hotspot).',
'net.ap_dhcp_time' => 'Doba zapůjčení adresy',
'net.ap_dhcp_start' => 'Začátek IP poolu',
'net.ap_dhcp_end' => 'Konec IP poolu',
'net.ap_addr_ip' => 'Vlastní IP adresa',
'net.ap_addr_mask' => 'Maska podsítě',
'net.sta_dhcp_enable' => 'Použít dynamickou IP',
'net.sta_addr_ip' => 'Statická IP modulu',
'net.sta_addr_mask' => 'Maska podsítě',
'net.sta_addr_gw' => 'Gateway',
'net.ap' => 'DHCP server (AP)',
'net.sta' => 'DHCP klient',
'net.sta_mac' => 'MAC adresa klienta',
'net.ap_mac' => 'MAC adresa AP',
'net.details' => 'MAC adresy',
// Wifi config page
'wifi.ap' => 'WiFi hotspot',
'wifi.sta' => 'Připojení k~externí síti',
'wifi.enable' => 'Zapnuto',
'wifi.tpw' => 'Vysílací výkon',
'wifi.ap_channel' => 'WiFi kanál',
'wifi.ap_ssid' => 'Jméno hotspotu',
'wifi.ap_password' => 'Přístupové heslo',
'wifi.ap_hidden' => 'Skrýt síť',
'wifi.sta_info' => 'Zvolená síť',
'wifi.not_conn' => 'Nepřipojen.',
'wifi.sta_none' => 'Žádná',
'wifi.sta_active_pw' => '🔒 Uložené heslo',
'wifi.sta_active_nopw' => '🔓 Bez hesla',
'wifi.connected_ip_is' => 'Připojen, IP: ',
'wifi.sta_password' => 'Heslo:',
'wifi.scanning' => 'Hledám sítě',
'wifi.scan_now' => 'Klikněte pro vyhledání sítí!',
'wifi.cant_scan_no_sta' => 'Klikněte pro zapnutí režimu klienta a vyhledání sítí!',
'wifi.select_ssid' => 'Dostupné sítě:',
'wifi.enter_passwd' => 'Zadejte heslo pro ":ssid:"',
'wifi.sta_explain' => 'Vyberte síť a připojte se tlačítkem vpravo nahoře.',
// Wifi connecting status page
'wificonn.status' => 'Stav:',
'wificonn.back_to_config' => 'Zpět k~nastavení WiFi',
'wificonn.telemetry_lost' => 'Spojení bylo přerušeno; připojování selhalo, nebo jste byli odpojeni od sítě.',
'wificonn.explain_android_sucks' => '
Pokud ESPTerm konfigurujete pomocí mobilu nebo z~externí sítě, může se stát
že některé ze zařízení změní síť a~ukazatel průběhu přestane fungovat.
Počkejte ~15s a pak zkontrolujte, zda se připojení zdařilo.
',
'wificonn.explain_reset' => '
Interní hotspot lze kdykoliv vynutit podržením tlačítka BOOT, až modrá LED začne blikat.
Podržíte-li tlačítko déle (LED začne blikat rychleji), dojde k~obnovení do výchozích anstavení.',
'wificonn.disabled' => "Režim klienta není povolen.",
'wificonn.idle' => "Žádná IP adresa, připojování neprobíhá.",
'wificonn.success' => "Připijen! IP adresa je ",
'wificonn.working' => "Připojuji k zvolené síti",
'wificonn.fail' => "Připojení selhalo, zkontrolujte nastavení a~pokus opakujte. Důvod: ",
// Access restrictions form
'pwlock.title' => 'Omezení přístupu',
'pwlock.explain' => '
Části webového rozhraní lze chránit heslem. Nemáte-li v úmyslu heslo měnit,
do jeho políčka nic nevyplňujte.<br>
Výchozí přístupové heslo je "%def_access_pw%".
',
'pwlock.region' => 'Chránit heslem',
'pwlock.region.none' => 'Nic, vše volně přístupné',
'pwlock.region.settings_noterm' => 'Nastavení, mimo terminál',
'pwlock.region.settings' => 'Všechna nastavení',
'pwlock.region.menus' => 'Celá admin. sekce',
'pwlock.region.all' => 'Vše, včetně terminálu',
'pwlock.new_access_pw' => 'Nové přístupové heslo',
'pwlock.new_access_pw2' => 'Zopakujte nové heslo',
'pwlock.admin_pw' => 'Systémové heslo',
'pwlock.access_name' => 'Uživatelské jméno',
// Setting admin password
'adminpw.title' => 'Změna systémového hesla',
'adminpw.explain' =>
'
Systémové heslo slouží k úpravám uložených výchozích nastavení
a ke změně přístupových oprávnění.
Toto heslo je uloženo mimo ostatní data, obnovení do výchozách nastavení
na něj nemá vliv.
Toto heslo nelze jednoduše obnovit, v případě zapomenutí vymažte flash paměť a obnovte firmware.<br>
Vychozí systémové heslo je "%def_admin_pw%".
',
'adminpw.new_admin_pw' => 'Nové systémové heslo',
'adminpw.new_admin_pw2' => 'Zopakujte nové heslo',
'adminpw.old_admin_pw' => 'Původní systémové heslo',
// Persist form
'persist.title' => 'Záloha a~obnovení konfigurace',
'persist.explain' => '
Všechna nastavení jsou ukládána do flash paměti. V~paměti jsou
vyhrazené dva oddíly, aktivní nastavení a záloha. Zálohu lze přepsat
za použití systémového hesla, původní nastavení z ní pak můžete kdykoliv obnovit.
Pro obnovení ze zálohy stačí podržet tlačítko BOOT, až modrá LED začne rychle blikat.
',
'persist.confirm_restore' => 'Chcete obnovit všechna nastavení?',
'persist.confirm_restore_hard' =>
'Opravdu chcete načíst tovární nastavení? Všechna nastavení kromě zálohy a systémového hesla
budou přepsána, včetně nastavení WiFi!',
'persist.confirm_store_defaults' =>
'Zadejte systémové heslo pro přepsání zálohy aktuálními parametry.',
'persist.password' => 'Systémové heslo:',
'persist.restore_defaults' => 'Obnovit ze zálohy',
'persist.write_defaults' => 'Zálohovat aktuální nastavení',
'persist.restore_hard' => 'Načíst tovární nastavení',
'persist.restore_hard_explain' =>
'(Tímto vymažete nastavení WiFi! Záloha a systémové heslo zůstanou beze změny.)',
'backup.title' => 'Záloha do souboru',
'backup.explain' => 'Všechna nastavení kromě systémového hesla je možné uložit do a obnovit z INI souboru.',
'backup.export' => 'Zálohovat do souboru',
'backup.import' => 'Nahrát soubor!',
// UART settings form
'uart.title' => 'Sériový port',
'uart.explain' => '
Tímto formulářem můžete upravit nastavení komunikačního UARTu.
Ladicí výpisy jsou na pinu P2 s~pevnými parametry: 115200 baud, 1 stop bit, žádná parita.
',
'uart.baud' => 'Rychlost',
'uart.parity' => 'Parita',
'uart.parity.none' => 'Źádná',
'uart.parity.odd' => 'Lichá',
'uart.parity.even' => 'Sudá',
'uart.stop_bits' => 'Stop-bity',
'uart.stop_bits.one' => '1',
'uart.stop_bits.one_and_half' => '1.5',
'uart.stop_bits.two' => '2',
// HW tuning form
'hwtuning.title' => 'Tuning hardwaru',
'hwtuning.explain' => '
ESP8266 lze přetaktovat z~80~MHz na 160~MHz. Vyšší rychlost umožní rychlejší překreslování
obrazovky a stránky se budou načítat rychleji. Nevýhodou je vyšší spotřeba a citlivost k~rušení.
',
'hwtuning.overclock' => 'Přetaktovat na 160~MHz',
'gpio2_config' => 'Funkce GPIO2',
'gpio4_config' => 'Funkce GPIO4',
'gpio5_config' => 'Funkce GPIO5',
'gpio_config.off' => 'Vypnuto',
'gpio_config.off_2' => 'Debug UART Tx',
'gpio_config.out_initial0' => 'Výstup (výchozí stav 0)',
'gpio_config.out_initial1' => 'Výstup (výchozí stav 1)',
'gpio_config.in_pull' => 'Vstup (s pull-upem)',
'gpio_config.in_nopull' => 'Vstup (plovoucí)',
// Generic button / dialog labels
'apply' => 'Použít!',
'start' => 'Start',
'cancel' => 'Zrušit',
'enabled' => 'Zapnuto',
'disabled' => 'Vypnuto',
'yes' => 'Ano',
'no' => 'Ne',
'confirm' => 'OK',
'copy' => 'Kopírovat',
'form_errors' => 'Neplatné hodnoty:',
];

@ -0,0 +1,291 @@
<?php
return [
'menu.cfg_wifi' => 'WLAN-Einstellungen',
'menu.cfg_network' => 'Netzwerkeinstellungen',
'menu.cfg_term' => 'Terminaleinstellungen',
'menu.about' => 'Über ESPTerm',
'menu.help' => 'Schnellreferenz',
'menu.term' => 'Zurück zum Terminal',
'menu.cfg_system' => 'Systemeinstellungen',
'menu.cfg_wifi_conn' => 'Verbinden mit dem Netzwerk',
'menu.settings' => 'Einstellungen',
// Terminal page
'title.term' => 'Terminal', // page title of the terminal page
'term_nav.fullscreen' => 'Vollbild',
'term_nav.config' => 'Konfiguration',
'term_nav.wifi' => 'WLAN',
'term_nav.help' => 'Hilfe',
'term_nav.about' => 'Info',
'term_nav.paste' => 'Einfügen',
'term_nav.upload' => 'Hochladen',
'term_nav.keybd' => 'Tastatur',
'term_nav.paste_prompt' => 'Text einfügen zum Versenden:',
'term_conn.connecting' => 'Verbinden',
'term_conn.waiting_content' => 'Warten auf Inhalt',
'term_conn.disconnected' => 'Nicht verbunden',
'term_conn.waiting_server' => 'Warten auf Server',
'term_conn.reconnecting' => 'Verbinden',
// Terminal settings page
'term.defaults' => 'Anfangseinstellungen',
'term.expert' => 'Expertenoptionen',
'term.explain_initials' => '
Dies sind die Anfangseinstellungen, die benutzt werden, nachdem ESPTerm startet,
oder wenn der Bildschirm mit dem <code>\ec</code>-Kommando zurückgesetzt wird.
Sie können durch Escape-Sequenzen verändert werden.
',
'term.explain_expert' => '
Dies sind erweiterte Konfigurationsoptionen, die meistens nicht verändert
werden müssen. Bearbeite sie nur, wenn du weißt, was du tust.',
'term.example' => 'Standardfarbenvorschau',
'term.explain_scheme' => '
Um die Standardtextfarbe und Standardhintergrundfarbe auszuwählen, klicke auf
die Vorschaupalette, oder benutze die Zahlen 0-15 für die Themafarben, 16-255
für Standardfarben, oder Hexadezimal (#FFFFFF) für True Color (24-bit).
',
'term.fgbg_presets' => 'Voreinstellungen',
'term.color_scheme' => 'Farbschema',
'term.reset_screen' => 'Bildschirm & Parser zurücksetzen',
'term.term_title' => 'Titeltext',
'term.term_width' => 'Breite',
'term.term_height' => 'Höhe',
'term.buttons' => 'Tastentext',
'term.theme' => 'Farbthema',
'term.cursor_shape' => 'Cursorstil',
'term.parser_tout_ms' => 'Parser-Auszeit',
'term.display_tout_ms' => 'Zeichenverzögerung',
'term.display_cooldown_ms' => 'Zeichenabkühlzeit',
'term.allow_decopt_12' => '\e?12h/l erlauben',
'term.fn_alt_mode' => 'SS3 Fn-Tasten',
'term.show_config_links' => 'Links anzeigen',
'term.show_buttons' => 'Tasten anzeigen',
'term.loopback' => 'Lokales Echo (<span style="text-decoration:overline">SRM</span>)',
'term.crlf_mode' => 'Enter = CR+LF (LNM)',
'term.want_all_fn' => 'F5, F11, F12 erfassen',
'term.button_msgs' => 'Tastencodes<br>(ASCII, dec, CSV)',
'term.color_fg' => 'Standardvordergr.',
'term.color_bg' => 'Standardhintergr.',
'term.color_fg_prev' => 'Vordergrund',
'term.color_bg_prev' => 'Hintergrund',
'term.colors_preview' => '',
'term.debugbar' => 'Debug-Leiste anzeigen',
'term.ascii_debug' => 'Kontrollcodes anzeigen',
'term.backdrop' => 'Hintergrundbild-URL',
'term.button_count' => 'Tastenanzahl',
'term.button_colors' => 'Tastenfarben',
'term.font_stack' => 'Schriftstapel',
'term.font_size' => 'Schriftgröße',
'cursor.block_blink' => 'Block, blinkend',
'cursor.block_steady' => 'Block, ruhig',
'cursor.underline_blink' => 'Unterstrich, blinkend',
'cursor.underline_steady' => 'Unterstrich, ruhig',
'cursor.bar_blink' => 'Balken, blinkend',
'cursor.bar_steady' => 'Balken, ruhig',
// Text upload dialog
'upload.title' => 'Text Hochladen',
'upload.prompt' => 'Eine Textdatei laden:',
'upload.endings' => 'Zeilenumbruch:',
'upload.endings.cr' => 'CR (Enter-Taste)',
'upload.endings.crlf' => 'CR LF (Windows)',
'upload.endings.lf' => 'LF (Linux)',
'upload.chunk_delay' => 'Datenblockverzögerung (ms):',
'upload.chunk_size' => 'Datenblockgröße (0=Linie):',
'upload.progress' => 'Hochladen:',
// Network config page
'net.explain_sta' => '
Schalte Dynamische IP aus um die statische IP-Addresse zu konfigurieren.',
'net.explain_ap' => '
Diese Einstellungen beeinflussen den eingebauten DHCP-Server im AP-Modus.',
'net.ap_dhcp_time' => 'Leasezeit',
'net.ap_dhcp_start' => 'Pool Start-IP',
'net.ap_dhcp_end' => 'Pool End-IP',
'net.ap_addr_ip' => 'Eigene IP-Addresse',
'net.ap_addr_mask' => 'Subnet-Maske',
'net.sta_dhcp_enable' => 'Dynamische IP',
'net.sta_addr_ip' => 'ESPTerm statische IP',
'net.sta_addr_mask' => 'Subnet-Maske',
'net.sta_addr_gw' => 'Gateway-IP',
'net.ap' => 'DHCP Server (AP)',
'net.sta' => 'DHCP Client (Station)',
'net.sta_mac' => 'Station MAC',
'net.ap_mac' => 'AP MAC',
'net.details' => 'MAC-Addressen',
// Wifi config page
'wifi.ap' => 'Eingebauter Access Point',
'wifi.sta' => 'Bestehendes Netzwerk beitreten',
'wifi.enable' => 'Aktiviert',
'wifi.tpw' => 'Sendeleistung',
'wifi.ap_channel' => 'Kanal',
'wifi.ap_ssid' => 'AP SSID',
'wifi.ap_password' => 'Passwort',
'wifi.ap_hidden' => 'SSID verbergen',
'wifi.sta_info' => 'Ausgewählt',
'wifi.not_conn' => 'Nicht verbunden.',
'wifi.sta_none' => 'Keine',
'wifi.sta_active_pw' => '🔒 Passwort gespeichert',
'wifi.sta_active_nopw' => '🔓 Offen',
'wifi.connected_ip_is' => 'Verbunden, IP ist ',
'wifi.sta_password' => 'Passwort:',
'wifi.scanning' => 'Scannen',
'wifi.scan_now' => 'Klicke hier um zu scannen!',
'wifi.cant_scan_no_sta' => 'Klicke hier um Client-Modus zu aktivieren und zu scannen!',
'wifi.select_ssid' => 'Verfügbare Netzwerke:',
'wifi.enter_passwd' => 'Passwort für ":ssid:"',
'wifi.sta_explain' =>
'Nach dem Auswählen eines Netzwerks, drücke Bestätigen, um dich zu verbinden.',
// Wifi connecting status page
'wificonn.status' => 'Status:',
'wificonn.back_to_config' => 'Zurück zur WLAN-Konfiguration',
'wificonn.telemetry_lost' => 'Telemetrie verloren; etwas lief schief, oder dein Gerät wurde getrennt.',
'wificonn.explain_android_sucks' => '
Wenn du gerade ESPTerm mit einem Handy oder über ein anderes externes Netzwerk
konfigurierst, kann dein Gerät die Verbindung verlieren und diese Fortschrittsanzeige
wird nicht funktionieren. Bitte warte eine Weile (etwa 15 Sekunden) und prüfe dann,
ob die Verbindung gelangen ist.',
'wificonn.explain_reset' => '
Um den eingebauten AP zur Aktivierung zu zwingen, halte den BOOT-Knopf gedrückt bis die
blaue LED beginnt, zu blinken. Halte ihn länger gedrückt (bis die LED schnell blinkt)
um eine "Werksrückstellung" zu vollziehen.',
'wificonn.disabled' => "Stationsmodus ist deaktiviert.",
'wificonn.idle' => "Nicht verbunden und ohne IP.",
'wificonn.success' => "Verbunden! Empfangene IP: ",
'wificonn.working' => "Verbinden mit dem ausgewählten AP",
'wificonn.fail' => "Verbindung fehlgeschlagen; prüfe die Einstellungen und versuche es erneut. Grund: ",
// Access restrictions form
'pwlock.title' => 'Zugriffsbeschränkungen',
'pwlock.explain' => '
Manche, oder alle Teile des Web-Interface können mit einem Passwort geschützt werden.
Lass die Passwortfelder leer wenn du es nicht verändern möchtest.<br>
Das voreingestellte Passwort ist "%def_access_pw%".',
'pwlock.region' => 'Geschützte Seiten',
'pwlock.region.none' => 'Keine, alles offen',
'pwlock.region.settings_noterm' => 'WLAN-, Netzwerk- & Systemeinstellungen',
'pwlock.region.settings' => 'Alle Einstellungsseiten',
'pwlock.region.menus' => 'Dieser ganze Menüabschnitt',
'pwlock.region.all' => 'Alles, sogar das Terminal',
'pwlock.new_access_pw' => 'Neues Passwort',
'pwlock.new_access_pw2' => 'Wiederholen',
'pwlock.admin_pw' => 'Systempasswort',
'pwlock.access_name' => 'Benutzername',
// Setting admin password
'adminpw.title' => 'Systempasswort ändern',
'adminpw.explain' =>'
Das "Systempasswort" wird benutzt, um die gespeicherten Standardeinstellungen
und die Zugriffsbeschränkungen zu verändern. Dieses Passwort wird nicht als Teil
der Hauptkonfiguration gespeichert, d.h. Speichern / Wiederherstellen wird das
Passwort nicht beeinflussen. Wenn das Systempasswort vergessen wird, ist
die einfachste Weise, wieder Zugriff zu erhalten, ein Re-flash des Chips.<br>
Das voreingestellte Systempasswort ist "%def_admin_pw%".
',
'adminpw.new_admin_pw' => 'Neues Systempasswort',
'adminpw.new_admin_pw2' => 'Wiederholen',
'adminpw.old_admin_pw' => 'Altes Systempasswort',
// Persist form
'persist.title' => 'Speichern & Wiederherstellen',
'persist.explain' => '
ESPTerm speichert alle Einstellungen im Flash-Speicher. Die aktiven Einstellungen
können in den “Voreinstellungsbereich” kopiert werden und später wiederhergestellt
werden mit der Taste unten.',
'persist.confirm_restore' => 'Alle Einstellungen zu den Voreinstellungen zurücksetzen?',
'persist.confirm_restore_hard' => '
Zurücksetzen zu den Firmware-Voreinstellungen? Dies wird alle aktiven
Einstellungen zürucksetzen und den AP-Modus aktivieren mit der Standard-SSID.',
'persist.confirm_store_defaults' =>
'Systempasswort eingeben um Voreinstellungen zu überschreiben',
'persist.password' => 'Systempasswort:',
'persist.restore_defaults' => 'Zu gespeicherten Voreinstellungen zurücksetzen',
'persist.write_defaults' => 'Aktive Einstellungen als Voreinstellungen speichern',
'persist.restore_hard' => 'Aktive Einstellungen zu Werkseinstellungen zurücksetzen',
'persist.restore_hard_explain' => '
(Dies löscht die WLAN-Konfiguration! Beeinflusst die gespeicherten Voreinstellungen
oder das Systempasswort nicht.)',
'backup.title' => 'Konfigurationsdatei sichern',
'backup.explain' => 'Die ganze Konfiguration außer dem Systempasswort können mit einer INI-Datei gesichert und wiederhergestellt werden.',
'backup.export' => 'Datei exportieren',
'backup.import' => 'Importieren!',
// UART settings form
'uart.title' => 'Serieller Port Parameter',
'uart.explain' => '
Dies steuert den Kommunikations-UART. Der Debug-UART ist auf 115.200 baud fest
eingestellt mit einem Stop-Bit und keiner Parität.
',
'uart.baud' => 'Baudrate',
'uart.parity' => 'Parität',
'uart.parity.none' => 'Keine',
'uart.parity.odd' => 'Ungerade',
'uart.parity.even' => 'Gerade',
'uart.stop_bits' => 'Stop-Bits',
'uart.stop_bits.one' => 'Eins',
'uart.stop_bits.one_and_half' => 'Eineinhalb',
'uart.stop_bits.two' => 'Zwei',
// HW tuning form
'hwtuning.title' => 'Hardware-Tuning',
'hwtuning.explain' => '
Der ESP8266 kann von 80&nbsp;MHz auf 160&nbsp;MHz übertaktet werden.
Alles wird etwas schneller sein, aber mit höherem Stromverbrauch,
und eventuell auch mit mehr Interferenz.
Mit Sorgfalt benutzen.
',
'hwtuning.overclock' => 'Auf 160MHz übertakten',
'gpio2_config' => 'GPIO2 Funktion',
'gpio4_config' => 'GPIO4 Funktion',
'gpio5_config' => 'GPIO5 Funktion',
'gpio_config.off' => 'Deaktiviert',
'gpio_config.off_2' => 'UART Tx Debuggen',
'gpio_config.out_initial0' => 'Output (Anfangslevel 0)',
'gpio_config.out_initial1' => 'Output (Anfangslevel 1)',
'gpio_config.in_pull' => 'Input (pull-up)',
'gpio_config.in_nopull' => 'Input (floating)',
// Generic button / dialog labels
'apply' => 'Bestätigen!',
'start' => 'Starten',
'cancel' => 'Abbrechen',
'enabled' => 'Aktiviert',
'disabled' => 'Deaktiviert',
'yes' => 'Ja',
'no' => 'Nein',
'confirm' => 'OK',
'copy' => 'Kopieren',
'form_errors' => 'Gültigkeitsfehler für:',
];

@ -1,9 +1,6 @@
<?php
return [
'appname' => 'ESPTerm',
'appname_demo' => 'ESPTerm<sup> DEMO</sup>',
'menu.cfg_wifi' => 'WiFi Settings',
'menu.cfg_network' => 'Network Settings',
'menu.cfg_term' => 'Terminal Settings',
@ -12,11 +9,13 @@ return [
'menu.term' => 'Back to Terminal',
'menu.cfg_system' => 'System Settings',
'menu.cfg_wifi_conn' => 'Connecting to Network',
'menu.settings' => 'Settings',
'title.term' => 'Terminal',
// Terminal page
'title.term' => 'Terminal', // page title of the terminal page
'term_nav.fullscreen' => 'Fullscreen',
'term_nav.config' => 'Config',
'term_nav.wifi' => 'WiFi',
'term_nav.help' => 'Help',
@ -26,18 +25,20 @@ return [
'term_nav.keybd' => 'Keyboard',
'term_nav.paste_prompt' => 'Paste text to send:',
'net.ap' => 'DHCP Server (AP)',
'net.sta' => 'DHCP Client (Station)',
'net.sta_mac' => 'Station MAC',
'net.ap_mac' => 'AP MAC',
'net.details' => 'MAC addresses',
'term_conn.connecting' => 'Connecting',
'term_conn.waiting_content' => 'Waiting for content',
'term_conn.disconnected' => 'Disconnected',
'term_conn.waiting_server' => 'Waiting for server',
'term_conn.reconnecting' => 'Reconnecting',
// Terminal settings page
'term.defaults' => 'Initial Settings',
'term.expert' => 'Expert Options',
'term.explain_initials' => '
Those are the initial settings used after ESPTerm powers on or when the screen
reset command is received. Some options can be changed by the application via escape sequences,
those changes won\'t be saved in Flash.
Those are the initial settings used after ESPTerm powers on,
or when the screen reset command is received (<code>\ec</code>).
They can be changed by the terminal application using escape sequences.
',
'term.explain_expert' => '
Those are advanced config options that usually don\'t need to be changed.
@ -45,22 +46,44 @@ return [
'term.example' => 'Default colors preview',
'term.explain_scheme' => '
To select default text and background color, click on the
preview palette. Alternatively, use numbers 0-15 for theme colors,
16-255 for standard colors and hex (#FFFFFF) for True Color (24-bit).
',
'term.fgbg_presets' => 'Defaults Presets',
'term.color_scheme' => 'Color Scheme',
'term.reset_screen' => 'Reset screen & parser',
'term.term_title' => 'Header text',
'term.term_width' => 'Width / height',
'term.default_fg_bg' => 'Text / background',
'term.buttons' => 'Button labels',
'term.theme' => 'Color scheme',
'term.cursor_shape' => 'Cursor style',
'term.parser_tout_ms' => 'Parser timeout',
'term.display_tout_ms' => 'Redraw delay',
'term.display_cooldown_ms' => 'Redraw cooldown',
'term.term_title' => 'Header Text',
'term.term_width' => 'Width',
'term.term_height' => 'Height',
'term.buttons' => 'Button Labels',
'term.theme' => 'Color Palette',
'term.cursor_shape' => 'Cursor Style',
'term.parser_tout_ms' => 'Parser Timeout',
'term.display_tout_ms' => 'Redraw Delay',
'term.display_cooldown_ms' => 'Redraw Cooldown',
'term.allow_decopt_12' => 'Allow \e?12h/l',
'term.fn_alt_mode' => 'SS3 Fn keys',
'term.show_config_links' => 'Show nav links',
'term.show_buttons' => 'Show buttons',
'term.loopback' => 'Local Echo',
'term.crlf_mode' => 'Enter sends CR+LF',
'term.loopback' => 'Local Echo (<span style="text-decoration:overline">SRM</span>)',
'term.crlf_mode' => 'Enter = CR+LF (LNM)',
'term.want_all_fn' => 'Capture F5, F11, F12',
'term.button_msgs' => 'Button codes<br>(ASCII, dec, CSV)',
'term.color_fg' => 'Default Fg.',
'term.color_bg' => 'Default Bg.',
'term.color_fg_prev' => 'Foreground',
'term.color_bg_prev' => 'Background',
'term.colors_preview' => '',
'term.debugbar' => 'Debug internal state',
'term.ascii_debug' => 'Display control codes',
'term.backdrop' => 'Background image URL',
'term.button_count' => 'Button count',
'term.button_colors' => 'Button colors',
'term.font_stack' => 'Font stack',
'term.font_size' => 'Font size',
'cursor.block_blink' => 'Block, blinking',
'cursor.block_steady' => 'Block, steady',
@ -69,23 +92,19 @@ return [
'cursor.bar_blink' => 'I-bar, blinking',
'cursor.bar_steady' => 'I-bar, steady',
// terminal color labels
'color.0' => 'Black',
'color.1' => 'Red',
'color.2' => 'Green',
'color.3' => 'Yellow',
'color.4' => 'Blue',
'color.5' => 'Purple',
'color.6' => 'Cyan',
'color.7' => 'Silver',
'color.8' => 'Gray',
'color.9' => 'Light Red',
'color.10' => 'Light Green',
'color.11' => 'Light Yellow',
'color.12' => 'Light Blue',
'color.13' => 'Light Purple',
'color.14' => 'Light Cyan',
'color.15' => 'White',
// Text upload dialog
'upload.title' => 'Text Upload',
'upload.prompt' => 'Load a text file:',
'upload.endings' => 'Line endings:',
'upload.endings.cr' => 'CR (Enter key)',
'upload.endings.crlf' => 'CR LF (Windows)',
'upload.endings.lf' => 'LF (Linux)',
'upload.chunk_delay' => 'Chunk delay (ms):',
'upload.chunk_size' => 'Chunk size (0=line):',
'upload.progress' => 'Upload:',
// Network config page
'net.explain_sta' => '
Switch off Dynamic IP to configure the static IP address.',
@ -104,6 +123,14 @@ return [
'net.sta_addr_mask' => 'Subnet mask',
'net.sta_addr_gw' => 'Gateway IP',
'net.ap' => 'DHCP Server (AP)',
'net.sta' => 'DHCP Client (Station)',
'net.sta_mac' => 'Station MAC',
'net.ap_mac' => 'AP MAC',
'net.details' => 'MAC addresses',
// Wifi config page
'wifi.ap' => 'Built-in Access Point',
'wifi.sta' => 'Join Existing Network',
@ -129,45 +156,94 @@ return [
'wifi.enter_passwd' => 'Enter password for ":ssid:"',
'wifi.sta_explain' => 'After selecting a network, press Apply to connect.',
'wifi.conn.status' => 'Status:',
'wifi.conn.back_to_config' => 'Back to WiFi config',
'wifi.conn.telemetry_lost' => 'Telemetry lost; something went wrong, or your device disconnected.',
'wifi.conn.explain_android_sucks' => '
// Wifi connecting status page
'wificonn.status' => 'Status:',
'wificonn.back_to_config' => 'Back to WiFi config',
'wificonn.telemetry_lost' => 'Telemetry lost; something went wrong, or your device disconnected.',
'wificonn.explain_android_sucks' => '
If you\'re configuring ESPTerm via a smartphone, or were connected
from another external network, your device may lose connection and this
progress indicator won\'t work. Please wait a while (~ 15 seconds),
from another external network, your device may lose connection and
this progress indicator won\'t work. Please wait a while (~ 15 seconds),
then check if the connection succeeded.',
'wifi.conn.explain_reset' => '
To force enable the built-in AP, hold the BOOT
button until the blue LED starts flashing. Hold the button longer (until the LED
flashes rapidly) for a "factory reset".',
'wificonn.explain_reset' => '
To force enable the built-in AP, hold the BOOT button until the blue LED
starts flashing. Hold the button longer (until the LED flashes rapidly)
for a "factory reset".',
'wificonn.disabled' =>"Station mode is disabled.",
'wificonn.idle' =>"Idle, not connected and has no IP.",
'wificonn.success' => "Connected! Received IP ",
'wificonn.working' => "Connecting to selected AP",
'wificonn.fail' => "Connection failed, check settings & try again. Cause: ",
// Access restrictions form
'pwlock.title' => 'Access Restrictions',
'pwlock.explain' => '
Some parts, or all of the web interface can be protected by a password prompt.
Leave the new password fields empty if you do not wish to change it.<br>
The default password is "%def_access_pw%".
',
'pwlock.region' => 'Protected pages',
'pwlock.region.none' => 'None, all open',
'pwlock.region.settings_noterm' => 'WiFi, Net & System settings',
'pwlock.region.settings' => 'All settings pages',
'pwlock.region.menus' => 'This entire menu section',
'pwlock.region.all' => 'Everything, even terminal',
'pwlock.new_access_pw' => 'New password',
'pwlock.new_access_pw2' => 'Repeat',
'pwlock.admin_pw' => 'Admin password',
'pwlock.access_name' => 'Username',
// Setting admin password
'wifi.conn.disabled' =>"Station mode is disabled.",
'wifi.conn.idle' =>"Idle, not connected and has no IP.",
'wifi.conn.success' => "Connected! Received IP ",
'wifi.conn.working' => "Connecting to selected AP",
'wifi.conn.fail' => "Connection failed, check settings & try again. Cause: ",
'adminpw.title' => 'Change Admin Password',
'adminpw.explain' =>
'
The "admin password" is used to manipulate the stored default settings
and to change access restrictions. This password is not saved as part
of the main config, i.e. using save / restore does not affect this
password. When the admin password is forgotten, the easiest way to
re-gain access is to wipe and re-flash the chip.<br>
The default admin password is "%def_admin_pw%".
',
'adminpw.new_admin_pw' => 'New admin password',
'adminpw.new_admin_pw2' => 'Repeat',
'adminpw.old_admin_pw' => 'Old admin password',
'system.save_restore' => 'Save & Restore',
'system.confirm_restore' => 'Restore all settings to their default values?',
'system.confirm_restore_hard' =>
// Persist form
'persist.title' => 'Save & Restore',
'persist.explain' => '
ESPTerm saves all settings in Flash. The active settings can be copied to
the "defaults area" and restored later using the blue button below.
',
'persist.confirm_restore' => 'Restore all settings to their default values?',
'persist.confirm_restore_hard' =>
'Restore to firmware default settings? This will reset ' .
'all active settings and switch to AP mode with the default SSID.',
'system.confirm_store_defaults' =>
'Enter admin password to confirm you want to store the current settings as defaults.',
'system.password' => 'Admin password:',
'system.restore_defaults' => 'Reset active settings to defaults',
'system.write_defaults' => 'Save active settings as defaults',
'system.restore_hard' => 'Reset active settings to firmware defaults',
'system.explain_persist' => '
ESPTerm contains two persistent memory banks, one for default and
one for active settings. Active settings can be stored as defaults
by the administrator (password required).
',
'system.uart' => 'Serial Port',
'system.explain_uart' => '
This form controls the primary, communication UART. The debug UART is fixed at 115.200 baud, one stop-bit and no parity.
'persist.confirm_store_defaults' =>
'Enter admin password to confirm you want to overwrite the default settings.',
'persist.password' => 'Admin password:',
'persist.restore_defaults' => 'Reset to saved defaults',
'persist.write_defaults' => 'Save active settings as defaults',
'persist.restore_hard' => 'Reset active settings to factory defaults',
'persist.restore_hard_explain' =>
'(This clears the WiFi config! Does not affect saved defaults or admin password.)',
'backup.title' => 'Back-up Config File',
'backup.explain' => 'All config except the admin password can be backed up and restored using an INI file.',
'backup.export' => 'Export to file',
'backup.import' => 'Import!',
// UART settings form
'uart.title' => 'Serial Port Parameters',
'uart.explain' => '
This form controls the communication UART. The debug UART is fixed
at 115.200 baud, one stop-bit and no parity.
',
'uart.baud' => 'Baud rate',
'uart.parity' => 'Parity',
@ -179,11 +255,37 @@ return [
'uart.stop_bits.one_and_half' => 'One and half',
'uart.stop_bits.two' => 'Two',
// HW tuning form
'hwtuning.title' => 'Hardware Tuning',
'hwtuning.explain' => '
ESP8266 can be overclocked from 80&nbsp;MHz to 160&nbsp;MHz. This will make
it more responsive and allow faster screen updates at the expense of slightly
higher power consumption. This can also make it more susceptible to interference.
Use with care.
',
'hwtuning.overclock' => 'Overclock to 160MHz',
'gpio2_config' => 'GPIO2 function',
'gpio4_config' => 'GPIO4 function',
'gpio5_config' => 'GPIO5 function',
'gpio_config.off' => 'Disabled',
'gpio_config.off_2' => 'Debug UART Tx',
'gpio_config.out_initial0' => 'Output (initial 0)',
'gpio_config.out_initial1' => 'Output (initial 1)',
'gpio_config.in_pull' => 'Input (pull-up)',
'gpio_config.in_nopull' => 'Input (floating)',
// Generic button / dialog labels
'apply' => 'Apply!',
'start' => 'Start',
'cancel' => 'Cancel',
'enabled' => 'Enabled',
'disabled' => 'Disabled',
'yes' => 'Yes',
'no' => 'No',
'confirm' => 'OK',
'copy' => 'Copy',
'form_errors' => 'Validation errors for:',
];

@ -0,0 +1,292 @@
<?php
return [
'menu.cfg_wifi' => 'WiFi Beállítások',
'menu.cfg_network' => 'Hálózati beállítások',
'menu.cfg_term' => 'Terminál beállítások',
'menu.about' => 'Az ESPTerm-ről',
'menu.help' => 'Gyors referencia',
'menu.term' => 'Vissza a terminálba',
'menu.cfg_system' => 'Rendszer beállítások',
'menu.cfg_wifi_conn' => 'Csatlakozás a hálózathoz',
'menu.settings' => 'Beállítások',
// Terminal page
'title.term' => 'Terminál', // page title of the terminal page
'term_nav.fullscreen' => 'Teljesképernyő',
'term_nav.config' => 'Beállítás',
'term_nav.wifi' => 'WiFi',
'term_nav.help' => 'Segítség',
'term_nav.about' => 'Info',
'term_nav.paste' => 'Beillesztés',
'term_nav.upload' => 'Feltöltés',
'term_nav.keybd' => 'Billentyűzet',
'term_nav.paste_prompt' => 'Szöveg beillesztése és küldése:',
'term_conn.connecting' => 'Csatlakozás',
'term_conn.waiting_content' => 'Várakozás a csatlakozásra',
'term_conn.disconnected' => 'Kapcsolat bontva',
'term_conn.waiting_server' => 'Várakozás a kiszolgálóra',
'term_conn.reconnecting' => 'Újracsatlakozás',
// Terminal settings page
'term.defaults' => 'Alap beállítások',
'term.expert' => 'Haladó beállítások',
'term.explain_initials' => '
Ezek az alap beállítások amik az ESPTerm bekapcsolása után,
vagy amikor képernyő reset parancsa érkezikd (<code>\ec</code>).
Ezek megváltoztathatóak egy terminál alkalmzás és escape szekveciák segítségével.
',
'term.explain_expert' => '
Ezek haladó beállítási opciók amiket általában nem kell megváltoztatni.
Csak akkor változtass rajta ha tudod mit csinálsz!',
'term.example' => 'Alapértelmezet színek előnézete',
'term.explain_scheme' => '
Az alapértelmezett szöveg és háttér szín kiválasztásához kattints a
paletta előnézet gombra. Alternatíva: használd a 0-15 számokat a téma színekhez,
16-255 számokat a normál színekhez és hexa (#FFFFFF) a True Color (24-bit) színekhez.
',
'term.fgbg_presets' => 'Alapértelmezett beállítások',
'term.color_scheme' => 'Szín séma',
'term.reset_screen' => 'A képernyő olvasó alapállapotba állítása',
'term.term_title' => 'Fejléc szöveg',
'term.term_width' => 'Szélesség',
'term.term_height' => 'Magasség',
'term.buttons' => 'Gomb cimkék',
'term.theme' => 'Szín paletta',
'term.cursor_shape' => 'Kurzor stílus',
'term.parser_tout_ms' => 'Olvasó időtúllépés',
'term.display_tout_ms' => 'Újrarajzolás késleltetése',
'term.display_cooldown_ms' => 'Újrarajzolás cooldown',
'term.allow_decopt_12' => '\e?12h/l engedélyezés',
'term.fn_alt_mode' => 'SS3 Fn gombok',
'term.show_config_links' => 'Navigációs linkek mutatása',
'term.show_buttons' => 'Gombok mutatása',
'term.loopback' => 'Helyi visszajelzés (<span style="text-decoration:overline">SRM</span>)',
'term.crlf_mode' => 'Enter = CR+LF (LNM)',
'term.want_all_fn' => 'F5, F11, F12 elfogása',
'term.button_msgs' => 'Gomb kódok<br>(ASCII, dec, CSV)',
'term.color_fg' => 'Alap előtér.',
'term.color_bg' => 'Alap háttér',
'term.color_fg_prev' => 'Előtér',
'term.color_bg_prev' => 'Háttér',
'term.colors_preview' => '',
'term.debugbar' => 'Belső állapot hibakeresés',
'term.ascii_debug' => 'Kontroll kódok mutatása',
'term.backdrop' => 'Háttérkép URL.je',
'term.button_count' => 'Gomb szám',
'term.button_colors' => 'Gomb színek',
'term.font_stack' => 'Betű típus',
'term.font_size' => 'Betű méret',
'cursor.block_blink' => 'Blokk, villog',
'cursor.block_steady' => 'Blokk, fix',
'cursor.underline_blink' => 'Aláhúzás, villog',
'cursor.underline_steady' => 'Aláhúzás, fix',
'cursor.bar_blink' => 'I, villog',
'cursor.bar_steady' => 'I, fix',
// Text upload dialog
'upload.title' => 'Szöveg feltöltése',
'upload.prompt' => 'Szöveg fájl betöltése:',
'upload.endings' => 'Sor vége:',
'upload.endings.cr' => 'CR (Enter gomb)',
'upload.endings.crlf' => 'CR LF (Windows)',
'upload.endings.lf' => 'LF (Linux)',
'upload.chunk_delay' => 'Chunk késleltetés (ms):',
'upload.chunk_size' => 'Chunk méret (0=line):',
'upload.progress' => 'Feltöltés:',
// Network config page
'net.explain_sta' => '
Kapcsold ki a dinamikus IP címet a statikus cím beállításához.',
'net.explain_ap' => '
Ezek a beállítások a beépített DHCP szervet és az AP módot befolyásolják.',
'net.ap_dhcp_time' => 'Lízing idő',
'net.ap_dhcp_start' => 'Kezdő IP cím',
'net.ap_dhcp_end' => 'Záró IP cím',
'net.ap_addr_ip' => 'Saját IP cím',
'net.ap_addr_mask' => 'Hálózati maszk',
'net.sta_dhcp_enable' => 'Dinamikus IP cím használata',
'net.sta_addr_ip' => 'ESPTerm statikus IP címe',
'net.sta_addr_mask' => 'Hálózati maszk',
'net.sta_addr_gw' => 'Útválasztó IP címe',
'net.ap' => 'DHCP Szerver (AP)',
'net.sta' => 'DHCP Kliens (Station)',
'net.sta_mac' => 'Állomás MAC címe',
'net.ap_mac' => 'AP MAC címe',
'net.details' => 'MAC címek',
// Wifi config page
'wifi.ap' => 'Beépített Access Point',
'wifi.sta' => 'Kapcsolódás létező hálózathoz',
'wifi.enable' => 'Engedélyezve',
'wifi.tpw' => 'Adás teljesítmény',
'wifi.ap_channel' => 'Csatorna',
'wifi.ap_ssid' => 'AP SSID',
'wifi.ap_password' => 'Jelszó',
'wifi.ap_hidden' => 'SSID rejtése',
'wifi.sta_info' => 'Kiválasztott',
'wifi.not_conn' => 'Nincs csatlkoztatva.',
'wifi.sta_none' => 'Egyiksem',
'wifi.sta_active_pw' => '🔒 Jelszó elmentve',
'wifi.sta_active_nopw' => '🔓 Szabad hozzáférés',
'wifi.connected_ip_is' => 'Csatlakozva, az IP cím ',
'wifi.sta_password' => 'Jelszó:',
'wifi.scanning' => 'Keresés',
'wifi.scan_now' => 'Kattints a keresés indításához!',
'wifi.cant_scan_no_sta' => 'Kattints a kliens mód engedélyezéséhez és a keresés indításához!',
'wifi.select_ssid' => 'Elérhető hálózatok:',
'wifi.enter_passwd' => 'Jelszó a(z) ":ssid:" hálózathoz',
'wifi.sta_explain' => 'A hálózat kiválasztása után nyomdj meg az Alkamaz gombot a csatlakozáshoz.',
// Wifi connecting status page
'wificonn.status' => 'Státusz:',
'wificonn.back_to_config' => 'Vissza a WiFi beállításhoz',
'wificonn.telemetry_lost' => 'Telemetria megszakadt; valami hiba történt, vagy az eszközöd elvesztette a kapcsolatot.',
'wificonn.explain_android_sucks' => '
Ha okostelefonon kapcsolódsz az ESPTerm-hez, vagy amikor csatlakozol
egy másik hálózatról, az eszközöd elveszítheti a kapcsolatot és
ez az indikátor nem fog működni. Kérlek várj egy keveset (~ 15 másodpercet),
és ellenőrizd, hogy a kapcsolat helyrejött-e.',
'wificonn.explain_reset' => '
Az beépített AP engedélyezéséhez tarts lenyomva a BOOT gombot amíg a kék led
villogni nem kezd. Tartsd addig lenyomva amíg a led el nem kezd gyorsan villogni
a gyári alapállapot visszaállításához".',
'wificonn.disabled' =>"Station mode letiltva.",
'wificonn.idle' =>"Alapállapot, nincs csatlakozva és nincs IP címe.",
'wificonn.success' => "Csatlakozva! Kaptam IP címet",
'wificonn.working' => "Csatlakozás a beállított AP-hez",
'wificonn.fail' => "Csatlakozás nem sikerült, ellenőrizd a beállítások és próbáld újra. A hibaok: ",
// Access restrictions form
'pwlock.title' => 'Hozzáférés korlátozása',
'pwlock.explain' => '
A web interfész néhany része vagy a teljes interfész jelszavas védelemmel látható el.
Hagyd a jelszó mezőt üresen ha nem akarod megváltoztatni.<br>
Az alapértelmezett jelszó "%def_access_pw%".
',
'pwlock.region' => 'Védett oldalak',
'pwlock.region.none' => 'Egyiksem, minden hozzáférhető',
'pwlock.region.settings_noterm' => 'WiFi, Hálózat és Rendszer beállítások',
'pwlock.region.settings' => 'Minden beállítás oldal',
'pwlock.region.menus' => 'Ez a teljes menű rész',
'pwlock.region.all' => 'Minden, még a terminál is',
'pwlock.new_access_pw' => 'Új jelszó',
'pwlock.new_access_pw2' => 'Jelszó ismét',
'pwlock.admin_pw' => 'Admin jelszó',
'pwlock.access_name' => 'Felhasználó név',
// Setting admin password
'adminpw.title' => 'Admin jelszó megváltoztatása',
'adminpw.explain' =>
'
Az "admin jelszo" a tárolt alap beállítások módosításához és a hozzáférések
változtatásához kell. Ez a jelszó nincs a többi beállítással egy helyre mentve,
tehát a mentés és visszaállítás műveletek nem befolyásolják.
Ha az admin jelszó elveszik akkor a legegyszerűbb módja a hozzáférés
visszaszerzésére a chip újraflashselésere.<br>
Az alap jelszó: "%def_admin_pw%".
',
'adminpw.new_admin_pw' => 'Új admin jelszó',
'adminpw.new_admin_pw2' => 'Jelszó ismét',
'adminpw.old_admin_pw' => 'Régi admin jelszó',
// Persist form
'persist.title' => 'Mentés & Visszaállítás',
'persist.explain' => '
ESPTerm az összes beállítást Flash-be menti. Az aktív beállítások at lehet másolni
a "alapértelmezett" területre és az később a lenti kék gombbal visszaállítható.
',
'persist.confirm_restore' => 'Minden beállítást visszaállítasz az "alap" értékre?',
'persist.confirm_restore_hard' =>
'Visszaállítod a rendszer alap beállításait? Ez minden aktív ' .
'beállítást törölni fog és AP módban az alap SSID-vel for újraindulni.',
'persist.confirm_store_defaults' =>
'Add meg az admin jelszót az alapállapotba állítás megerősítéshez.',
'persist.password' => 'Admin jelszó:',
'persist.restore_defaults' => 'Mentett beállítások visszaállítása',
'persist.write_defaults' => 'Aktív beállítások mentése alapértelmezetnek',
'persist.restore_hard' => 'Gyári alapbeállítások betöltése',
'persist.restore_hard_explain' =>
'(Ez törli a Wifi beállításokat, de nincs hatása az admin jelszóra.)',
'backup.title' => 'Configurációs fájl biztonsági másolat készítés',
'backup.explain' => 'Minden beállítás menthető és visszaállítható az admin jelszó kivételévelAll config except the admin password can be backed up and restored using egy .INI fájllal.',
'backup.export' => 'Fáljbe exportálás',
'backup.import' => 'Importálás!',
// UART settings form
'uart.title' => 'Soros port paraméterek',
'uart.explain' => '
Ez a beállítás szabályozza a kommunikációs UART-ot. A hibakereső UART fix
115.200 baud-val, egy stop-bittel és paritás bit nélkül működik.
',
'uart.baud' => 'Baud rate',
'uart.parity' => 'Parity',
'uart.parity.none' => 'Egyiksem',
'uart.parity.odd' => 'Páratlan',
'uart.parity.even' => 'Páros',
'uart.stop_bits' => 'Stop-bit',
'uart.stop_bits.one' => 'Egy',
'uart.stop_bits.one_and_half' => 'Másfél',
'uart.stop_bits.two' => 'Kettő',
// HW tuning form
'hwtuning.title' => 'Hardware Tuning',
'hwtuning.explain' => '
ESP8266-t órajelét lehetséges 80&nbsp;MHz-ről 160&nbsp;MHz-re emelni. Ettől
jobb válaszidők és gyakoribb képernyő frissítések várhatóak, viszont megnövekszik
az energia felhasználás. Az interferencia esélye is megnő.
Ovatosan használd!.
',
'hwtuning.overclock' => 'Órajel emelése 160MHz-re',
'gpio2_config' => 'GPIO2 function', // TODO translate
'gpio4_config' => 'GPIO4 function',
'gpio5_config' => 'GPIO5 function',
'gpio_config.off' => 'Disabled',
'gpio_config.off_2' => 'Debug UART Tx',
'gpio_config.out_initial0' => 'Output (initial 0)',
'gpio_config.out_initial1' => 'Output (initial 1)',
'gpio_config.in_pull' => 'Input (pull-up)',
'gpio_config.in_nopull' => 'Input (floating)',
// Generic button / dialog labels
'apply' => 'Alkalmaz',
'start' => 'Start',
'cancel' => 'Mégse',
'enabled' => 'Engedélyezve',
'disabled' => 'Letiltva',
'yes' => 'Igen',
'no' => 'Nem',
'confirm' => 'OK',
'copy' => 'Másolás',
'form_errors' => 'Validációs hiba:',
];

@ -0,0 +1,12 @@
// define language keys used by JS here
module.exports = [
'wifi.connected_ip_is',
'wifi.not_conn',
'wifi.enter_passwd',
'term_nav.fullscreen',
'term_conn.connecting',
'term_conn.waiting_content',
'term_conn.disconnected',
'term_conn.waiting_server',
'term_conn.reconnecting'
]

@ -0,0 +1,21 @@
{
"name": "espterm-front-end",
"version": "1.0.0",
"description": "ESPTerm web interface",
"license": "MPL-2.0",
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-env": "^1.6.0",
"babel-preset-minify": "^0.2.0",
"html-minifier": "^3.5.5",
"node-sass": "^4.5.3",
"standard": "^10.0.3",
"webpack": "^3.6.0"
},
"scripts": {
"webpack": "webpack --display-modules $@",
"sass": "node-sass $@",
"html-minifier": "html-minifier $@"
}
}

@ -13,8 +13,3 @@
}
?>
</nav>
<script>
function menuOpen() { $('#menu').toggleClass('expanded') }
$('#brand').on('click', menuOpen).on('keypress', cr(menuOpen));
</script>

@ -3,15 +3,14 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, maximum-scale=1">
<title><?= $_GET['PAGE_TITLE'] ?></title>
<link href="/css/app.css" rel="stylesheet">
<script src="/js/app.js"></script>
<link href="/css/app.<?= GIT_HASH . '-' . LOCALE ?>.css" rel="stylesheet">
<script src="/js/app.<?= GIT_HASH . '-' . LOCALE ?>.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'] ?>">
@ -33,4 +32,7 @@ if (strpos($_GET['BODYCLASS'], 'cfg') !== false) {
<span class="lead"><?= tr('form_errors') ?></span>&nbsp;<span class="list"></span>
</div>
<div class="Box message hidden"></div>
<div class="Box errmessage hidden"></div>
<?php endif; ?>

@ -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
@ -10,17 +10,23 @@
</p>
<p>
<a href="http://measure.feld.cvut.cz/" target="blank">Katedra měření, FEL ČVUT</a><br>
Department of Measurement, FEE CTU
Vyvinuto na <a href="http://measure.feld.cvut.cz/" target="blank">Katedře měření, FEL ČVUT</a><br>
Developed at the Department of Measurement, FEE CTU in Prague
</p>
</div>
<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,12 +37,17 @@
<td>v%vers_sdk%</td>
</tr>
</table>
</div>
</div>
<div class="Box">
<h2>Issues</h2>
<p>
Please report any issues to the <a href="%githubrepo%/issues">bugtracker</a> or send them by e-mail (see above).
Please report any issues to our <a href="%githubrepo%/issues">bugtracker</a> or send them by e-mail.
</p>
<p>
ESPTerm has a <a href="https://groups.google.com/forum/#!forum/espterm-dev">mailing list</a> for
troubleshooting and release announcements.
</p>
<p>
Firmware updates can be downloaded from the <a href="%githubrepo%/releases">releases page</a> and flashed
@ -58,12 +69,24 @@
<div class="Box">
<h2>Thanks</h2>
<p>
The webserver is based on a <a href="https://github.com/MightyPork/libesphttpd">fork</a> of the
<a href="https://github.com/Spritetm/esphttpd">esphttpd</a> library by Jeroen Domburg (Sprite_tm).
</p>
<p>
Using (modified) JS library <a href="https://github.com/kylebarrow/chibi">chibi.js</a> by
Kyle Barrow as a lightweight jQuery alternative.
</p>
<p><i>…for making this project possible, in no particular order, go to:</i></p>
<ul style="padding-left: 20px">
<li>
*Jeroen "SpriteTM" Domburg,* for writing the <a href="https://github.com/Spritetm/esphttpd">esphttpd</a>
server library we use (as a <a href="https://github.com/MightyPork/libesphttpd">fork</a>)
</li>
<li>
*Kyle Barrow,* for writing the <a href="https://github.com/kylebarrow/chibi">chibi.js</a> library
we use instead of jQuery
</li>
<li>
*cpsdqs,* for rewriting the front-end to use HTML5 canvas and other JS improvements
</li>
<li>
*Guenter Honisch,* for finding bugs and suggesting many improvements
</li>
<li>
*doc. Jan Fischer,* who came up with the original idea
</li>
</ul>
</div>

@ -2,7 +2,8 @@
$ipmask='pattern="^([0-9]{1,3}\.){3}[0-9]{1,3}$"';
?>
<form class="Box str mobcol" action="<?= e(url('network_set')) ?>" method="GET" id="form-2">
<!-- STA -->
<form class="Box str mobcol" action="<?= e(url('network_set')) ?>" method="GET" id="form-netsta">
<h2 tabindex=0><?= tr('net.sta') ?></h2>
<div class="Row explain">
@ -31,11 +32,12 @@ $ipmask='pattern="^([0-9]{1,3}\.){3}[0-9]{1,3}$"';
</div>
<div class="Row buttons">
<a class="button icn-ok" href="#" onclick="qs('#form-2').submit()"><?= tr('apply') ?></a>
<a class="button icn-ok" href="#" onclick="qs('#form-netsta').submit()"><?= tr('apply') ?></a>
</div>
</form>
<form class="Box str mobcol" action="<?= e(url('network_set')) ?>" method="GET" id="form-1">
<!-- AP -->
<form class="Box str mobcol" action="<?= e(url('network_set')) ?>" method="GET" id="form-netap">
<h2 tabindex=0><?= tr('net.ap') ?></h2>
<div class="Row explain">
@ -69,10 +71,11 @@ $ipmask='pattern="^([0-9]{1,3}\.){3}[0-9]{1,3}$"';
</div>
<div class="Row buttons">
<a class="button icn-ok" href="#" onclick="qs('#form-1').submit()"><?= tr('apply') ?></a>
<a class="button icn-ok" href="#" onclick="qs('#form-netap').submit()"><?= tr('apply') ?></a>
</div>
</form>
<!-- MACs -->
<div class="Box mobcol">
<h2><?= tr('net.details') ?></h2>

@ -1,90 +1,193 @@
<!-- Persist -->
<div class="Box str mobcol">
<h2 tabindex=0><?= tr('system.save_restore') ?></h2>
<h2 tabindex=0><?= tr('persist.title') ?></h2>
<div class="Row explain nomargintop">
<?= tr('system.explain_persist') ?>
<?= tr('persist.explain') ?>
</div>
<div class="Row buttons2">
<a class="button icn-restore"
onclick="return confirm('<?= tr('system.confirm_restore') ?>');"
onclick="return confirm('<?= e(tr('persist.confirm_restore')) ?>');"
href="<?= e(url('restore_defaults')) ?>">
<?= tr('system.restore_defaults') ?>
<?= tr('persist.restore_defaults') ?>
</a>
</div>
<div class="Row buttons2">
<a onclick="writeDefaults(); return false;" href="#"><?= tr('system.write_defaults') ?></a>
<a onclick="writeDefaults(); return false;" href="#"><?= tr('persist.write_defaults') ?></a>
</div>
<div class="Row buttons2">
<a onclick="return confirm('<?= tr('system.confirm_restore_hard') ?>');"
<a onclick="return confirm('<?= e(tr('persist.confirm_restore_hard')) ?>');"
href="<?= e(url('restore_hard')) ?>">
<?= tr('system.restore_hard') ?>
</a>
<?= tr('persist.restore_hard') ?>
</a><br>
<?= tr('persist.restore_hard_explain') ?>
</div>
</div>
<!-- Backup -->
<div class="Box str mobcol">
<h2 tabindex=0><?= tr('backup.title') ?></h2>
<div class="Row explain nomargintop">
<?= tr('backup.explain') ?>
</div>
<form class="Box str mobcol" action="<?= e(url('system_set')) ?>" method="GET" id="form-1">
<h2 tabindex=0><?= tr('system.uart') ?></h2>
<div class="Row buttons2">
<a class="button"
href="<?= e(url('ini_export')) ?>">
<?= tr('backup.export') ?>
</a>
</div>
<div class="Row buttons2">
<form method="POST" action="<?= e(url('ini_import')) ?>" enctype='multipart/form-data'>
<span class="filewrap"><input accept=".ini,text/plain" type="file" name="file"></span><!--
--><input type="submit" value="<?= tr('backup.import') ?>">
</form>
</div>
</div>
<!-- Overclock -->
<form class="Box str mobcol" action="<?= e(url('system_set')) ?>" method="GET" id="form-hw">
<h2 tabindex=0><?= tr('hwtuning.title') ?></h2>
<div class="Row explain">
<?= tr('system.explain_uart') ?>
<?= tr('hwtuning.explain') ?>
</div>
<div class="Row checkbox" >
<label><?= tr('hwtuning.overclock') ?></label><!--
--><span class="box" tabindex=0 role=checkbox></span>
<input type="hidden" id="overclock" name="overclock" value="%overclock%">
</div>
<div class="Row">
<label for="uart_baud"><?= tr('uart.baud') ?><span class="mq-phone">&nbsp;(bps)</span></label>
<select name="uart_baud" id="uart_baud" class="short">
<?php foreach([
300, 600, 1200, 2400, 4800, 9600, 19200, 38400,
57600, 74880, 115200, 230400, 460800, 921600, 1843200, 3686400,
] as $b):
?><option value="<?=$b?>"><?= number_format($b, 0, ',', '.') ?></option>
<?php endforeach; ?>
<label for="gpio2_conf"><?= tr("gpio2_config") ?></label>
<select name="gpio2_conf" id="gpio2_conf">
<option value="0"><?= tr("gpio_config.off_2") ?></option>
<option value="1"><?= tr("gpio_config.out_initial0") ?></option>
<option value="2"><?= tr("gpio_config.out_initial1") ?></option>
<option value="3"><?= tr("gpio_config.in_pull") ?></option>
<option value="4"><?= tr("gpio_config.in_nopull") ?></option>
</select>
<span class="mq-no-phone">&nbsp;bps</span>
</div>
<div class="Row">
<label for="uart_parity"><?= tr('uart.parity') ?></label>
<select name="uart_parity" id="uart_parity" class="short">
<?php foreach([
2 => tr('uart.parity.none'),
1 => tr('uart.parity.odd'),
0 => tr('uart.parity.even'),
] as $k => $label):
?><option value="<?=$k?>"><?=$label?></option>
<?php endforeach; ?>
<label for="gpio4_conf"><?= tr("gpio4_config") ?></label>
<select name="gpio4_conf" id="gpio4_conf">
<option value="0"><?= tr("gpio_config.off") ?></option>
<option value="1"><?= tr("gpio_config.out_initial0") ?></option>
<option value="2"><?= tr("gpio_config.out_initial1") ?></option>
<option value="3"><?= tr("gpio_config.in_pull") ?></option>
<option value="4"><?= tr("gpio_config.in_nopull") ?></option>
</select>
</div>
<div class="Row">
<label for="uart_stopbits"><?= tr('uart.stop_bits') ?></label>
<select name="uart_stopbits" id="uart_stopbits" class="short">
<?php foreach([
1 => tr('uart.stop_bits.one'),
2 => tr('uart.stop_bits.one_and_half'),
3 => tr('uart.stop_bits.two'),
] as $k => $label):
?><option value="<?=$k?>"><?=$label?></option>
<?php endforeach; ?>
<label for="gpio5_conf"><?= tr("gpio5_config") ?></label>
<select name="gpio5_conf" id="gpio5_conf">
<option value="0"><?= tr("gpio_config.off") ?></option>
<option value="1"><?= tr("gpio_config.out_initial0") ?></option>
<option value="2"><?= tr("gpio_config.out_initial1") ?></option>
<option value="3"><?= tr("gpio_config.in_pull") ?></option>
<option value="4"><?= tr("gpio_config.in_nopull") ?></option>
</select>
</div>
<div class="Row buttons">
<a class="button icn-ok" href="#" onclick="qs('#form-hw').submit()"><?= tr('apply') ?></a>
</div>
</form>
<?php
$NOFILL = 'readonly onfocus="this.removeAttribute(\'readonly\')" style="cursor:text" autocomplete="off"';
?>
<!-- Access perms -->
<form class="Box str mobcol" action="<?= e(url('system_set')) ?>" method="GET" id="form-access">
<h2 tabindex=0><?= tr('pwlock.title') ?></h2>
<div class="Row explain">
<?= tr('pwlock.explain') ?>
</div>
<div class="Row">
<label for="pwlock"><?= tr("pwlock.region") ?></label>
<select name="pwlock" id="pwlock">
<option value="0"><?= tr("pwlock.region.none") ?></option>
<option value="1"><?= tr("pwlock.region.settings_noterm") ?></option>
<option value="2"><?= tr("pwlock.region.settings") ?></option>
<option value="3"><?= tr("pwlock.region.menus") ?></option>
<option value="4"><?= tr("pwlock.region.all") ?></option>
</select>
</div>
<div class="Row">
<label for="access_name"><?= tr('pwlock.access_name') ?></label>
<input type="text" name="access_name" id="access_name" value="%h:access_name%">
</div>
<div class="Row">
<label for="access_pw"><?= tr('pwlock.new_access_pw') ?></label>
<input type="password" name="access_pw" id="access_pw" <?=$NOFILL?>>
</div>
<div class="Row">
<label for="access_pw2"><?= tr('pwlock.new_access_pw2') ?></label>
<input type="password" name="access_pw2" id="access_pw2" <?=$NOFILL?>>
</div>
<div class="Row">
<label for="pw"><?= tr('pwlock.admin_pw') ?></label>
<input type="password" name="pw" id="pw" required>
</div>
<div class="Row buttons">
<a class="button icn-ok" href="#" onclick="qs('#form-access').submit()"><?= tr('apply') ?></a>
</div>
</form>
<!-- Admin pw -->
<form class="Box str mobcol" action="<?= e(url('system_set')) ?>" method="GET" id="form-admin">
<h2 tabindex=0><?= tr('adminpw.title') ?></h2>
<div class="Row explain">
<?= tr('adminpw.explain') ?>
</div>
<div class="Row">
<label for="admin_pw"><?= tr('adminpw.new_admin_pw') ?></label>
<input type="password" name="admin_pw" id="admin_pw">
</div>
<div class="Row">
<label for="admin_pw2"><?= tr('adminpw.new_admin_pw2') ?></label>
<input type="password" name="admin_pw2" id="admin_pw2">
</div>
<div class="Row">
<label for="pw"><?= tr('adminpw.old_admin_pw') ?></label>
<input type="password" name="pw" id="pw" required>
</div>
<div class="Row buttons">
<a class="button icn-ok" href="#" onclick="qs('#form-1').submit()"><?= tr('apply') ?></a>
<a class="button icn-ok" href="#" onclick="qs('#form-admin').submit()"><?= tr('apply') ?></a>
</div>
</form>
<script>
function writeDefaults() {
var pw = prompt('<?= tr('system.confirm_store_defaults') ?>');
var pw = prompt('<?= tr('persist.confirm_store_defaults') ?>');
if (!pw) return;
location.href = <?=json_encode(url('write_defaults')) ?> + '?pw=' + pw;
}
$('#uart_baud').val(%uart_baud%);
$('#uart_parity').val(%uart_parity%);
$('#uart_stopbits').val(%uart_stopbits%);
$('#pwlock').val(%pwlock%);
$('#gpio2_conf').val(%gpio2_conf%);
$('#gpio4_conf').val(%gpio4_conf%);
$('#gpio5_conf').val(%gpio5_conf%);
</script>

@ -2,110 +2,109 @@
<a href="<?= e(url('reset_screen')) ?>"><?= tr('term.reset_screen') ?></a>
</div>
<form class="Box mobopen str" action="<?= e(url('term_set')) ?>" method="GET" id='form-1'>
<h2><?= tr('term.defaults') ?></h2>
<!-- Theme -->
<form class="Box mobcol str" action="<?= e(url('term_set')) ?>" method="GET" id='form-scheme'>
<h2><?= tr('term.color_scheme') ?></h2>
<div class="Row explain">
<?= tr('term.explain_initials') ?>
<?= tr('term.explain_scheme') ?>
</div>
<div class="Row">
<label for="theme"><?= tr("term.theme") ?></label>
<select name="theme" id="theme" class="short" onchange="showColor()">
<select name="theme" id="theme">
<option value="0">Tango</option>
<option value="1">Linux</option>
<option value="1">Linux (CGA)</option>
<option value="2">XTerm</option>
<option value="3">Rxvt</option>
<option value="4">Ambience</option>
<option value="5">Solarized</option>
<option value="5">Solarized Dark</option>
<option value="11">Solarized Dark, high contrast</option>
<option value="10">Solarized Light</option>
<option value="6">CGA NTSC</option>
<option value="7">ZX Spectrum</option>
<option value="8">Apple II</option>
<option value="9">Commodore</option>
</select>
<span onclick="TermConf.prevTheme()" class="mq-no-phone theme-nav-btn"></span>
<span onclick="TermConf.nextTheme()" class="mq-no-phone theme-nav-btn"></span>
</div>
<div class="Row color-preview">
<div class="colorprev">
<span data-fg=0 class="bg0 fg0">30</span><!--
--><span data-fg=1 class="bg0 fg1">31</span><!--
--><span data-fg=2 class="bg0 fg2">32</span><!--
--><span data-fg=3 class="bg0 fg3">33</span><!--
--><span data-fg=4 class="bg0 fg4">34</span><!--
--><span data-fg=5 class="bg0 fg5">35</span><!--
--><span data-fg=6 class="bg0 fg6">36</span><!--
--><span data-fg=7 class="bg0 fg7">37</span>
</div>
<div class="colorprev">
<span data-fg=8 class="bg0 fg8">90</span><!--
--><span data-fg=9 class="bg0 fg9">91</span><!--
--><span data-fg=10 class="bg0 fg10">92</span><!--
--><span data-fg=11 class="bg0 fg11">93</span><!--
--><span data-fg=12 class="bg0 fg12">94</span><!--
--><span data-fg=13 class="bg0 fg13">95</span><!--
--><span data-fg=14 class="bg0 fg14">96</span><!--
--><span data-fg=15 class="bg0 fg15">97</span>
</div>
<div class="colorprev">
<span data-bg=0 class="bg0 fg15">40</span><!--
--><span data-bg=1 class="bg1 fg15">41</span><!--
--><span data-bg=2 class="bg2 fg15">42</span><!--
--><span data-bg=3 class="bg3 fg0">43</span><!--
--><span data-bg=4 class="bg4 fg15">44</span><!--
--><span data-bg=5 class="bg5 fg15">45</span><!--
--><span data-bg=6 class="bg6 fg15">46</span><!--
--><span data-bg=7 class="bg7 fg0">47</span>
</div>
<div class="colorprev">
<span data-bg=8 class="bg8 fg15">100</span><!--
--><span data-bg=9 class="bg9 fg0">101</span><!--
--><span data-bg=10 class="bg10 fg0">102</span><!--
--><span data-bg=11 class="bg11 fg0">103</span><!--
--><span data-bg=12 class="bg12 fg0">104</span><!--
--><span data-bg=13 class="bg13 fg0">105</span><!--
--><span data-bg=14 class="bg14 fg0">106</span><!--
--><span data-bg=15 class="bg15 fg0">107</span>
<label><?= tr("term.color_bg_prev") ?></label>
<div>
<div class="colorprev bg">
<span data-bg=0 data-fg=15>0</span><!--
--><span data-bg=1 data-fg=15>1</span><!--
--><span data-bg=2 data-fg=15>2</span><!--
--><span data-bg=3 data-fg=0>3</span><!--
--><span data-bg=4 data-fg=15>4</span><!--
--><span data-bg=5 data-fg=15>5</span><!--
--><span data-bg=6 data-fg=15>6</span><!--
--><span data-bg=7 data-fg=0>7</span>
</div>
<div class="colorprev bg">
<span data-bg=8 data-fg=15>8</span><!--
--><span data-bg=9 data-fg=0>9</span><!--
--><span data-bg=10 data-fg=0>10</span><!--
--><span data-bg=11 data-fg=0>11</span><!--
--><span data-bg=12 data-fg=0>12</span><!--
--><span data-bg=13 data-fg=0>13</span><!--
--><span data-bg=14 data-fg=0>14</span><!--
--><span data-bg=15 data-fg=0>15</span>
</div>
</div>
</div>
<div class="Row color-preview">
<div style="
" id="color-example">
<?= tr("term.example") ?>
</div>
<label><?= tr("term.color_fg_prev") ?></label>
<div>
<div class="colorprev fg">
<span data-fg=0>0</span><!--
--><span data-fg=1>1</span><!--
--><span data-fg=2>2</span><!--
--><span data-fg=3>3</span><!--
--><span data-fg=4>4</span><!--
--><span data-fg=5>5</span><!--
--><span data-fg=6>6</span><!--
--><span data-fg=7>7</span>
</div>
<div class="Row">
<label><?= tr("term.default_fg_bg") ?></label>
<select name="default_fg" id="default_fg" class="short" onchange="showColor()">
<?php for($i=0; $i<16; $i++): ?>
<option value="<?=$i?>"><?= tr("color.$i") ?></option>
<?php endfor; ?>
</select>&nbsp;<!--
--><select name="default_bg" id="default_bg" class="short" onchange="showColor()">
<?php for($i=0; $i<16; $i++): ?>
<option value="<?=$i?>"><?= tr("color.$i") ?></option>
<?php endfor; ?>
</select>
<div class="colorprev fg">
<span data-fg=8>8</span><!--
--><span data-fg=9>9</span><!--
--><span data-fg=10>10</span><!--
--><span data-fg=11>11</span><!--
--><span data-fg=12>12</span><!--
--><span data-fg=13>13</span><!--
--><span data-fg=14>14</span><!--
--><span data-fg=15>15</span>
</div>
</div>
</div>
<div class="Row">
<label for="term_width"><?= tr('term.term_width') ?></label>
<input type="number" step=1 min=1 max=255 name="term_width" id="term_width" value="%term_width%" required>&nbsp;<!--
--><input type="number" step=1 min=1 max=255 name="term_height" id="term_height" value="%term_height%" required>
<div class="Row color-preview">
<label><?= tr("term.colors_preview") ?></label>
<div class="color-example" data-fg="" data-bg="">
<?= tr("term.example") ?>
</div>
</div>
<div class="Row">
<label for="term_title"><?= tr('term.term_title') ?></label>
<input type="text" name="term_title" id="term_title" value="%h:term_title%" required>
<div class="Row color-preview">
<label><?= tr("term.fgbg_presets") ?></label>
<div id="fgbg_presets"></div>
</div>
<div class="Row">
<label><?= tr("term.buttons") ?></label>
<input class="short" type="text" name="btn1" id="btn1" value="%h:btn1%">&nbsp;
<input class="short" type="text" name="btn2" id="btn2" value="%h:btn2%">&nbsp;
<input class="short" type="text" name="btn3" id="btn3" value="%h:btn3%">&nbsp;
<input class="short" type="text" name="btn4" id="btn4" value="%h:btn4%">&nbsp;
<input class="short" type="text" name="btn5" id="btn5" value="%h:btn5%">
<div class="SubRow">
<label for="default_fg"><?= tr("term.color_fg") ?></label>
<input type="text" name="default_fg" id="default_fg" class="short" value="%default_fg%">
</div>
<div class="SubRow">
<label for="default_bg"><?= tr("term.color_bg") ?></label>
<input type="text" name="default_bg" id="default_bg" class="short" value="%default_bg%">
</div>
</div>
<div class="Row">
@ -121,17 +120,168 @@
</div>
<div class="Row buttons">
<a class="button icn-ok" href="#" onclick="qs('#form-1').submit()"><?= tr('apply') ?></a>
<a class="button icn-ok" href="#" onclick="qs('#form-scheme').submit()"><?= tr('apply') ?></a>
</div>
</form>
<form class="Box fold str" action="<?= e(url('term_set')) ?>" method="GET" id='form-2'>
<!-- Initials -->
<form class="Box mobcol str" action="<?= e(url('term_set')) ?>" method="GET" id='form-initial'>
<h2><?= tr('term.defaults') ?></h2>
<div class="Row explain">
<?= tr('term.explain_initials') ?>
</div>
<div class="Row">
<div class="SubRow">
<label for="width"><?= tr('term.term_width') ?></label>
<input type="number" step=1 min=1 max=255 name="width" id="width" value="%width%" required>
</div>
<div class="SubRow">
<label for="height"><?= tr('term.term_height') ?></label>
<input type="number" step=1 min=1 max=255 name="height" id="height" value="%height%" required>
</div>
</div>
<div class="Row">
<label for="title"><?= tr('term.term_title') ?></label>
<input type="text" name="title" id="title" value="%h:title%" required>
</div>
<div class="Row checkbox" >
<label><?= tr('term.show_buttons') ?></label><!--
--><span class="box" tabindex=0 role=checkbox></span>
<input type="hidden" id="show_buttons" name="show_buttons" value="%show_buttons%">
</div>
<div class="Row">
<label for="button_count"><?= tr('term.button_count') ?></label>
<input type="number" step=1 min=0 max=5 name="button_count" id="button_count" value="%h:button_count%" required>
</div>
<div class="Row">
<label><?= tr("term.buttons") ?></label>
<input class="tiny" type="text" name="btn1" id="btn1" value="%h:btn1%">
<input class="tiny" type="text" name="btn2" id="btn2" value="%h:btn2%">
<input class="tiny" type="text" name="btn3" id="btn3" value="%h:btn3%">
<input class="tiny" type="text" name="btn4" id="btn4" value="%h:btn4%">
<input class="tiny" type="text" name="btn5" id="btn5" value="%h:btn5%">
</div>
<div class="Row">
<label><?= tr("term.button_msgs") ?></label>
<input class="tiny" type="text" name="bm1" id="bm1" value="%h:bm1%">
<input class="tiny" type="text" name="bm2" id="bm2" value="%h:bm2%">
<input class="tiny" type="text" name="bm3" id="bm3" value="%h:bm3%">
<input class="tiny" type="text" name="bm4" id="bm4" value="%h:bm4%">
<input class="tiny" type="text" name="bm5" id="bm5" value="%h:bm5%">
</div>
<div class="Row">
<label><?= tr("term.button_colors") ?></label>
<input class="tiny" type="text" name="bc1" id="bc1" value="%h:bc1%">
<input class="tiny" type="text" name="bc2" id="bc2" value="%h:bc2%">
<input class="tiny" type="text" name="bc3" id="bc3" value="%h:bc3%">
<input class="tiny" type="text" name="bc4" id="bc4" value="%h:bc4%">
<input class="tiny" type="text" name="bc5" id="bc5" value="%h:bc5%">
</div>
<div class="Row">
<label for="backdrop"><?= tr('term.backdrop') ?></label>
<input type="text" name="backdrop" id="backdrop" value="%h:backdrop%" required>
</div>
<div class="Row checkbox" >
<label><?= tr('term.crlf_mode') ?></label><!--
--><span class="box" tabindex=0 role=checkbox></span>
<input type="hidden" id="crlf_mode" name="crlf_mode" value="%crlf_mode%">
</div>
<div class="Row checkbox" >
<label><?= tr('term.loopback') ?></label><!--
--><span class="box" tabindex=0 role=checkbox></span>
<input type="hidden" id="loopback" name="loopback" value="%loopback%">
</div>
<div class="Row buttons">
<a class="button icn-ok" href="#" onclick="qs('#form-initial').submit()"><?= tr('apply') ?></a>
</div>
</form>
<!-- UART -->
<form class="Box mobcol str" action="<?= e(url('term_set')) ?>" method="GET" id="form-uart">
<h2 tabindex=0><?= tr('uart.title') ?></h2>
<div class="Row explain">
<?= tr('uart.explain') ?>
</div>
<div class="Row">
<label for="uart_baudrate"><?= tr('uart.baud') ?><span class="mq-phone">&nbsp;(bps)</span></label>
<select name="uart_baudrate" id="uart_baudrate" class="short">
<?php
foreach([
300, 600, 1200, 2400, 4800, 9600, 19200, 38400,
57600, 74880, 115200, 230400, 460800, 921600, 1843200, 3686400,
] as $b):
?><option value="<?=$b?>"><?= number_format($b, 0, ',', '.') ?></option>
<?php endforeach; ?>
</select>
<span class="mq-no-phone">&nbsp;bps</span>
</div>
<div class="Row">
<label for="uart_parity"><?= tr('uart.parity') ?></label>
<select name="uart_parity" id="uart_parity" class="short">
<?php
foreach([
2 => tr('uart.parity.none'),
1 => tr('uart.parity.odd'),
0 => tr('uart.parity.even'),
] as $k => $label):
?><option value="<?=$k?>"><?=$label?></option>
<?php endforeach; ?>
</select>
</div>
<div class="Row">
<label for="uart_stopbits"><?= tr('uart.stop_bits') ?></label>
<select name="uart_stopbits" id="uart_stopbits" class="short">
<?php
foreach([
1 => tr('uart.stop_bits.one'),
2 => tr('uart.stop_bits.one_and_half'),
3 => tr('uart.stop_bits.two'),
] as $k => $label):
?><option value="<?=$k?>"><?=$label?></option>
<?php endforeach; ?>
</select>
</div>
<div class="Row buttons">
<a class="button icn-ok" href="#" onclick="qs('#form-uart').submit()"><?= tr('apply') ?></a>
</div>
</form>
<!-- Expert terminal opts -->
<form class="Box mobcol str" action="<?= e(url('term_set')) ?>" method="GET" id='form-expert'>
<h2><?= tr('term.expert') ?></h2>
<div class="Row explain">
<?= tr('term.explain_expert') ?>
</div>
<div class="Row">
<label for="font_stack"><?= tr('term.font_stack') ?></label>
<input type="text" name="font_stack" id="font_stack" value="%h:font_stack%" required>
</div>
<div class="Row">
<label for="font_size"><?= tr('term.font_size') ?><span class="mq-phone">&nbsp;(px)</span></label>
<input type="number" step=1 min=0 name="font_size" id="font_size" value="%font_size%" required>
<span class="mq-no-phone">&nbsp;px</span>
</div>
<div class="Row">
<label for="parser_tout_ms"><?= tr('term.parser_tout_ms') ?><span class="mq-phone">&nbsp;(ms)</span></label>
<input type="number" step=1 min=0 name="parser_tout_ms" id="parser_tout_ms" value="%parser_tout_ms%" required>
@ -150,31 +300,28 @@
<span class="mq-no-phone">&nbsp;ms</span>
</div>
<div class="Row">
<label><?= tr("term.button_msgs") ?></label>
<input class="short" type="text" name="bm1" id="bm1" value="%h:bm1%">&nbsp;
<input class="short" type="text" name="bm2" id="bm2" value="%h:bm2%">&nbsp;
<input class="short" type="text" name="bm3" id="bm3" value="%h:bm3%">&nbsp;
<input class="short" type="text" name="bm4" id="bm4" value="%h:bm4%">&nbsp;
<input class="short" type="text" name="bm5" id="bm5" value="%h:bm5%">
<div class="Row checkbox" >
<label><?= tr('term.debugbar') ?></label><!--
--><span class="box" tabindex=0 role=checkbox></span>
<input type="hidden" id="debugbar" name="debugbar" value="%debugbar%">
</div>
<div class="Row checkbox" >
<label><?= tr('term.fn_alt_mode') ?></label><!--
<label><?= tr('term.ascii_debug') ?></label><!--
--><span class="box" tabindex=0 role=checkbox></span>
<input type="hidden" id="fn_alt_mode" name="fn_alt_mode" value="%fn_alt_mode%">
<input type="hidden" id="ascii_debug" name="ascii_debug" value="%ascii_debug%">
</div>
<div class="Row checkbox" >
<label><?= tr('term.crlf_mode') ?></label><!--
<label><?= tr('term.fn_alt_mode') ?></label><!--
--><span class="box" tabindex=0 role=checkbox></span>
<input type="hidden" id="crlf_mode" name="crlf_mode" value="%crlf_mode%">
<input type="hidden" id="fn_alt_mode" name="fn_alt_mode" value="%fn_alt_mode%">
</div>
<div class="Row checkbox" >
<label><?= tr('term.show_buttons') ?></label><!--
<label><?= tr('term.want_all_fn') ?></label><!--
--><span class="box" tabindex=0 role=checkbox></span>
<input type="hidden" id="show_buttons" name="show_buttons" value="%show_buttons%">
<input type="hidden" id="want_all_fn" name="want_all_fn" value="%want_all_fn%">
</div>
<div class="Row checkbox" >
@ -184,39 +331,24 @@
</div>
<div class="Row checkbox" >
<label><?= tr('term.loopback') ?></label><!--
<label><?= tr('term.allow_decopt_12') ?></label><!--
--><span class="box" tabindex=0 role=checkbox></span>
<input type="hidden" id="loopback" name="loopback" value="%loopback%">
<input type="hidden" id="allow_decopt_12" name="allow_decopt_12" value="%allow_decopt_12%">
</div>
<div class="Row buttons">
<a class="button icn-ok" href="#" onclick="qs('#form-2').submit()"><?= tr('apply') ?></a>
<a class="button icn-ok" href="#" onclick="qs('#form-expert').submit()"><?= tr('apply') ?></a>
</div>
</form>
<script>
$('#default_fg').val(%default_fg%);
$('#default_bg').val(%default_bg%);
$('#cursor_shape').val(%cursor_shape%);
$('#theme').val(%theme%);
function showColor() {
var ex = qs('#color-example');
ex.className = '';
ex.classList.add('fg'+$('#default_fg').val());
ex.classList.add('bg'+$('#default_bg').val());
var th = $('#theme').val();
$('.color-preview').forEach(function(e) {
e.className = 'Row color-preview theme-'+th;
});
}
showColor();
$('.colorprev span').on('click', function() {
var fg = this.dataset.fg;
var bg = this.dataset.bg;
if (typeof fg != 'undefined') $('#default_fg').val(fg);
if (typeof bg != 'undefined') $('#default_bg').val(bg);
showColor()
$.ready(function () {
$('#cursor_shape').val('%cursor_shape%');
$('#theme').val('%theme%');
$('#uart_baudrate').val('%uart_baudrate%');
$('#uart_parity').val('%uart_parity%');
$('#uart_stopbits').val('%uart_stopbits%');
TermConf.init();
});
</script>

@ -1,4 +1,5 @@
<form class="Box str mobcol" action="<?= e(url('wifi_set')) ?>" method="GET" id="form-1">
<!-- AP -->
<form class="Box str mobcol" action="<?= e(url('wifi_set')) ?>" method="GET" id="form-ap">
<h2 tabindex=0><?= tr('wifi.ap') ?></h2>
<div class="Row checkbox x-ap-toggle">
@ -38,11 +39,12 @@
</div>
<div class="Row buttons">
<a class="button icn-ok" href="#" onclick="qs('#form-1').submit()"><?= tr('apply') ?></a>
<a class="button icn-ok" href="#" onclick="qs('#form-ap').submit()"><?= tr('apply') ?></a>
</div>
</form>
<form class="Box str mobcol expanded" action="<?= e(url('wifi_set')) ?>" method="GET" id="form-2">
<!-- STA -->
<form class="Box str mobcol expanded" action="<?= e(url('wifi_set')) ?>" method="GET" id="form-sta">
<h2 tabindex=0><?= tr('wifi.sta') ?></h2>
<div class="Row checkbox x-sta-toggle">
@ -85,7 +87,7 @@
</div>
<div class="Row buttons">
<a class="button icn-ok" href="#" onclick="qs('#form-2').submit()"><?= tr('apply') ?></a>
<a class="button icn-ok" href="#" onclick="qs('#form-sta').submit()"><?= tr('apply') ?></a>
</div>
</form>

@ -1,13 +1,13 @@
<h1><?= tr('menu.cfg_wifi_conn') ?></h1>
<div class="Box">
<p><b><?= tr('wifi.conn.status') ?></b> <span id="status"></span><span class="anim-dots">.</span></p>
<a href="<?= e(url('cfg_wifi')) ?>" id="backbtn" class="button"><?= tr('wifi.conn.back_to_config') ?></a>
<p><b><?= tr('wificonn.status') ?></b> <span id="status"></span><span class="anim-dots">.</span></p>
<a href="<?= e(url('cfg_wifi')) ?>" id="backbtn" class="button"><?= tr('wificonn.back_to_config') ?></a>
</div>
<div class="Box">
<p><?= tr('wifi.conn.explain_android_sucks') ?></p>
<p><?= tr('wifi.conn.explain_reset') ?></p>
<p><?= tr('wificonn.explain_android_sucks') ?></p>
<p><?= tr('wificonn.explain_reset') ?></p>
</div>
<script>
@ -16,15 +16,15 @@
var failCounter = 0;
var messages = <?= json_encode([
'disabled' => tr('wifi.conn.disabled'),
'idle' => tr('wifi.conn.idle'),
'success' => tr('wifi.conn.success'),
'working' => tr('wifi.conn.working'),
'fail' => tr('wifi.conn.fail'),
'disabled' => tr('wificonn.disabled'),
'idle' => tr('wificonn.idle'),
'success' => tr('wificonn.success'),
'working' => tr('wificonn.working'),
'fail' => tr('wificonn.fail'),
]) ?>;
function onFail() {
$("#status").html(<?= json_encode(tr('wifi.conn.telemetry_lost')) ?>);
$("#status").html(<?= json_encode(tr('wificonn.telemetry_lost')) ?>);
$('.anim-dots').addClass('hidden');
}

@ -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"; ?>
@ -11,10 +16,15 @@
<?php require __DIR__ . "/help/sgr_colors.php"; ?>
<?php require __DIR__ . "/help/cmd_cursor.php"; ?>
<?php require __DIR__ . "/help/cmd_screen.php"; ?>
<?php require __DIR__ . "/help/cmd_d2d.php"; ?>
<?php require __DIR__ . "/help/cmd_system.php"; ?>
<?php require __DIR__ . "/help/iocontrol.php"; ?>
<script>
function hpfold(yes) {
$('.fold').toggleClass('expanded', !!yes);
}
// show theme colors - but this is a static page, so we don't know the current theme.
themes.themePreview(1)
</script>

@ -21,6 +21,8 @@
<li>`A` - UK ASCII: # replaced with £</li>
<li>`0` - Symbols and basic line drawing (standard DEC alternate character set)</li>
<li>`1` - Symbols and advanced line drawing (based on DOS codepage 437, ESPTerm specific)</li>
<li>`2` - Block characters and thick line drawing (ESPTerm specific)</li>
<li>`3` - Extra line drawing (ESPTerm specific)</li>
</ul>
<p>
@ -28,20 +30,34 @@
stays unchanged.
</p>
<script>
function bchst(start, str) {
var ar = str.split(' ');
for(var i=0;i<ar.length;i++) {
var a = String.fromCharCode(start+i);
var r = ar[i];
document.write('<div'+(r===a?' class="none"':'')+'><span>'+(start+i)+'</span><span>'+$.htmlEscape(a)+'</span><span>'+$.htmlEscape(r)+'</span></div>');
}
}
</script>
<?php
$codepages = load_esp_charsets();
foreach($codepages as $name => $cp) {
echo "<h4>Codepage `$name`</h4>\n";
echo '<div class="charset">';
foreach($cp as $point) {
$dis = $point[1]==$point[2]?' class="none"' : '';
echo "<div$dis><span>$point[0]</span><span>$point[1]</span><span>$point[2]</span></div>";
}
echo '</div>';
$t = implode("\x01", $cp['chars']);
$t = json_encode($t, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
$t = str_replace('\u0001', " ", $t); // space is never included
$t = htmlspecialchars($t,ENT_HTML5);
echo '<script>bchst('.$cp['start'].','.$t.')';
echo '</script></div>';
}
?>
<h3>Switching commands</h3>
<h3>Codepage switching commands</h3>
<p>
There are two character set slots, G0 and G1.

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

@ -0,0 +1,92 @@
<div class="Box fold">
<h2>Commands: Networking</h2>
<div class="Row v">
<p>
ESPTerm implements commands for device-to-device messaging and for requesting external
servers. This can be used e.g. for remote control, status reporting or data upload / download.
</p>
<p>
Networking commands use the format `\e^...\a`, a Privacy Message (PM).
PM is similar to OSC, which uses `]` in place of `^`. The PM payload (text between `\e^` and `\a`)
must be shorter than 256 bytes, and should not contain any control characters (ASCII &lt; 32).
</p>
<h3>Device-to-device Messaging</h3>
<p>
To send a message to another ESPTerm module, use: `\e^M;<i>DestIP</i>;<i>message</i>\a`.
</p>
<p>
This command sends a POST request to `http://<i>&lt;DestIP&gt;</i>/api/v1/msg`.
The IP address may be appended by a port, if needed (eg. :8080). In addition to POST,
a GET request can also be used. In that case, any GET arguments (`/api/v1/msg?<i>arguments</i>`)
will be used instead of the request body. This is intended for external access
when sending POST requests is not convenient.
</p>
<p>
Each ESPTerm listens for such requests and relays them to UART:
`\e^m;<i>SrcIP</i>;L=<i>length</i>;<i>message</i>\a`, with _length_ being the byte length of
_message_, as ASCII.
</p>
<p>
Notice a pattern with the first letter: capital is always a command, lower case a response.
This is followed with the HTTP commands and any networking commands added in the future.
</p>
<p>
*Example:* Node 192.168.0.10 sends a message to 192.168.0.19: `\e^M;192.168.0.19;Hello\a`.
Node 192.168.0.19 receives `\e^m;192.168.0.10;L=5;Hello\a` on the UART. Note that the IP
address in the reception message is that of the first node, thus it can be used to send a message back.
</p>
<h3>External HTTP requests</h3>
<p>
To request an external server, use `\e^H;<i>method</i>;<i>options</i>;<i>url</i>\n<i>body</i>\a`.
</p>
<ul>
<li>`_method_` - can be any usual HTTP verb, such as `GET`, `POST`, `PUT`, `HEAD`.
<li>`_options_` - is a comma-separated list of flags and parameters:
<ul>
<li>`H` - get response headers
<li>`B` - get response body
<li>`X` - ignore the response, return nothing
<li>`N=<i>nonce</i>` - a custom string that will be added in the options field of the response message.
Use this to keep track of which request a response belongs to.
<li>`T=<i>ms</i>` - request timeout (default 5000~ms), in milliseconds
<li>`L=<i>bytes</i>` - limit response length (default 0 = don't limit). Applies to the head, body, or both combined, depending on the `H` and `B` flags
<li>`l=<i>bytes</i>` - limit the response buffer size (default 5000~B).
This can reduce RAM usage, however it shouldn't be set too small, as this buffer
is used for both headers and the response body.
</ul>
<li>`_url_` - full request URL, including `http://`. Port may be specified if different from :80,
and GET arguments may be appended to the URL if needed.
<li>`_body_` - optional, separated from `_url_` by a single line feed character (`\n`).
This can be used for POST and PUT requests. Note: the command may be truncated to the
maximum total length of 256 characters if too long.
</ul>
<p>The response has the following format: `\e^h;<i>status</i>;<i>options</i>;<i>response</i>\a`</p>
<ul>
<li>`_status_` - a HTTP status code, eg. 200 is OK, 404 Not found.
<li>`_options_` - similar to those in the request, here describing the response data.
This field can contain comma-separated `B`, `H` and `L=<i>bytes</i>` and `N=<i>nonce</i>`.
<li>`_response_` - the response, as requested. If both headers and body are received,
they will be separated by an empty line (i.e. `\r\n\r\n`). Response can be up to several
kilobytes long, depending on the `L=` and `l=` options.
</ul>
<p>
*Example:* `\e^H;GET;B;http://wtfismyip.com/text\a` - get the body of a web page
(wtfismyip.com is a service that sends back your IP address).
A response could be `\e^h;200;B,L=11;80.70.60.50\a`.
</p>
</div>
</div>

@ -8,50 +8,43 @@
If an argument is left out, it's treated as 0 or 1, depending on what makes sense for the command.
</p>
<h3>Erasing &amp; Inserting</h3>
<table class="ansiref w100">
<thead><tr><th>Code</th><th>Meaning</th></tr></thead>
<tbody>
<tr>
<td>
`\e[<i>m</i>J`
</td>
<td>`\e[<i>m</i>J`</td>
<td>
Clear part of screen. _m_: 0 - from cursor, 1 - to cursor, 2 - all
</td>
</tr>
<tr>
<td>
`\e[<i>m</i>K`
</td>
<td>`\e[<i>m</i>K`</td>
<td>
Erase part of line. _m_: 0 - from cursor, 1 - to cursor, 2 - all
</td>
</tr>
<tr>
<td>
`\e[<i>n</i>X`</td>
<td>`\e[<i>n</i>X`</td>
<td>
Erase _n_ characters in line.
</td>
</tr>
<tr>
<td>
<code>
<td><code>
\e[<i>n</i>L \\
\e[<i>n</i>M
</code>
</td>
</code></td>
<td>
Insert (`L`) or delete (`M`) _n_ lines. Following lines are pulled up or pushed down.
</td>
</tr>
<tr>
<td>
<code>
<td><code>
\e[<i>n</i>@ \\
\e[<i>n</i>P
</code>
</td>
</code></td>
<td>
Insert (`@`) or delete (`P`) _n_ characters. The rest of the line is pulled left or pushed right.
Characters going past the end of line are lost.
@ -59,5 +52,83 @@
</tr>
</tbody>
</table>
<h3>Supersized lines</h3>
<table class="ansiref w100">
<thead><tr><th>Code</th><th>Meaning</th></tr></thead>
<tbody>
<tr>
<td>`\e#1`, `\e#2`</td>
<td>
Make the current line part of a double-height line.
Use `1` for the top, `2` for the bottom half.
</td>
</tr>
<tr>
<td>`\e#3`, `\e#4`</td>
<td>
Make the current line part of a double-width, double-height line.
Use `3` for the top, `4` for the bottom half.
</td>
</tr>
<tr>
<td>`\e#6`</td>
<td>
Make the current line double-width.
</td>
</tr>
<tr>
<td>`\e#5`</td>
<td>
Reset the current line to normal size.
</td>
</tr>
</tbody>
</table>
<h3>Other</h3>
<table class="ansiref w100">
<thead><tr><th>Code</th><th>Meaning</th></tr></thead>
<tbody>
<tr>
<td>`\ec`</td>
<td>
Clear screen, reset attributes and cursor. This command also restores the default
screen size, title, button labels and messages and the background URL.
</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>`\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[<i>n</i>b`</td>
<td>
Repeat last printed characters _n_ times (moving cursor and using the current style).
</td>
</tr>
<tr>
<td>`\e#8`</td>
<td>
Reset all screen attributes to default and fill the screen with the letter "E". This was
historically used for aligning CRT displays, now can be useful e.g. for testing erasing commands.
</td>
</tr>
</tbody>
</table>
</div>
</div>

@ -8,14 +8,26 @@
Those changes are not retained after restart.
</p>
<h3>Single-byte commands &amp; queries</h3>
<table class="ansiref w100">
<thead><tr><th>Code</th><th>Meaning</th></tr></thead>
<tbody>
<tr>
<td>`\ec`</td>
<td>_CAN_ (24)</td>
<td>
This ASCII code is sent by ESPTerm when it becomes ready to receive commands.
When this code is received on the UART, it means ESPTerm has restarted and is ready.
Use this to detect spontaneous restarts which require a full screen repaint.
As a control character sent to ESPTerm, CAN aborts any currently received commands
and clears the parser.
</td>
</tr>
<tr>
<td>_ENQ_ (5)</td>
<td>
Clear screen, reset attributes and cursor.
The screen size, title and button labels remain unchanged.
ESPTerm responds to this control characters with an "answerback message".
This message contains the curretn version, unique ID, and the IP address if in Client mode.
</td>
</tr>
<tr>
@ -25,12 +37,21 @@
Can be used to check if the terminal has booted up and is ready to receive commands.
</td>
</tr>
</tbody>
</table>
<h3>Setting parameters</h3>
<table class="ansiref w100">
<thead><tr><th>Code</th><th>Meaning</th></tr></thead>
<tbody>
<tr>
<td>_CAN_ (24)</td>
<td>`\e[<i>n</i> q`</td>
<td>
This ASCII code is not a command, but is sent by ESPTerm when it becomes ready to receive commands.
When this code is received on the UART, it means ESPTerm has restarted and is ready. Use this to detect
spontaneous restarts which require a full screen repaint.
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>
@ -38,64 +59,119 @@
<td>Set screen title to _t_ (this is a standard OSC command)</td>
</tr>
<tr>
<td>`\e]27;1;<i>u</i>\a`</td>
<td>
<code>
\e]<i>80+n</i>;<i>t</i>\a
</code>
Set background image to URL _u_ (including protocol)
that can be resolved by the user's browser. The image will be scaled
to fit the screen, preserving aspect ratio. A certain border must be added
to account for the screen margins. Use empty string to disable the image feature.
Note that this *won't work for users connected to the built-in AP*.
</td>
</tr>
<tr>
<td>`\e]27;2;<i>n</i>\a`</td>
<td>
Set label for button _n_ = 1-5 (code 81-85) to _t_ - e.g.`\e]81;Yes\a`
sets the first button text to "Yes".
Set number of visible buttons to _n_ (0-5). To hide/show the entire buttons bar,
use the dedicated hiding commands (see below)
</td>
</tr>
<tr>
<td><code>
\e]28;<i>x</i>;<i>t</i>\a
</code></td>
<td>
<code>
\e]<i>90+n</i>;<i>m</i>\a
</code>
Set label for button _x_ (1-5) to _t_ - e.g.`\e]28;1;Yes\a`
sets the first button text to "Yes".
</td>
</tr>
<tr>
<td><code>
\e]29;<i>x</i>;<i>m</i>\a
</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.
Set message for button _x_ (1-5) to _m_ - e.g.`\e]29;3;+\a`
sets the 3rd button to send "+" when pressed. The message can be up to
10 bytes long.
</td>
</tr>
<tr>
<td><code>
\e]30;<i>x</i>;<i>c</i>\a
</code></td>
<td>
<code>
Set button _x_ (1-5) color to _c_ - e.g.`\e]30;2;#00FF00\a`
makes the 2nd button green. Supported are SGR colors 1-255
and TrueColor in the format `#RRGGBB`. Use 0 to
reset to the default color.
</td>
</tr>
<tr>
<td><code>
\e[?800h \\
\e[?800l
</code>
</td>
</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>
<td>
<code>
<td><code>
\e[?801h \\
\e[?801l
</code>
</td>
</code></td>
<td>
Show (`h`) or hide (`l`) menu/help links under the screen.
</td>
</tr>
<tr>
<td><code>
\e[?2004h \\
\e[?2004l
</code></td>
<td>
<code>
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[12h \\
\e[12l
</code>
</td>
</code></td>
<td>
Enable (`h`) or disable (`l`) Send-Receive Mode (SRM).
SRM is the opposite of Local Echo, meaning `\e[12h` disables and `\e[12l` enables Local Echo.
</td>
</tr>
</tbody>
</table>
<h3>Other</h3>
<table class="ansiref w100">
<thead><tr><th>Code</th><th>Meaning</th></tr></thead>
<tbody>
<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>`\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>
<td><code>
\e[?<i>n</i>s \\
\e[?<i>n</i>r
</code></td>
<td>
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>
</tbody>
</table>

@ -21,6 +21,7 @@
Here are some examples of control key codes:
</p>
<div class="tscroll">
<table>
<thead><tr><th>Key</th><th>Code</th><th>Key</th><th>Code</th></tr></thead>
<tr>
@ -108,6 +109,7 @@
<td>ASCII 1-26</td>
</tr>
</table>
</div>
<h3>Action buttons</h3>

@ -0,0 +1,39 @@
<div class="Box fold">
<h2>Remote GPIO Control</h2>
<div class="Row v">
<p>
ESPTerm provides a simple API to remotely control and read GPIO pins GPIO2, GPIO4, and GPIO5.
The main use of this API is to remotely reset a device that communicates with ESPTerm
through the UART.
</p>
<p>
GPIO2 is normally used for debug UART, so when used as GPIO, debug logging is disabled. You
can configure the pin functions in <a href="<?= url('cfg_system') ?>">System Settings</a>.
</p>
<p>
The GPIO control endpoint is `/api/v1/gpio`, with optional GET arguments:
</p>
<ul>
<li>`do2=<i>x</i>` - set GPIO2 level. <i>x</i> can be `0`, `1`, or `t` to toggle the pin.
<li>`do4=<i>x</i>` - set GPIO4 level
<li>`do5=<i>x</i>` - set GPIO5 level
<li>`pulse=<i>ms</i>` - the command starts a pulse. After the given amount of time
(milliseconds) has elapsed, the pins are set to the opposite levels than what was specified
(in the case of toggle, the original pin state)
</ul>
<p>
A quick example: <a href="/api/v1/gpio?do4=1&amp;pulse=500">`/api/v1/gpio?do4=1&amp;pulse=500`</a>
sends a 500ms long positive pulse on GPIO4.
</p>
<p>
The GPIO endpoint always returns a JSON object like this: `{"io2":0,"io4":1,"io5":0}`, showing
the current input levels. Input reading works always, regardless of the GPIO settings.
</p>
</div>
</div>

@ -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,65 +1,112 @@
<div class="Box fold theme-0">
<h2>Commands: Color SGR</h2>
<div class="Box fold">
<h2>Commands: Color Attributes</h2>
<div class="Row v">
<p>
Colors are set using SGR commands (like `\e[10;20;30m`). The following tables list the SGR codes to use.
Selected colors are used for any new text entered, as well as for empty space when using line and screen clearing commands.
The configured default colors can be restored using SGR 39 for foreground and SGR 49 for background.
Colors are set using SGR commands (like `\e[30;47m`). The following tables list the SGR
codes to use. Selected colors are used for any new text entered, as well as for empty
space when using clearing commands (except screen reset `\ec`, which first clears all
style attriutes. The configured default colors can be restored using `SGR 39` for
foreground and `SGR 49` for background.
</p>
<p>
The actual color representation depends on a color theme which
The actual color representation of the basic 16 colors depends on a color theme which
can be selected in <a href="<?= url('cfg_term') ?>">Terminal Settings</a>.
</p>
<p>
Background image can be set using `\e]70;<i>url</i>\a` (see section System Functions).
</p>
<h3>Foreground colors</h3>
<div class="colorprev">
<span class="bg7 fg0">30</span>
<span class="bg0 fg1">31</span>
<span class="bg0 fg2">32</span>
<span class="bg0 fg3">33</span>
<span class="bg0 fg4">34</span>
<span class="bg0 fg5">35</span>
<span class="bg0 fg6">36</span>
<span class="bg0 fg7">37</span>
<span data-bg="0" data-fg="0" style="text-shadow: 0 0 3px white;">30</span><!--
--><span data-bg="0" data-fg="1">31</span><!--
--><span data-bg="0" data-fg="2">32</span><!--
--><span data-bg="0" data-fg="3">33</span><!--
--><span data-bg="0" data-fg="4">34</span><!--
--><span data-bg="0" data-fg="5">35</span><!--
--><span data-bg="0" data-fg="6">36</span><!--
--><span data-bg="0" data-fg="7">37</span>
</div>
<div class="colorprev">
<span class="bg0 fg8">90</span>
<span class="bg0 fg9">91</span>
<span class="bg0 fg10">92</span>
<span class="bg0 fg11">93</span>
<span class="bg0 fg12">94</span>
<span class="bg0 fg13">95</span>
<span class="bg0 fg14">96</span>
<span class="bg0 fg15">97</span>
<span data-bg="0" data-fg="8">90</span><!--
--><span data-bg="0" data-fg="9">91</span><!--
--><span data-bg="0" data-fg="10">92</span><!--
--><span data-bg="0" data-fg="11">93</span><!--
--><span data-bg="0" data-fg="12">94</span><!--
--><span data-bg="0" data-fg="13">95</span><!--
--><span data-bg="0" data-fg="14">96</span><!--
--><span data-bg="0" data-fg="15">97</span>
</div>
<h3>Background colors</h3>
<div class="colorprev">
<span class="bg0 fg15">40</span>
<span class="bg1 fg15">41</span>
<span class="bg2 fg15">42</span>
<span class="bg3 fg0">43</span>
<span class="bg4 fg15">44</span>
<span class="bg5 fg15">45</span>
<span class="bg6 fg15">46</span>
<span class="bg7 fg0">47</span>
<span data-bg="0" data-fg="15">40</span><!--
--><span data-bg="1" data-fg="15">41</span><!--
--><span data-bg="2" data-fg="15">42</span><!--
--><span data-bg="3" data-fg="0">43</span><!--
--><span data-bg="4" data-fg="15">44</span><!--
--><span data-bg="5" data-fg="15">45</span><!--
--><span data-bg="6" data-fg="15">46</span><!--
--><span data-bg="7" data-fg="0">47</span>
</div>
<div class="colorprev">
<span class="bg8 fg15">100</span>
<span class="bg9 fg0">101</span>
<span class="bg10 fg0">102</span>
<span class="bg11 fg0">103</span>
<span class="bg12 fg0">104</span>
<span class="bg13 fg0">105</span>
<span class="bg14 fg0">106</span>
<span class="bg15 fg0">107</span>
<span data-bg="8" data-fg="15">100</span><!--
--><span data-bg="9" data-fg="0">101</span><!--
--><span data-bg="10" data-fg="0">102</span><!--
--><span data-bg="11" data-fg="0">103</span><!--
--><span data-bg="12" data-fg="15">104</span><!--
--><span data-bg="13" data-fg="0">105</span><!--
--><span data-bg="14" data-fg="0">106</span><!--
--><span data-bg="15" data-fg="0">107</span>
</div>
<h3>256-color palette</h3>
<p>
ESPTerm supports in total 256 standard colors. The dark and bright basic colors are
numbered 0-7 and 8-15. To use colors higher than 15 (or 0-15 using this simpler numbering),
send `CSI 38 ; 5 ; <i>n</i> m`, where `n` is the color to set. Use `CSI 48 ; 5 ; <i>n</i> m` for background colors.
</p>
<div class="colorprev" id="pal256">
</div>
</div>
</div>
<script>
$.ready(function() {
var wrap = qs('#pal256');
var table = themes.buildColorTable();
for (var i = 0; i < 256; i++) {
var el = document.createElement('span')
var clr = table[i]
if (i < 16) {
clr = themes.themes[1][i]
}
el.style.color = 'black'
if ( i < 7 || i == 12 || i == 8 ||
(i >= 16 && i <= 33) ||
(i >= 52 && i <= 69) ||
(i >= 88 && i <= 99) ||
(i >= 124 && i <= 129)) {
el.style.color = 'white'
}
el.textContent = ""+i
el.style.backgroundColor = clr
wrap.appendChild(el)
if (i==15||(i-16)%24==23) {
el = document.createElement('br')
wrap.appendChild(el)
}
}
});
</script>

@ -1,9 +1,9 @@
<div class="Box fold">
<h2>Commands: Style SGR</h2>
<h2>Commands: Style Attributes</h2>
<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>

@ -12,15 +12,14 @@
appear on the screen. Set _Parser Timeout = 0_ in <a href="<?= url('cfg_term') ?>">Terminal Settings</a>
to be able to manually enter escape sequences.
<li>There is very little RAM available to the webserver, and it can support at most 4 connections at the same time.
Each terminal session (open window with the terminal screen) uses one persistent connection for screen updates.
*Avoid leaving unused windows open*, or either the RAM or connections may be exhausted.
<li>Use Ctrl+F12 to open a screen debug panel. Additional debugging can be enabled in the
<a href="<?= url('cfg_term') ?>">Terminal Settings</a> (near the bottom).
<li>*For best performance*, use the module in Client mode (connected to external network) and minimize the number
of simultaneous connections. Enabling AP consumes extra RAM because the DHCP server and Captive Portal
DNS server are started.
<li>In AP mode, *check that the WiFi channel used is clear*; interference may cause flaky connection.
<li>*Check that the WiFi channel used is clear*; interference may cause flaky connection.
A good mobile app to use for this is
<a href="https://play.google.com/store/apps/details?id=com.farproc.wifi.analyzer">WiFi Analyzer (Google Play)</a>.
Adjust the hotspot strength and range using the _Tx Power setting_.

@ -1,62 +1,70 @@
<?php if (!DEBUG): ?>
<script>
// Workaround for badly loaded page
setTimeout(function() {
if (typeof termInit == 'undefined' || typeof $ == 'undefined') {
console.error("Page load failed, refreshing…");
location.reload(true);
if (typeof termInit == 'undefined') {
console.error("Page load failed, refreshing…")
location.reload(true)
}
}, 3000);
</script>
<?php endif; ?>
<div class="Modal light hidden" id="fu_modal">
<div id="fu_form" class="Dialog">
<div class="fu-content">
<h2>Text Upload</h2>
<h2><?= tr('upload.title') ?></h2>
<p>
<label for="fu_file">Load a text file:</label>
<label for="fu_file"><?= tr('upload.prompt') ?></label>
<input type="file" id="fu_file" accept="text/*" /><br>
<textarea id="fu_text"></textarea>
</p>
<p>
<label for="fu_crlf">Line Endings:</label>
<label for="fu_crlf"><?= tr('upload.endings') ?></label>
<select id="fu_crlf">
<option value="CR">CR (Enter key)</option>
<option value="CRLF">CR LF (Windows)</option>
<option value="LF">LF (Linux)</option>
<option value="CR"><?= tr('upload.endings.cr') ?></option>
<option value="CRLF"><?= tr('upload.endings.crlf') ?></option>
<option value="LF"><?= tr('upload.endings.lf') ?></option>
</select>
</p>
<p>
<label for="fu_delay">Line Delay (ms):</label>
<label for="fu_delay"><?= tr('upload.chunk_delay') ?></label>
<input id="fu_delay" type="number" value=1 min=0>
</p>
<p>
<label for="fu_chunk"><?= tr('upload.chunk_size') ?></label>
<input id="fu_chunk" type="number" value=0 min=0 max=100>
</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;
<i class="fu-prog-box">Upload: <span id="fu_prog"></span></i>
<button id="term-fu-start" class="icn-ok x-fu-go"><?= tr('start') ?></button>&nbsp;
<button id="term-fu-close" class="icn-cancel x-fu-cancel"><?= tr('cancel') ?></button>&nbsp;
<i class="fu-prog-box"><?= tr('upload.progress') ?> <span id="fu_prog"></span></i>
</div>
</div>
</div>
<h1><!-- Screen title gets loaded here by JS --></h1>
<h1 id="screen-title">ESPTerm</h1>
<a href="#" id="term-fit-screen" class="mq-tablet-max"><i id="resize-button-icon" class="icn-resize-small"></i></a>
<div id="term-wrap">
<div id="screen" class="theme-%theme%"></div>
<div id="action-buttons">
<button data-n="1"></button><!--
--><button data-n="2"></button><!--
--><button data-n="3"></button><!--
--><button data-n="4"></button><!--
--><button data-n="5"></button>
<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"><?= tr('copy') ?></button>
</div>
<div class="screen-margin top"></div>
<div class="screen-margin left"></div>
<div class="screen-margin right"></div>
<div class="screen-margin bottom"></div>
</div>
</div>
<textarea id="softkb-input" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
<div id="action-buttons" class="hidden"><!-- JS, buttons --></div>
</div>
<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-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 +74,14 @@
<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);
});
} catch(e) {
termInit({ allFn: !!+'%want_all_fn%', });
} catch (e) {
console.error(e);
<?php if (!DEBUG): ?>
console.error("Fail, reloading in 3s…");
setTimeout(function() {
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;
}
}

@ -7,7 +7,7 @@
@import "utils";
$form-label-w: 160px;
$form-label-w: 220px;
$form-label-gap: 8px;
$form-field-w: 250px;
@ -20,7 +20,8 @@ $c-form-highlight-a: #2ea1f9;
$c-modal-bg: #242426;
$screen-stack: "DejaVu Sans Mono", "Liberation Mono", "Inconsolata", monospace;
$font-stack: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif;
$screen-stack: "DejaVu Sans Mono", "Liberation Mono", "Inconsolata", "Menlo", monospace;
@function dist($x) {
@return modular-scale($x, 1rem, $golden);
@ -55,3 +56,5 @@ $screen-stack: "DejaVu Sans Mono", "Liberation Mono", "Inconsolata", monospace;
@include media($tablet-max) {
.mq-normal-min { display: none !important; }
}
@import "print_override";

@ -2,10 +2,14 @@
#{$all-text-inputs}, select, label.select-wrap {
width: $form-field-w;
margin-right: 3px;
}
input[type="number"], input.short, select.short {
width: $form-field-w/2;
width: 123.5px;
}
input.tiny, select.tiny {
width: 90px;
}
#{$all-text-inputs}, select {
@ -13,7 +17,7 @@ input[type="number"], input.short, select.short {
border-bottom: 2px solid $c-form-highlight;
background-color: $c-form-field-bg;
color: $c-form-field-fg;
padding: 6px;
padding: 4px 6px;
line-height: 1em;
//outline: 0 none !important;
//-moz-outline: 0 none !important;

@ -12,6 +12,21 @@ form { @include naked(); }
}
}
.Box.message {
font-weight: bold;
color: #0fca50;
transition: opacity linear 1s;
opacity: 1;
&.hiding {
opacity: 0;
}
}
.Box.errmessage {
@extend .Box.message;
color: crimson;
}
.Row {
vertical-align: middle;
margin: 12px auto;
@ -21,6 +36,18 @@ form { @include naked(); }
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
.SubRow {
display: flex;
flex-direction: row;
@include media($phone) {
flex-direction: column;
margin: 6px auto;
width: 100%;
}
}
&:first-child {
margin-top: 0;
@ -60,6 +87,10 @@ form { @include naked(); }
}
}
&.buttons2 {
display:block;
}
&.centered {
justify-content: center;
}
@ -84,6 +115,14 @@ form { @include naked(); }
border-top: 2px solid rgba(255, 255, 255, 0.1);
}
.filewrap {
background: $c-form-field-bg;
padding: 6px 10px;
border-radius: 3px;
border: 1px solid #666666;
margin-right: .5em;
}
textarea {
display: inline-block;
vertical-align: top;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save