term now supported and subcriptions now properly lifted
This commit is contained in:
parent
cf3c395850
commit
b285e2364e
3
Makefile
3
Makefile
|
@ -29,10 +29,11 @@ $(PUBLIC_DIR)/index.html: $(FRONTEND_DIR)/index.html $(PUBLIC_DIR)
|
||||||
.PHONY: frontend backend
|
.PHONY: frontend backend
|
||||||
|
|
||||||
frontend: $(PUBLIC_DIR)/index.html $(PUBLIC_DIR)
|
frontend: $(PUBLIC_DIR)/index.html $(PUBLIC_DIR)
|
||||||
|
mkdir -p $(PUBLIC_DIR)/assets
|
||||||
make -C $(FRONTEND_DIR)
|
make -C $(FRONTEND_DIR)
|
||||||
cp $(FRONTEND_DIR)/elm.min.js $(PUBLIC_DIR)/
|
cp $(FRONTEND_DIR)/elm.min.js $(PUBLIC_DIR)/
|
||||||
cp $(FRONTEND_DIR)/src/ports.websocket.js $(PUBLIC_DIR)/
|
cp $(FRONTEND_DIR)/src/ports.websocket.js $(PUBLIC_DIR)/
|
||||||
mkdir -p $(PUBLIC_DIR)/assets
|
cp $(FRONTEND_DIR)/assets/CourierPrime-Regular.ttf $(PUBLIC_DIR)/assets/
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
$(CARGO_BUILD) --manifest-path=$(BACKEND_DIR)/Cargo.toml
|
$(CARGO_BUILD) --manifest-path=$(BACKEND_DIR)/Cargo.toml
|
||||||
|
|
27
README.md
27
README.md
|
@ -1,6 +1,5 @@
|
||||||
# About
|
# About
|
||||||
Example demonstrating how one might architect a single page application
|
A tool that lets you remotely try new exotic hardware such as Tenstorrent Wormhole or SiFive P550 Dev board from the comfort of your browser.
|
||||||
Elm app.
|
|
||||||
|
|
||||||
|
|
||||||
# Building
|
# Building
|
||||||
|
@ -13,15 +12,19 @@ Now open `http://127.0.0.1:8080` in your browser.
|
||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
- [x] Add Makefile
|
- [ ] Binding to `0.0.0.0` should not be default behavior.
|
||||||
- [ ] `EventHandlers.onMessage` should inject successfully decoded message into
|
- [ ] Rename `term.rs` to `terminal.rs` unless there is a good reason not to.
|
||||||
Msg type directly...
|
- [ ] Move to `actix-ws`
|
||||||
- [ ] Run `uglify` twice as per [this link](https://github.com/rtfeldman/elm-spa-example/tree/master?tab=readme-ov-file#production-build)
|
- [ ] Use [`spawn_local`][discord_chat] instead of `addr.do_send`
|
||||||
- [ ] Frontend should initiate with TimeRequest
|
- [ ] Re-factor if and else branches of `term::msg_handler`
|
||||||
- [ ] JSONify backend code for all send/receive
|
- [ ] Stopping the websocket should terminate terminal. The ergonomic way
|
||||||
- [ ] Backend should only communicate over websocket
|
to do this is to have each widget provision its own stop handler.
|
||||||
|
- [ ] Reduce warnings, especially from unused imports.
|
||||||
|
- [ ] Implement logic for `CloseTerm` UpMsg for terminal
|
||||||
|
- [ ] Implement terminal
|
||||||
|
- [ ] Start by simply adding new term page
|
||||||
- [ ] Close websocket after 15s of no response(from frontend) to websocket pings
|
- [ ] Close websocket after 15s of no response(from frontend) to websocket pings
|
||||||
- [ ] Implement dark mode
|
- [ ] Implement dark mode
|
||||||
- [ ] Add GPLV3 License
|
|
||||||
- [ ] Add `make release` target that is nix ready...
|
|
||||||
- [ ] Add `default.nix`
|
[discord_chat]: https://discord.com/channels/771444961383153695/771447545154371646/1327308910654259291
|
||||||
|
|
356
backend/Cargo.lock
generated
356
backend/Cargo.lock
generated
|
@ -113,7 +113,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb"
|
checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.93",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -248,7 +248,7 @@ dependencies = [
|
||||||
"actix-router",
|
"actix-router",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.93",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -259,7 +259,7 @@ checksum = "b6ac1e58cded18cb28ddc17143c4dea5345b3ad575e14f32f66e4054a56eb271"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.93",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -299,6 +299,29 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "alacritty_terminal"
|
||||||
|
version = "0.24.2"
|
||||||
|
source = "git+https://github.com/alacritty/alacritty?rev=53395536aa4ebebcbc0431e7336c2a6857efcff5#53395536aa4ebebcbc0431e7336c2a6857efcff5"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"bitflags",
|
||||||
|
"home",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"miow",
|
||||||
|
"parking_lot",
|
||||||
|
"piper",
|
||||||
|
"polling",
|
||||||
|
"regex-automata",
|
||||||
|
"rustix-openpty",
|
||||||
|
"serde",
|
||||||
|
"signal-hook",
|
||||||
|
"unicode-width",
|
||||||
|
"vte",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alloc-no-stdlib"
|
name = "alloc-no-stdlib"
|
||||||
version = "2.0.4"
|
version = "2.0.4"
|
||||||
|
@ -329,6 +352,12 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atomic-waker"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
@ -343,11 +372,15 @@ dependencies = [
|
||||||
"actix-files",
|
"actix-files",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"actix-web-actors",
|
"actix-web-actors",
|
||||||
|
"alacritty_terminal",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"elm_rs",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"log",
|
"log",
|
||||||
|
"once_cell",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -362,7 +395,7 @@ dependencies = [
|
||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
"object",
|
"object",
|
||||||
"rustc-demangle",
|
"rustc-demangle",
|
||||||
"windows-targets",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -376,6 +409,9 @@ name = "bitflags"
|
||||||
version = "2.6.0"
|
version = "2.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
|
@ -462,7 +498,16 @@ dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-targets",
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "concurrent-queue"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -531,6 +576,12 @@ dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cursor-icon"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.3.11"
|
version = "0.3.11"
|
||||||
|
@ -550,7 +601,7 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"syn",
|
"syn 2.0.93",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -571,7 +622,28 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.93",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "elm_rs"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "043b06877e194324cabd54e3f0eef99b0131d80b6b6073cb22697bed97862aa3"
|
||||||
|
dependencies = [
|
||||||
|
"elm_rs_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "elm_rs_derive"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b3e657caae2215ca5380ae58302ec2ff1b6c87a55aa9137444972cd9b3540bf"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -602,6 +674,22 @@ version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.0.35"
|
version = "1.0.35"
|
||||||
|
@ -633,6 +721,12 @@ version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-io"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
|
@ -709,12 +803,27 @@ version = "0.15.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
|
checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "home"
|
||||||
|
version = "0.5.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.12"
|
version = "0.2.12"
|
||||||
|
@ -888,7 +997,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.93",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -976,6 +1085,12 @@ version = "0.2.169"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.4.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
version = "0.7.4"
|
version = "0.7.4"
|
||||||
|
@ -1058,6 +1173,15 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miow"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "359f76430b20a79f9e20e115b3428614e654f04fab314482fc0fda0ebd3c6044"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -1108,7 +1232,7 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall",
|
"redox_syscall",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"windows-targets",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1135,12 +1259,38 @@ version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "piper"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
|
||||||
|
dependencies = [
|
||||||
|
"atomic-waker",
|
||||||
|
"fastrand",
|
||||||
|
"futures-io",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
|
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "polling"
|
||||||
|
version = "3.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"concurrent-queue",
|
||||||
|
"hermit-abi",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rustix",
|
||||||
|
"tracing",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "powerfmt"
|
name = "powerfmt"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
@ -1263,6 +1413,31 @@ dependencies = [
|
||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "0.38.43"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"errno",
|
||||||
|
"itoa",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix-openpty"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a25c3aad9fc1424eb82c88087789a7d938e1829724f3e4043163baf0d13cfc12"
|
||||||
|
dependencies = [
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"rustix",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
|
@ -1298,7 +1473,7 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.93",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1342,6 +1517,16 @@ version = "1.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook"
|
||||||
|
version = "0.3.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"signal-hook-registry",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.2"
|
version = "1.4.2"
|
||||||
|
@ -1382,6 +1567,17 @@ version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "1.0.109"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.93"
|
version = "2.0.93"
|
||||||
|
@ -1401,7 +1597,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.93",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1456,9 +1652,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.42.0"
|
version = "1.43.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
|
checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -1522,6 +1718,12 @@ version = "1.0.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
|
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.1.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.4"
|
version = "2.5.4"
|
||||||
|
@ -1545,6 +1747,12 @@ version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "v_htmlescape"
|
name = "v_htmlescape"
|
||||||
version = "0.15.8"
|
version = "0.15.8"
|
||||||
|
@ -1557,6 +1765,30 @@ version = "0.9.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vte"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a0b683b20ef64071ff03745b14391751f6beab06a54347885459b77a3f2caa5"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cursor-icon",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"utf8parse",
|
||||||
|
"vte_generate_state_changes",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vte_generate_state_changes"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.0+wasi-snapshot-preview1"
|
version = "0.11.0+wasi-snapshot-preview1"
|
||||||
|
@ -1584,7 +1816,7 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.93",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1606,7 +1838,7 @@ checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.93",
|
||||||
"wasm-bindgen-backend",
|
"wasm-bindgen-backend",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
@ -1632,7 +1864,16 @@ version = "0.52.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-targets",
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.48.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.48.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1641,7 +1882,7 @@ version = "0.52.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-targets",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1650,7 +1891,22 @@ version = "0.59.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-targets",
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||||
|
dependencies = [
|
||||||
|
"windows_aarch64_gnullvm 0.48.5",
|
||||||
|
"windows_aarch64_msvc 0.48.5",
|
||||||
|
"windows_i686_gnu 0.48.5",
|
||||||
|
"windows_i686_msvc 0.48.5",
|
||||||
|
"windows_x86_64_gnu 0.48.5",
|
||||||
|
"windows_x86_64_gnullvm 0.48.5",
|
||||||
|
"windows_x86_64_msvc 0.48.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1659,28 +1915,46 @@ version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows_aarch64_gnullvm",
|
"windows_aarch64_gnullvm 0.52.6",
|
||||||
"windows_aarch64_msvc",
|
"windows_aarch64_msvc 0.52.6",
|
||||||
"windows_i686_gnu",
|
"windows_i686_gnu 0.52.6",
|
||||||
"windows_i686_gnullvm",
|
"windows_i686_gnullvm",
|
||||||
"windows_i686_msvc",
|
"windows_i686_msvc 0.52.6",
|
||||||
"windows_x86_64_gnu",
|
"windows_x86_64_gnu 0.52.6",
|
||||||
"windows_x86_64_gnullvm",
|
"windows_x86_64_gnullvm 0.52.6",
|
||||||
"windows_x86_64_msvc",
|
"windows_x86_64_msvc 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_gnullvm"
|
name = "windows_aarch64_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_msvc"
|
name = "windows_aarch64_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnu"
|
name = "windows_i686_gnu"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
|
@ -1693,24 +1967,48 @@ version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_msvc"
|
name = "windows_i686_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnu"
|
name = "windows_x86_64_gnu"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnullvm"
|
name = "windows_x86_64_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_msvc"
|
name = "windows_x86_64_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
|
@ -1749,7 +2047,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.93",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1771,7 +2069,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.93",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1791,7 +2089,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.93",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1814,7 +2112,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.93",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -8,8 +8,12 @@ actix-web = "4.0"
|
||||||
actix-files = "0.6"
|
actix-files = "0.6"
|
||||||
actix-web-actors = "4.0"
|
actix-web-actors = "4.0"
|
||||||
actix = "0.13"
|
actix = "0.13"
|
||||||
|
alacritty_terminal = { git = "https://github.com/alacritty/alacritty", rev = "53395536aa4ebebcbc0431e7336c2a6857efcff5" }
|
||||||
chrono = "0.4" # For timestamp generation
|
chrono = "0.4" # For timestamp generation
|
||||||
env_logger = "0.10" # For logging
|
env_logger = "0.10" # For logging
|
||||||
|
once_cell = "1.20.2"
|
||||||
|
log = "0.4"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
log = "0.4"
|
tokio = "1.43.0"
|
||||||
|
elm_rs = "0.2.2"
|
||||||
|
|
150
backend/EncodeDecodeTop.elm
Normal file
150
backend/EncodeDecodeTop.elm
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
|
||||||
|
-- generated by elm_rs
|
||||||
|
|
||||||
|
|
||||||
|
module EncodeDecodeTop exposing (..)
|
||||||
|
|
||||||
|
import Dict exposing (Dict)
|
||||||
|
import Http
|
||||||
|
import Json.Decode
|
||||||
|
import Json.Encode
|
||||||
|
import Url.Builder
|
||||||
|
|
||||||
|
|
||||||
|
resultEncoder : (e -> Json.Encode.Value) -> (t -> Json.Encode.Value) -> (Result e t -> Json.Encode.Value)
|
||||||
|
resultEncoder errEncoder okEncoder enum =
|
||||||
|
case enum of
|
||||||
|
Ok inner ->
|
||||||
|
Json.Encode.object [ ( "Ok", okEncoder inner ) ]
|
||||||
|
Err inner ->
|
||||||
|
Json.Encode.object [ ( "Err", errEncoder inner ) ]
|
||||||
|
|
||||||
|
|
||||||
|
resultDecoder : Json.Decode.Decoder e -> Json.Decode.Decoder t -> Json.Decode.Decoder (Result e t)
|
||||||
|
resultDecoder errDecoder okDecoder =
|
||||||
|
Json.Decode.oneOf
|
||||||
|
[ Json.Decode.map Ok (Json.Decode.field "Ok" okDecoder)
|
||||||
|
, Json.Decode.map Err (Json.Decode.field "Err" errDecoder)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
type DownMsg
|
||||||
|
= Landing (LandingDownMsg)
|
||||||
|
| Term (TermDownMsg)
|
||||||
|
|
||||||
|
|
||||||
|
downMsgEncoder : DownMsg -> Json.Encode.Value
|
||||||
|
downMsgEncoder enum =
|
||||||
|
case enum of
|
||||||
|
Landing inner ->
|
||||||
|
Json.Encode.object [ ( "Landing", landingDownMsgEncoder inner ) ]
|
||||||
|
Term inner ->
|
||||||
|
Json.Encode.object [ ( "Term", termDownMsgEncoder inner ) ]
|
||||||
|
|
||||||
|
type TermDownMsg
|
||||||
|
= FullTermUpdate (TerminalScreen)
|
||||||
|
| BackendTermStartFailure (String)
|
||||||
|
| TermNotStarted
|
||||||
|
|
||||||
|
|
||||||
|
termDownMsgEncoder : TermDownMsg -> Json.Encode.Value
|
||||||
|
termDownMsgEncoder enum =
|
||||||
|
case enum of
|
||||||
|
FullTermUpdate inner ->
|
||||||
|
Json.Encode.object [ ( "FullTermUpdate", terminalScreenEncoder inner ) ]
|
||||||
|
BackendTermStartFailure inner ->
|
||||||
|
Json.Encode.object [ ( "BackendTermStartFailure", Json.Encode.string inner ) ]
|
||||||
|
TermNotStarted ->
|
||||||
|
Json.Encode.string "TermNotStarted"
|
||||||
|
|
||||||
|
type alias TerminalScreen =
|
||||||
|
{ cols : Int
|
||||||
|
, rows : Int
|
||||||
|
, content : String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
terminalScreenEncoder : TerminalScreen -> Json.Encode.Value
|
||||||
|
terminalScreenEncoder struct =
|
||||||
|
Json.Encode.object
|
||||||
|
[ ( "cols", (Json.Encode.int) struct.cols )
|
||||||
|
, ( "rows", (Json.Encode.int) struct.rows )
|
||||||
|
, ( "content", (Json.Encode.string) struct.content )
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
type LandingDownMsg
|
||||||
|
= Greeting (String)
|
||||||
|
| TimeUpdate (String)
|
||||||
|
|
||||||
|
|
||||||
|
landingDownMsgEncoder : LandingDownMsg -> Json.Encode.Value
|
||||||
|
landingDownMsgEncoder enum =
|
||||||
|
case enum of
|
||||||
|
Greeting inner ->
|
||||||
|
Json.Encode.object [ ( "Greeting", Json.Encode.string inner ) ]
|
||||||
|
TimeUpdate inner ->
|
||||||
|
Json.Encode.object [ ( "TimeUpdate", Json.Encode.string inner ) ]
|
||||||
|
|
||||||
|
type UpMsg
|
||||||
|
= Landing (LandingUpMsg)
|
||||||
|
| Term (TermUpMsg)
|
||||||
|
|
||||||
|
|
||||||
|
upMsgDecoder : Json.Decode.Decoder UpMsg
|
||||||
|
upMsgDecoder =
|
||||||
|
Json.Decode.oneOf
|
||||||
|
[ Json.Decode.map Landing (Json.Decode.field "Landing" (landingUpMsgDecoder))
|
||||||
|
, Json.Decode.map Term (Json.Decode.field "Term" (termUpMsgDecoder))
|
||||||
|
]
|
||||||
|
|
||||||
|
type TermUpMsg
|
||||||
|
= RequestFullTermState
|
||||||
|
| CloseTerm
|
||||||
|
| RequestIncrementalTermStateUpdate
|
||||||
|
| SendCharacter (String)
|
||||||
|
|
||||||
|
|
||||||
|
termUpMsgDecoder : Json.Decode.Decoder TermUpMsg
|
||||||
|
termUpMsgDecoder =
|
||||||
|
Json.Decode.oneOf
|
||||||
|
[ Json.Decode.string
|
||||||
|
|> Json.Decode.andThen
|
||||||
|
(\x ->
|
||||||
|
case x of
|
||||||
|
"RequestFullTermState" ->
|
||||||
|
Json.Decode.succeed RequestFullTermState
|
||||||
|
unexpected ->
|
||||||
|
Json.Decode.fail <| "Unexpected variant " ++ unexpected
|
||||||
|
)
|
||||||
|
, Json.Decode.string
|
||||||
|
|> Json.Decode.andThen
|
||||||
|
(\x ->
|
||||||
|
case x of
|
||||||
|
"CloseTerm" ->
|
||||||
|
Json.Decode.succeed CloseTerm
|
||||||
|
unexpected ->
|
||||||
|
Json.Decode.fail <| "Unexpected variant " ++ unexpected
|
||||||
|
)
|
||||||
|
, Json.Decode.string
|
||||||
|
|> Json.Decode.andThen
|
||||||
|
(\x ->
|
||||||
|
case x of
|
||||||
|
"RequestIncrementalTermStateUpdate" ->
|
||||||
|
Json.Decode.succeed RequestIncrementalTermStateUpdate
|
||||||
|
unexpected ->
|
||||||
|
Json.Decode.fail <| "Unexpected variant " ++ unexpected
|
||||||
|
)
|
||||||
|
, Json.Decode.map SendCharacter (Json.Decode.field "SendCharacter" (Json.Decode.string))
|
||||||
|
]
|
||||||
|
|
||||||
|
type LandingUpMsg
|
||||||
|
= RequestGreet (String)
|
||||||
|
|
||||||
|
|
||||||
|
landingUpMsgDecoder : Json.Decode.Decoder LandingUpMsg
|
||||||
|
landingUpMsgDecoder =
|
||||||
|
Json.Decode.oneOf
|
||||||
|
[ Json.Decode.map RequestGreet (Json.Decode.field "RequestGreet" (Json.Decode.string))
|
||||||
|
]
|
||||||
|
|
|
@ -1,27 +1,36 @@
|
||||||
use actix_web_actors::ws::WebsocketContext;
|
use actix_web_actors::ws::WebsocketContext;
|
||||||
|
use elm_rs::{Elm, ElmEncode, ElmDecode};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
type BackendType = crate::TryItBackend;
|
||||||
pub enum DownMsg {
|
|
||||||
|
#[derive(Serialize, Debug, Elm, ElmDecode)]
|
||||||
|
pub enum LandingDownMsg {
|
||||||
Greeting(String),
|
Greeting(String),
|
||||||
TimeUpdate(String),
|
TimeUpdate(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug, Elm, ElmEncode)]
|
||||||
pub enum UpMsg {
|
pub enum LandingUpMsg {
|
||||||
RequestGreet(String),
|
RequestGreet(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn cleanup(
|
||||||
|
actor_instance: &mut BackendType,
|
||||||
|
ctx: &mut WebsocketContext<BackendType>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
pub fn msg_handler(
|
pub fn msg_handler(
|
||||||
msg: UpMsg,
|
msg: LandingUpMsg,
|
||||||
ws: &mut crate::MyWebSocket,
|
actor_instance: &mut crate::TryItBackend,
|
||||||
ctx: &mut WebsocketContext<crate::MyWebSocket>,
|
ctx: &mut WebsocketContext<BackendType>,
|
||||||
) {
|
) {
|
||||||
match msg {
|
match msg {
|
||||||
UpMsg::RequestGreet(name) => {
|
LandingUpMsg::RequestGreet(name) => {
|
||||||
let greeting = format!("Hello, {}!", name);
|
let greeting = format!("Hello, {}!", name);
|
||||||
let response = crate::DownMsg::Landing(DownMsg::Greeting(greeting));
|
let response = crate::DownMsg::LandingDownMsg(LandingDownMsg::Greeting(greeting));
|
||||||
ws.send_down_msg(response, ctx);
|
actor_instance.send_down_msg(response, ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,43 +1,89 @@
|
||||||
|
use actix_web::middleware::Compress;
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
use actix_files::Files;
|
use actix_files::Files;
|
||||||
use actix_web::{web, App, Error, HttpRequest, HttpResponse, HttpServer, Responder};
|
use actix_web::{web, App, Error, HttpRequest, HttpResponse, HttpServer, middleware};
|
||||||
use actix_web_actors::ws;
|
use actix_web_actors::ws;
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use log::info;
|
use log::info;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{fs, time::Duration};
|
use std::{fs, os::unix::fs::PermissionsExt, time::Duration};
|
||||||
|
|
||||||
|
|
||||||
|
use elm_rs::{Elm, ElmEncode, ElmDecode};
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
mod landing;
|
mod landing;
|
||||||
|
mod term;
|
||||||
|
mod terminal_size;
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug, Message, Elm, ElmDecode)]
|
||||||
|
#[rtype(result = "()")]
|
||||||
pub enum DownMsg {
|
pub enum DownMsg {
|
||||||
Landing(landing::DownMsg),
|
LandingDownMsg(landing::LandingDownMsg),
|
||||||
|
TermDownMsg(term::TermDownMsg),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug, Elm, ElmEncode)]
|
||||||
enum UpMsg {
|
enum UpMsg {
|
||||||
Landing(landing::UpMsg),
|
LandingUpMsg(landing::LandingUpMsg),
|
||||||
|
TermUpMsg(term::TermUpMsg),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MyWebSocket;
|
pub struct TryItBackend {
|
||||||
|
addr: Option<Addr<TryItBackend>>,
|
||||||
|
graceful_stop: bool
|
||||||
|
}
|
||||||
|
|
||||||
impl Actor for MyWebSocket {
|
fn do_elm_gen() {
|
||||||
|
let mut encode_decode_top = vec![];
|
||||||
|
elm_rs::export!("EncodeDecode", &mut encode_decode_top, {
|
||||||
|
encoders: [UpMsg,
|
||||||
|
term::TermUpMsg,
|
||||||
|
landing::LandingUpMsg],
|
||||||
|
decoders: [DownMsg,
|
||||||
|
term::TermDownMsg, term::TerminalScreen, term::Cursor, term::Line,
|
||||||
|
landing::LandingDownMsg]
|
||||||
|
}).unwrap();
|
||||||
|
let output = String::from_utf8(encode_decode_top).unwrap();
|
||||||
|
fs::write("../frontend/src/EncodeDecode.elm", output).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Actor for TryItBackend {
|
||||||
type Context = ws::WebsocketContext<Self>;
|
type Context = ws::WebsocketContext<Self>;
|
||||||
|
|
||||||
fn started(&mut self, ctx: &mut Self::Context) {
|
fn started(&mut self, ctx: &mut Self::Context) {
|
||||||
info!("WebSocket actor started");
|
info!("WebSocket actor started");
|
||||||
|
let addr = ctx.address();
|
||||||
|
self.addr = Some(addr);
|
||||||
ctx.run_interval(Duration::from_secs(1), |_, ctx| {
|
ctx.run_interval(Duration::from_secs(1), |_, ctx| {
|
||||||
let current_time = Local::now().format("%H:%M:%S").to_string();
|
let current_time = Local::now().format("%H:%M:%S").to_string();
|
||||||
let message = DownMsg::Landing(landing::DownMsg::TimeUpdate(current_time));
|
let message = DownMsg::LandingDownMsg(landing::LandingDownMsg::TimeUpdate(current_time));
|
||||||
|
|
||||||
if let Ok(json_message) = serde_json::to_string(&message) {
|
if let Ok(json_message) = serde_json::to_string(&message) {
|
||||||
ctx.text(json_message);
|
ctx.text(json_message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stopped(&mut self, ctx: &mut Self::Context) {
|
||||||
|
if !self.graceful_stop {
|
||||||
|
landing::cleanup(self, ctx);
|
||||||
|
term::cleanup(self, ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWebSocket {
|
}
|
||||||
|
|
||||||
|
impl actix::Handler<DownMsg> for TryItBackend {
|
||||||
|
type Result = ();
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: DownMsg, ctx: &mut ws::WebsocketContext<Self>) {
|
||||||
|
self.send_down_msg(msg, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for TryItBackend {
|
||||||
fn handle(
|
fn handle(
|
||||||
&mut self,
|
&mut self,
|
||||||
msg: Result<ws::Message, ws::ProtocolError>,
|
msg: Result<ws::Message, ws::ProtocolError>,
|
||||||
|
@ -47,20 +93,28 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWebSocket {
|
||||||
Ok(ws::Message::Text(text)) => {
|
Ok(ws::Message::Text(text)) => {
|
||||||
if let Ok(up_msg) = serde_json::from_str::<UpMsg>(&text) {
|
if let Ok(up_msg) = serde_json::from_str::<UpMsg>(&text) {
|
||||||
match up_msg {
|
match up_msg {
|
||||||
UpMsg::Landing(msg) =>
|
UpMsg::LandingUpMsg(msg) => landing::msg_handler(msg, self, ctx),
|
||||||
landing::msg_handler(msg, self, ctx),
|
UpMsg::TermUpMsg(msg) => term::msg_handler(msg, self, ctx),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(ws::Message::Ping(msg)) => {
|
Ok(ws::Message::Ping(msg)) => {
|
||||||
ctx.pong(&msg);
|
ctx.pong(&msg);
|
||||||
}
|
}
|
||||||
|
Ok(ws::Message::Close(reason)) => {
|
||||||
|
log::info!("WebSocket connection closed: {:?}", reason);
|
||||||
|
|
||||||
|
// Call `term::cleanup` to remove the associated terminal instance
|
||||||
|
term::cleanup(self, ctx);
|
||||||
|
self.graceful_stop = true;
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MyWebSocket {
|
impl TryItBackend {
|
||||||
|
|
||||||
fn send_down_msg(&self, down_msg: DownMsg, ctx: &mut ws::WebsocketContext<Self>) {
|
fn send_down_msg(&self, down_msg: DownMsg, ctx: &mut ws::WebsocketContext<Self>) {
|
||||||
if let Ok(serialized_msg) = serde_json::to_string(&down_msg) {
|
if let Ok(serialized_msg) = serde_json::to_string(&down_msg) {
|
||||||
ctx.text(serialized_msg);
|
ctx.text(serialized_msg);
|
||||||
|
@ -71,14 +125,26 @@ impl MyWebSocket {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn websocket_handler(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
|
async fn websocket_handler(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
|
||||||
ws::start(MyWebSocket {}, &req, stream)
|
let try_it_backend = TryItBackend { addr: None, graceful_stop: false };
|
||||||
|
ws::start(try_it_backend, &req, stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
let address = "127.0.0.1";
|
// Check if an environment variable `GENERATE_ELM` is set
|
||||||
|
if std::env::var("GENERATE_ELM").is_ok() {
|
||||||
|
do_elm_gen();
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let address = if std::env::var("DEBUG").is_ok() {
|
||||||
|
"0.0.0.0"
|
||||||
|
} else {
|
||||||
|
"127.0.0.1"
|
||||||
|
};
|
||||||
|
|
||||||
let port = std::env::var("EXAMPLE_ELM_APP_PORT")
|
let port = std::env::var("EXAMPLE_ELM_APP_PORT")
|
||||||
.unwrap_or("8080".to_string())
|
.unwrap_or("8080".to_string())
|
||||||
.parse()
|
.parse()
|
||||||
|
@ -88,9 +154,15 @@ async fn main() -> std::io::Result<()> {
|
||||||
|
|
||||||
HttpServer::new(|| {
|
HttpServer::new(|| {
|
||||||
App::new()
|
App::new()
|
||||||
|
.wrap(Compress::default())
|
||||||
|
.wrap(middleware::DefaultHeaders::new()
|
||||||
|
.add(("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0"))
|
||||||
|
.add(("Pragma", "no-cache"))
|
||||||
|
.add(("Expires", "0"))
|
||||||
|
)
|
||||||
.route("/ws/", web::get().to(websocket_handler)) // WebSocket endpoint
|
.route("/ws/", web::get().to(websocket_handler)) // WebSocket endpoint
|
||||||
.service(Files::new("/assets", "./public/assets"))
|
.service(Files::new("/assets", "./public/assets"))
|
||||||
.service(Files::new("/", "./public").index_file("index.html")) // Serve frontend
|
.service(Files::new("/", "./public").index_file("index.html"))
|
||||||
.default_service(web::route().to(|| async {
|
.default_service(web::route().to(|| async {
|
||||||
let index_html = fs::read_to_string("./public/index.html")
|
let index_html = fs::read_to_string("./public/index.html")
|
||||||
.unwrap_or_else(|_| "404 Not Found".to_string());
|
.unwrap_or_else(|_| "404 Not Found".to_string());
|
||||||
|
|
341
backend/src/term.rs
Normal file
341
backend/src/term.rs
Normal file
|
@ -0,0 +1,341 @@
|
||||||
|
use actix::Addr;
|
||||||
|
use actix_web_actors::ws::WebsocketContext;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::terminal_size;
|
||||||
|
use alacritty_terminal::event::Notify;
|
||||||
|
use alacritty_terminal::event::{Event, EventListener};
|
||||||
|
use alacritty_terminal::event_loop::{EventLoop, EventLoopSender, Msg, Notifier};
|
||||||
|
use alacritty_terminal::sync::FairMutex;
|
||||||
|
use alacritty_terminal::term::cell::Cell;
|
||||||
|
use alacritty_terminal::term::{self, Term as Terminal};
|
||||||
|
use alacritty_terminal::{tty, Grid};
|
||||||
|
use actix::AsyncContext;
|
||||||
|
|
||||||
|
use elm_rs::{Elm, ElmEncode, ElmDecode};
|
||||||
|
|
||||||
|
// use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::format;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
type BackendType = crate::TryItBackend;
|
||||||
|
const max_terminals: usize = 9;
|
||||||
|
|
||||||
|
static TERM_SESSION_BY_ACTOR: LazyLock<
|
||||||
|
RwLock<HashMap<Addr<BackendType>, Arc<RwLock<TerminalSession>>>>,
|
||||||
|
> = LazyLock::new(|| RwLock::new(HashMap::new()));
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, PartialEq, Elm, ElmEncode)]
|
||||||
|
#[serde(crate = "serde")]
|
||||||
|
pub enum TermUpMsg {
|
||||||
|
RequestFullTermState,
|
||||||
|
CloseTerm,
|
||||||
|
RequestIncrementalTermStateUpdate,
|
||||||
|
SendCharacter(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug, Elm, ElmDecode)]
|
||||||
|
pub enum TermDownMsg {
|
||||||
|
TermUpdate(TerminalScreen),
|
||||||
|
BackendTermStartFailure(String),
|
||||||
|
TermNotStarted,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug, Elm, ElmDecode, PartialEq, Clone)]
|
||||||
|
pub struct Line {
|
||||||
|
pub line: i32,
|
||||||
|
pub content: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug, Elm, ElmDecode)]
|
||||||
|
pub struct Cursor {
|
||||||
|
pub row: i32,
|
||||||
|
pub col: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug, Elm, ElmDecode)]
|
||||||
|
pub struct TerminalScreen {
|
||||||
|
pub cols: usize,
|
||||||
|
pub rows: usize,
|
||||||
|
pub damaged_lines: Vec<Line>,
|
||||||
|
// (line, col)
|
||||||
|
pub cursor_loc: Cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TerminalSession {
|
||||||
|
term: Arc<FairMutex<Terminal<EventProxy>>>,
|
||||||
|
old_content: String,
|
||||||
|
// (line, col)
|
||||||
|
old_cursor_pos: (i32, usize),
|
||||||
|
/// Use to write to the terminal from the outside world.
|
||||||
|
tx: Notifier,
|
||||||
|
cols: u16,
|
||||||
|
rows: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct EventProxy{
|
||||||
|
channel: std::sync::mpsc::Sender<Event>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventListener for EventProxy {
|
||||||
|
fn send_event(&self, event: Event) {
|
||||||
|
let _ = self.channel.send(event.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cleanup(
|
||||||
|
actor_instance: &mut BackendType,
|
||||||
|
ctx: &mut WebsocketContext<BackendType>,
|
||||||
|
) {
|
||||||
|
if let Some(actor_id) = actor_instance.addr.clone() {
|
||||||
|
if term_session_already_registered_in_hashmap(&actor_id) {
|
||||||
|
let mut sessions = TERM_SESSION_BY_ACTOR.write().unwrap();
|
||||||
|
if let Some(some_session) = sessions.get(&actor_id) {
|
||||||
|
some_session.read().unwrap().tx.0.send(Msg::Shutdown).unwrap();
|
||||||
|
log::info!("Closed pty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
sessions.remove(&actor_id);
|
||||||
|
log::info!("Cleaned up terminal session for actor {:?}", actor_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn msg_handler(
|
||||||
|
msg: TermUpMsg,
|
||||||
|
actor_instance: &mut BackendType,
|
||||||
|
ctx: &mut WebsocketContext<BackendType>,
|
||||||
|
) {
|
||||||
|
// only proceed if msg not CloseTerm
|
||||||
|
if !(msg == TermUpMsg::CloseTerm) {
|
||||||
|
if let Some(actor_id) = actor_instance.addr.clone() {
|
||||||
|
if term_session_already_registered_in_hashmap(&actor_id) {
|
||||||
|
let mut term_session = TERM_SESSION_BY_ACTOR.write().unwrap();
|
||||||
|
if let Some(term_session) = term_session.get_mut(&actor_id) {
|
||||||
|
|
||||||
|
match msg {
|
||||||
|
TermUpMsg::RequestFullTermState => {
|
||||||
|
send_terminal_update_if_changed(
|
||||||
|
&actor_id,
|
||||||
|
term_session
|
||||||
|
);
|
||||||
|
}
|
||||||
|
TermUpMsg::SendCharacter(c) => {
|
||||||
|
{
|
||||||
|
let term_session = term_session.write().unwrap();
|
||||||
|
term_session.tx.notify(c.to_string().into_bytes());
|
||||||
|
let mut term = term_session.term.lock();
|
||||||
|
term.scroll_display(alacritty_terminal::grid::Scroll::Bottom);
|
||||||
|
}
|
||||||
|
send_terminal_update_if_changed(
|
||||||
|
&actor_id,
|
||||||
|
term_session
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if get_session_count() >= max_terminals {
|
||||||
|
backend_term_start_error(
|
||||||
|
actor_instance,
|
||||||
|
ctx,
|
||||||
|
format!("Maximum number of {} open terminal sessions reached. Somebody must close at least one!", max_terminals).to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let (rows, cols) = (24, 80);
|
||||||
|
|
||||||
|
let id = get_session_count() as u64;
|
||||||
|
|
||||||
|
let pty_config = tty::Options {
|
||||||
|
shell: Some(tty::Shell::new(
|
||||||
|
"/usr/bin/env".to_string(),
|
||||||
|
vec!["bash".to_string()],
|
||||||
|
)),
|
||||||
|
..tty::Options::default()
|
||||||
|
};
|
||||||
|
let config = term::Config::default();
|
||||||
|
let terminal_size = terminal_size::TerminalSize::new(rows, cols);
|
||||||
|
let pty = match tty::new(&pty_config, terminal_size.into(), id) {
|
||||||
|
Ok(pty) => pty,
|
||||||
|
Err(msg) => {
|
||||||
|
backend_term_start_error(
|
||||||
|
actor_instance,
|
||||||
|
ctx,
|
||||||
|
format!("Failed to create pty: {}", msg),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let (event_sender, event_receiver) = std::sync::mpsc::channel();
|
||||||
|
let event_proxy = EventProxy {
|
||||||
|
channel: event_sender,
|
||||||
|
};
|
||||||
|
let term = Terminal::new::<terminal_size::TerminalSize>(
|
||||||
|
config,
|
||||||
|
&terminal_size.into(),
|
||||||
|
event_proxy.clone(),
|
||||||
|
);
|
||||||
|
let term = Arc::new(FairMutex::new(term));
|
||||||
|
let pty_event_loop =
|
||||||
|
match EventLoop::new(term.clone(), event_proxy, pty, false, false) {
|
||||||
|
Ok(loop_instance) => loop_instance,
|
||||||
|
Err(err) => {
|
||||||
|
backend_term_start_error(
|
||||||
|
actor_instance,
|
||||||
|
ctx,
|
||||||
|
format!("EventLoop::new failed: {:?}", err),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let notifier = Notifier(pty_event_loop.channel());
|
||||||
|
let term_clone = term.clone();
|
||||||
|
pty_event_loop.spawn();
|
||||||
|
|
||||||
|
let actor_id_thread_clone = actor_id.clone();
|
||||||
|
let term_thread_clone = term.clone();
|
||||||
|
let addr_thread_clone = ctx.address().clone();
|
||||||
|
ctx.spawn(actix::fut::wrap_future(async move {
|
||||||
|
// Start a new thread
|
||||||
|
thread::spawn(move || {
|
||||||
|
loop {
|
||||||
|
if let Ok(event) = event_receiver.recv() {
|
||||||
|
let mut sessions = TERM_SESSION_BY_ACTOR.write().unwrap();
|
||||||
|
match event {
|
||||||
|
Event::Exit => {
|
||||||
|
sessions.remove(&actor_id_thread_clone);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if let Some(term_session) = sessions.get_mut(&actor_id_thread_clone) {
|
||||||
|
send_terminal_update_if_changed(
|
||||||
|
&addr_thread_clone,
|
||||||
|
term_session
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
let terminal_session = TerminalSession {
|
||||||
|
term: term.clone(),
|
||||||
|
old_content: String::new(),
|
||||||
|
old_cursor_pos: (0, 0),
|
||||||
|
tx: notifier,
|
||||||
|
cols: cols,
|
||||||
|
rows: rows,
|
||||||
|
};
|
||||||
|
TERM_SESSION_BY_ACTOR
|
||||||
|
.write()
|
||||||
|
.unwrap()
|
||||||
|
.insert(actor_id, Arc::new(RwLock::new(terminal_session)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn backend_term_start_error(
|
||||||
|
actor_instance: &mut BackendType,
|
||||||
|
ctx: &mut WebsocketContext<BackendType>,
|
||||||
|
msg: String,
|
||||||
|
) {
|
||||||
|
log::error!("{}", msg);
|
||||||
|
let down_msg = crate::DownMsg::TermDownMsg(TermDownMsg::BackendTermStartFailure(msg));
|
||||||
|
actor_instance.send_down_msg(down_msg, ctx);
|
||||||
|
}
|
||||||
|
fn get_session_count() -> usize {
|
||||||
|
let sessions_map = TERM_SESSION_BY_ACTOR.read().unwrap();
|
||||||
|
sessions_map.len()
|
||||||
|
}
|
||||||
|
fn term_session_already_registered_in_hashmap(actor_id: &Addr<BackendType>) -> bool {
|
||||||
|
let sessions_map = TERM_SESSION_BY_ACTOR.read().unwrap();
|
||||||
|
sessions_map.contains_key(&actor_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn full_term_state(terminal_session: &TerminalSession) -> (String, alacritty_terminal::index::Point) {
|
||||||
|
let (rows, cols) = (terminal_session.rows, terminal_session.cols);
|
||||||
|
let term = terminal_session.term.lock();
|
||||||
|
let grid = term.grid().clone();
|
||||||
|
let cursor = grid.cursor.point.clone();
|
||||||
|
|
||||||
|
return (term_grid_to_string(&grid, rows, cols), cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn term_grid_to_string(grid: &Grid<Cell>, rows: u16, cols: u16) -> String {
|
||||||
|
let mut term_content = String::with_capacity((rows * cols) as usize);
|
||||||
|
|
||||||
|
// Populate string from grid
|
||||||
|
for indexed in grid.display_iter() {
|
||||||
|
let x = indexed.point.column.0 as usize;
|
||||||
|
let y = indexed.point.line.0 as usize;
|
||||||
|
if y < rows as usize && x < cols as usize {
|
||||||
|
term_content.push(indexed.c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return term_content;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_term_content_by_line(term_content: &str, cols: u16) -> Vec<Line> {
|
||||||
|
let mut term_content_by_line: Vec<Line> = vec![];
|
||||||
|
let term_content_chars: Vec<char> = term_content.chars().collect();
|
||||||
|
for (index, chunk) in term_content_chars.chunks(cols as usize).enumerate() {
|
||||||
|
let content = chunk.iter().collect::<String>();
|
||||||
|
let line = Line { line: index as i32, content: content };
|
||||||
|
term_content_by_line.push(line);
|
||||||
|
|
||||||
|
}
|
||||||
|
return term_content_by_line;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn send_terminal_update_if_changed(
|
||||||
|
actor_instance: &Addr<BackendType>,
|
||||||
|
term_session: &mut Arc<RwLock<TerminalSession>>
|
||||||
|
) {
|
||||||
|
let mut term_session = term_session.write().unwrap();
|
||||||
|
|
||||||
|
let (new_content, new_cursor) = full_term_state(&*term_session);
|
||||||
|
let new_term_content_by_line =
|
||||||
|
split_term_content_by_line(&new_content, term_session.cols);
|
||||||
|
|
||||||
|
let term_content_len_has_changed = term_session.old_content.len() != new_content.len();
|
||||||
|
let damaged_lines =
|
||||||
|
if term_content_len_has_changed {
|
||||||
|
new_term_content_by_line
|
||||||
|
} else {
|
||||||
|
let old_term_content_by_line =
|
||||||
|
split_term_content_by_line(
|
||||||
|
&term_session.old_content,
|
||||||
|
term_session.cols);
|
||||||
|
new_term_content_by_line
|
||||||
|
.iter()
|
||||||
|
.zip(old_term_content_by_line.iter())
|
||||||
|
.filter(|(a, b)| a != b)
|
||||||
|
.map(|(a, b)| (a.clone())) // Extract values from references
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
let down_msg = crate::DownMsg::TermDownMsg(TermDownMsg::TermUpdate(TerminalScreen {
|
||||||
|
rows: term_session.rows as usize,
|
||||||
|
cols: term_session.cols as usize,
|
||||||
|
damaged_lines: damaged_lines,
|
||||||
|
cursor_loc: Cursor {
|
||||||
|
row: new_cursor.line.0,
|
||||||
|
col: new_cursor.column.0,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
actor_instance.do_send(down_msg);
|
||||||
|
term_session.old_content = new_content;
|
||||||
|
}
|
55
backend/src/terminal_size.rs
Normal file
55
backend/src/terminal_size.rs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
use alacritty_terminal::event::WindowSize;
|
||||||
|
use alacritty_terminal::grid::Dimensions;
|
||||||
|
use alacritty_terminal::index::{Column, Line};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct TerminalSize {
|
||||||
|
pub cell_width: u16,
|
||||||
|
pub cell_height: u16,
|
||||||
|
pub num_cols: u16,
|
||||||
|
pub num_lines: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TerminalSize {
|
||||||
|
pub fn new(rows: u16, cols: u16) -> Self {
|
||||||
|
Self {
|
||||||
|
cell_width: 1,
|
||||||
|
cell_height: 1,
|
||||||
|
num_cols: cols,
|
||||||
|
num_lines: rows,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dimensions for TerminalSize {
|
||||||
|
fn total_lines(&self) -> usize {
|
||||||
|
self.screen_lines()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn screen_lines(&self) -> usize {
|
||||||
|
self.num_lines as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
fn columns(&self) -> usize {
|
||||||
|
self.num_cols as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
fn last_column(&self) -> Column {
|
||||||
|
Column(self.num_cols as usize - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bottommost_line(&self) -> Line {
|
||||||
|
Line(self.num_lines as i32 - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TerminalSize> for WindowSize {
|
||||||
|
fn from(size: TerminalSize) -> Self {
|
||||||
|
Self {
|
||||||
|
num_lines: size.num_lines,
|
||||||
|
num_cols: size.num_cols,
|
||||||
|
cell_width: size.cell_width,
|
||||||
|
cell_height: size.cell_height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
0
backend/types.elm
Normal file
0
backend/types.elm
Normal file
BIN
frontend/assets/CourierPrime-Regular.ttf
Normal file
BIN
frontend/assets/CourierPrime-Regular.ttf
Normal file
Binary file not shown.
|
@ -11,6 +11,7 @@
|
||||||
"elm/html": "1.0.0",
|
"elm/html": "1.0.0",
|
||||||
"elm/json": "1.1.3",
|
"elm/json": "1.1.3",
|
||||||
"elm/url": "1.0.0",
|
"elm/url": "1.0.0",
|
||||||
|
"elm-community/list-extra": "8.7.0",
|
||||||
"kageurufu/elm-websockets": "1.0.1",
|
"kageurufu/elm-websockets": "1.0.1",
|
||||||
"mdgriffith/elm-ui": "1.1.8"
|
"mdgriffith/elm-ui": "1.1.8"
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,6 +3,14 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Elm UI Website</title>
|
<title>Elm UI Website</title>
|
||||||
|
<style>
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Courier Prime';
|
||||||
|
src: url('assets/CourierPrime-Regular.ttf') format('truetype');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="elm"></div>
|
<div id="elm"></div>
|
||||||
|
|
|
@ -2,11 +2,12 @@ module Body exposing (Msg(..), Model(..), init, update, view, handleRoute, subsc
|
||||||
|
|
||||||
import Element
|
import Element
|
||||||
|
|
||||||
import Page.About
|
|
||||||
import Page.Contact
|
|
||||||
import Page.Landing
|
import Page.Landing
|
||||||
import Page.Products
|
import Page.Products
|
||||||
import Page.Resources
|
import Page.Resources
|
||||||
|
import Page.About
|
||||||
|
import Page.Contact
|
||||||
|
import Page.Term
|
||||||
import Page.NotFound
|
import Page.NotFound
|
||||||
|
|
||||||
type Msg
|
type Msg
|
||||||
|
@ -15,6 +16,7 @@ type Msg
|
||||||
| MsgResources Page.Resources.Msg
|
| MsgResources Page.Resources.Msg
|
||||||
| MsgAbout Page.About.Msg
|
| MsgAbout Page.About.Msg
|
||||||
| MsgContact Page.Contact.Msg
|
| MsgContact Page.Contact.Msg
|
||||||
|
| MsgTerm Page.Term.Msg
|
||||||
| MsgNotFound Page.NotFound.Msg
|
| MsgNotFound Page.NotFound.Msg
|
||||||
|
|
||||||
type Model
|
type Model
|
||||||
|
@ -23,10 +25,15 @@ type Model
|
||||||
| ModelResources Page.Resources.Model
|
| ModelResources Page.Resources.Model
|
||||||
| ModelAbout Page.About.Model
|
| ModelAbout Page.About.Model
|
||||||
| ModelContact Page.Contact.Model
|
| ModelContact Page.Contact.Model
|
||||||
|
| ModelTerm Page.Term.Model
|
||||||
| ModelNotFound Page.NotFound.Model
|
| ModelNotFound Page.NotFound.Model
|
||||||
|
|
||||||
init : () -> Model
|
init : () -> (Model, Cmd Msg)
|
||||||
init flags = ModelLanding (Page.Landing.init flags)
|
init flags =
|
||||||
|
let
|
||||||
|
(landingModel, landingCmd) = Page.Landing.init ()
|
||||||
|
in
|
||||||
|
(ModelLanding landingModel, Cmd.map MsgLanding landingCmd)
|
||||||
|
|
||||||
subscriptions : Model -> Sub Msg
|
subscriptions : Model -> Sub Msg
|
||||||
subscriptions model =
|
subscriptions model =
|
||||||
|
@ -36,22 +43,53 @@ subscriptions model =
|
||||||
ModelResources m -> Page.Resources.subscriptions m |> Sub.map MsgResources
|
ModelResources m -> Page.Resources.subscriptions m |> Sub.map MsgResources
|
||||||
ModelAbout m -> Page.About.subscriptions m |> Sub.map MsgAbout
|
ModelAbout m -> Page.About.subscriptions m |> Sub.map MsgAbout
|
||||||
ModelContact m -> Page.Contact.subscriptions m |> Sub.map MsgContact
|
ModelContact m -> Page.Contact.subscriptions m |> Sub.map MsgContact
|
||||||
|
ModelTerm m -> Page.Term.subscriptions m |> Sub.map MsgTerm
|
||||||
ModelNotFound m -> Page.NotFound.subscriptions m |> Sub.map MsgNotFound
|
ModelNotFound m -> Page.NotFound.subscriptions m |> Sub.map MsgNotFound
|
||||||
|
|
||||||
handleRoute : String -> Model
|
handleRoute : String -> (Model, Cmd Msg)
|
||||||
handleRoute path =
|
handleRoute path =
|
||||||
let
|
|
||||||
page =
|
|
||||||
case path of
|
case path of
|
||||||
"/" -> ModelLanding <| Page.Landing.init ()
|
"/" ->
|
||||||
"/Products" -> ModelProducts <| Page.Products.init ()
|
let
|
||||||
"/Resources" -> ModelResources <| Page.Resources.init ()
|
(landingModel, landingCmd) = Page.Landing.init ()
|
||||||
"/About" -> ModelAbout <| Page.About.init ()
|
|
||||||
"/Contact" -> ModelContact <| Page.Contact.init ()
|
|
||||||
_ -> ModelNotFound <| Page.NotFound.init ()
|
|
||||||
in
|
in
|
||||||
page
|
(ModelLanding landingModel, Cmd.map MsgLanding landingCmd)
|
||||||
|
|
||||||
|
"/Products" ->
|
||||||
|
let
|
||||||
|
(productsModel, productsCmd) = Page.Products.init ()
|
||||||
|
in
|
||||||
|
(ModelProducts productsModel, Cmd.map MsgProducts productsCmd)
|
||||||
|
|
||||||
|
"/Resources" ->
|
||||||
|
let
|
||||||
|
(resourcesModel, resourcesCmd) = Page.Resources.init ()
|
||||||
|
in
|
||||||
|
(ModelResources resourcesModel, Cmd.map MsgResources resourcesCmd)
|
||||||
|
|
||||||
|
"/About" ->
|
||||||
|
let
|
||||||
|
(aboutModel, aboutCmd) = Page.About.init ()
|
||||||
|
in
|
||||||
|
(ModelAbout aboutModel, Cmd.map MsgAbout aboutCmd)
|
||||||
|
|
||||||
|
"/Contact" ->
|
||||||
|
let
|
||||||
|
(contactModel, contactCmd) = Page.Contact.init ()
|
||||||
|
in
|
||||||
|
(ModelContact contactModel, Cmd.map MsgContact contactCmd)
|
||||||
|
|
||||||
|
"/Term" ->
|
||||||
|
let
|
||||||
|
(termModel, termCmd) = Page.Term.init ()
|
||||||
|
in
|
||||||
|
(ModelTerm termModel, Cmd.map MsgTerm termCmd)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
let
|
||||||
|
(notFoundModel, notFoundCmd) = Page.NotFound.init ()
|
||||||
|
in
|
||||||
|
(ModelNotFound notFoundModel, Cmd.map MsgNotFound notFoundCmd)
|
||||||
update : Msg -> Model -> (Model, Cmd Msg)
|
update : Msg -> Model -> (Model, Cmd Msg)
|
||||||
update bodyMsg bodyModel =
|
update bodyMsg bodyModel =
|
||||||
let
|
let
|
||||||
|
@ -77,6 +115,9 @@ update bodyMsg bodyModel =
|
||||||
(MsgContact msg, ModelContact m) ->
|
(MsgContact msg, ModelContact m) ->
|
||||||
updatePage msg m Page.Contact.update MsgContact ModelContact
|
updatePage msg m Page.Contact.update MsgContact ModelContact
|
||||||
|
|
||||||
|
(MsgTerm msg, ModelTerm m) ->
|
||||||
|
updatePage msg m Page.Term.update MsgTerm ModelTerm
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
(bodyModel, Cmd.none)
|
(bodyModel, Cmd.none)
|
||||||
|
|
||||||
|
@ -89,6 +130,7 @@ view model =
|
||||||
ModelResources m -> Page.Resources.view m |> Element.map MsgResources
|
ModelResources m -> Page.Resources.view m |> Element.map MsgResources
|
||||||
ModelAbout m -> Page.About.view m |> Element.map MsgAbout
|
ModelAbout m -> Page.About.view m |> Element.map MsgAbout
|
||||||
ModelContact m -> Page.Contact.view m |> Element.map MsgContact
|
ModelContact m -> Page.Contact.view m |> Element.map MsgContact
|
||||||
|
ModelTerm m -> Page.Term.view m |> Element.map MsgTerm
|
||||||
ModelNotFound m -> Page.NotFound.view m |> Element.map MsgNotFound
|
ModelNotFound m -> Page.NotFound.view m |> Element.map MsgNotFound
|
||||||
in
|
in
|
||||||
Element.el [Element.centerY ,Element.centerX] content
|
Element.el [Element.centerY ,Element.centerX] content
|
||||||
|
|
161
frontend/src/EncodeDecode.elm
Normal file
161
frontend/src/EncodeDecode.elm
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
|
||||||
|
-- generated by elm_rs
|
||||||
|
|
||||||
|
|
||||||
|
module EncodeDecode exposing (..)
|
||||||
|
|
||||||
|
import Dict exposing (Dict)
|
||||||
|
-- import Http
|
||||||
|
import Json.Decode
|
||||||
|
import Json.Encode
|
||||||
|
import Url.Builder
|
||||||
|
|
||||||
|
|
||||||
|
resultEncoder : (e -> Json.Encode.Value) -> (t -> Json.Encode.Value) -> (Result e t -> Json.Encode.Value)
|
||||||
|
resultEncoder errEncoder okEncoder enum =
|
||||||
|
case enum of
|
||||||
|
Ok inner ->
|
||||||
|
Json.Encode.object [ ( "Ok", okEncoder inner ) ]
|
||||||
|
Err inner ->
|
||||||
|
Json.Encode.object [ ( "Err", errEncoder inner ) ]
|
||||||
|
|
||||||
|
|
||||||
|
resultDecoder : Json.Decode.Decoder e -> Json.Decode.Decoder t -> Json.Decode.Decoder (Result e t)
|
||||||
|
resultDecoder errDecoder okDecoder =
|
||||||
|
Json.Decode.oneOf
|
||||||
|
[ Json.Decode.map Ok (Json.Decode.field "Ok" okDecoder)
|
||||||
|
, Json.Decode.map Err (Json.Decode.field "Err" errDecoder)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
type UpMsg
|
||||||
|
= LandingUpMsg (LandingUpMsg)
|
||||||
|
| TermUpMsg (TermUpMsg)
|
||||||
|
|
||||||
|
|
||||||
|
upMsgEncoder : UpMsg -> Json.Encode.Value
|
||||||
|
upMsgEncoder enum =
|
||||||
|
case enum of
|
||||||
|
LandingUpMsg inner ->
|
||||||
|
Json.Encode.object [ ( "LandingUpMsg", landingUpMsgEncoder inner ) ]
|
||||||
|
TermUpMsg inner ->
|
||||||
|
Json.Encode.object [ ( "TermUpMsg", termUpMsgEncoder inner ) ]
|
||||||
|
|
||||||
|
type TermUpMsg
|
||||||
|
= RequestFullTermState
|
||||||
|
| CloseTerm
|
||||||
|
| RequestIncrementalTermStateUpdate
|
||||||
|
| SendCharacter (String)
|
||||||
|
|
||||||
|
|
||||||
|
termUpMsgEncoder : TermUpMsg -> Json.Encode.Value
|
||||||
|
termUpMsgEncoder enum =
|
||||||
|
case enum of
|
||||||
|
RequestFullTermState ->
|
||||||
|
Json.Encode.string "RequestFullTermState"
|
||||||
|
CloseTerm ->
|
||||||
|
Json.Encode.string "CloseTerm"
|
||||||
|
RequestIncrementalTermStateUpdate ->
|
||||||
|
Json.Encode.string "RequestIncrementalTermStateUpdate"
|
||||||
|
SendCharacter inner ->
|
||||||
|
Json.Encode.object [ ( "SendCharacter", Json.Encode.string inner ) ]
|
||||||
|
|
||||||
|
type LandingUpMsg
|
||||||
|
= RequestGreet (String)
|
||||||
|
|
||||||
|
|
||||||
|
landingUpMsgEncoder : LandingUpMsg -> Json.Encode.Value
|
||||||
|
landingUpMsgEncoder enum =
|
||||||
|
case enum of
|
||||||
|
RequestGreet inner ->
|
||||||
|
Json.Encode.object [ ( "RequestGreet", Json.Encode.string inner ) ]
|
||||||
|
|
||||||
|
type DownMsg
|
||||||
|
= LandingDownMsg (LandingDownMsg)
|
||||||
|
| TermDownMsg (TermDownMsg)
|
||||||
|
|
||||||
|
|
||||||
|
downMsgDecoder : Json.Decode.Decoder DownMsg
|
||||||
|
downMsgDecoder =
|
||||||
|
Json.Decode.oneOf
|
||||||
|
[ Json.Decode.map LandingDownMsg (Json.Decode.field "LandingDownMsg" (landingDownMsgDecoder))
|
||||||
|
, Json.Decode.map TermDownMsg (Json.Decode.field "TermDownMsg" (termDownMsgDecoder))
|
||||||
|
]
|
||||||
|
|
||||||
|
type TermDownMsg
|
||||||
|
= TermUpdate (TerminalScreen)
|
||||||
|
| BackendTermStartFailure (String)
|
||||||
|
| TermNotStarted
|
||||||
|
|
||||||
|
|
||||||
|
termDownMsgDecoder : Json.Decode.Decoder TermDownMsg
|
||||||
|
termDownMsgDecoder =
|
||||||
|
Json.Decode.oneOf
|
||||||
|
[ Json.Decode.map TermUpdate (Json.Decode.field "TermUpdate" (terminalScreenDecoder))
|
||||||
|
, Json.Decode.map BackendTermStartFailure (Json.Decode.field "BackendTermStartFailure" (Json.Decode.string))
|
||||||
|
, Json.Decode.string
|
||||||
|
|> Json.Decode.andThen
|
||||||
|
(\x ->
|
||||||
|
case x of
|
||||||
|
"TermNotStarted" ->
|
||||||
|
Json.Decode.succeed TermNotStarted
|
||||||
|
unexpected ->
|
||||||
|
Json.Decode.fail <| "Unexpected variant " ++ unexpected
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
type alias TerminalScreen =
|
||||||
|
{ cols : Int
|
||||||
|
, rows : Int
|
||||||
|
, damagedLines : List (Line)
|
||||||
|
, cursorLoc : Cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
terminalScreenDecoder : Json.Decode.Decoder TerminalScreen
|
||||||
|
terminalScreenDecoder =
|
||||||
|
Json.Decode.succeed TerminalScreen
|
||||||
|
|> Json.Decode.andThen (\x -> Json.Decode.map x (Json.Decode.field "cols" (Json.Decode.int)))
|
||||||
|
|> Json.Decode.andThen (\x -> Json.Decode.map x (Json.Decode.field "rows" (Json.Decode.int)))
|
||||||
|
|> Json.Decode.andThen (\x -> Json.Decode.map x (Json.Decode.field "damaged_lines" (Json.Decode.list (lineDecoder))))
|
||||||
|
|> Json.Decode.andThen (\x -> Json.Decode.map x (Json.Decode.field "cursor_loc" (cursorDecoder)))
|
||||||
|
|
||||||
|
|
||||||
|
type alias Cursor =
|
||||||
|
{ row : Int
|
||||||
|
, col : Int
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
cursorDecoder : Json.Decode.Decoder Cursor
|
||||||
|
cursorDecoder =
|
||||||
|
Json.Decode.succeed Cursor
|
||||||
|
|> Json.Decode.andThen (\x -> Json.Decode.map x (Json.Decode.field "row" (Json.Decode.int)))
|
||||||
|
|> Json.Decode.andThen (\x -> Json.Decode.map x (Json.Decode.field "col" (Json.Decode.int)))
|
||||||
|
|
||||||
|
|
||||||
|
type alias Line =
|
||||||
|
{ line : Int
|
||||||
|
, content : String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
lineDecoder : Json.Decode.Decoder Line
|
||||||
|
lineDecoder =
|
||||||
|
Json.Decode.succeed Line
|
||||||
|
|> Json.Decode.andThen (\x -> Json.Decode.map x (Json.Decode.field "line" (Json.Decode.int)))
|
||||||
|
|> Json.Decode.andThen (\x -> Json.Decode.map x (Json.Decode.field "content" (Json.Decode.string)))
|
||||||
|
|
||||||
|
|
||||||
|
type LandingDownMsg
|
||||||
|
= Greeting (String)
|
||||||
|
| TimeUpdate (String)
|
||||||
|
|
||||||
|
|
||||||
|
landingDownMsgDecoder : Json.Decode.Decoder LandingDownMsg
|
||||||
|
landingDownMsgDecoder =
|
||||||
|
Json.Decode.oneOf
|
||||||
|
[ Json.Decode.map Greeting (Json.Decode.field "Greeting" (Json.Decode.string))
|
||||||
|
, Json.Decode.map TimeUpdate (Json.Decode.field "TimeUpdate" (Json.Decode.string))
|
||||||
|
]
|
||||||
|
|
|
@ -47,6 +47,7 @@ view model =
|
||||||
resources = headerButton "Resources"
|
resources = headerButton "Resources"
|
||||||
about = headerButton "About"
|
about = headerButton "About"
|
||||||
contact = headerButton "Contact"
|
contact = headerButton "Contact"
|
||||||
|
term = headerButton "Term"
|
||||||
in
|
in
|
||||||
Element.row [Element.width Element.fill,
|
Element.row [Element.width Element.fill,
|
||||||
Element.spacing 15,
|
Element.spacing 15,
|
||||||
|
@ -57,4 +58,5 @@ view model =
|
||||||
, resources
|
, resources
|
||||||
, about
|
, about
|
||||||
, contact
|
, contact
|
||||||
|
, term
|
||||||
]
|
]
|
||||||
|
|
|
@ -10,6 +10,7 @@ import Browser.Navigation
|
||||||
import Browser exposing (UrlRequest)
|
import Browser exposing (UrlRequest)
|
||||||
import Html exposing (header)
|
import Html exposing (header)
|
||||||
import Ports
|
import Ports
|
||||||
|
import EncodeDecode
|
||||||
|
|
||||||
-- internal imports
|
-- internal imports
|
||||||
import Body
|
import Body
|
||||||
|
@ -30,16 +31,20 @@ type alias Model =
|
||||||
init : () -> Url.Url -> Browser.Navigation.Key -> ( Model, Cmd Msg )
|
init : () -> Url.Url -> Browser.Navigation.Key -> ( Model, Cmd Msg )
|
||||||
init flags url key =
|
init flags url key =
|
||||||
let
|
let
|
||||||
page = Body.init flags
|
(page, cmd) = Body.handleRoute url.path
|
||||||
header = Header.init flags
|
header = Header.init flags
|
||||||
model =
|
model =
|
||||||
{ key = key
|
{ key = key
|
||||||
, url = url
|
, url = url
|
||||||
, page = Body.handleRoute url.path
|
, page = page
|
||||||
, header = header
|
, header = header
|
||||||
}
|
}
|
||||||
in
|
in
|
||||||
(model, Ports.socketOpen)
|
( model
|
||||||
|
, Cmd.batch
|
||||||
|
[ cmd |> Cmd.map Body
|
||||||
|
, Ports.socketOpen
|
||||||
|
] )
|
||||||
|
|
||||||
|
|
||||||
update : Msg -> Model -> (Model, Cmd Msg)
|
update : Msg -> Model -> (Model, Cmd Msg)
|
||||||
|
@ -52,9 +57,10 @@ update msg model =
|
||||||
( {model | page = newPage}, cmd |> Cmd.map Body )
|
( {model | page = newPage}, cmd |> Cmd.map Body )
|
||||||
UrlChanged url ->
|
UrlChanged url ->
|
||||||
let
|
let
|
||||||
newModel = {model | page = Body.handleRoute url.path, url = url}
|
(page, cmd) = Body.handleRoute url.path
|
||||||
|
newModel = {model | page = page, url = url}
|
||||||
in
|
in
|
||||||
( newModel, Cmd.none )
|
( newModel, cmd |> Cmd.map Body )
|
||||||
UrlRequest (Browser.Internal url) ->
|
UrlRequest (Browser.Internal url) ->
|
||||||
( model, Browser.Navigation.pushUrl model.key (Url.toString url) )
|
( model, Browser.Navigation.pushUrl model.key (Url.toString url) )
|
||||||
_ -> (model, Cmd.none)
|
_ -> (model, Cmd.none)
|
||||||
|
|
|
@ -11,8 +11,8 @@ import Element exposing (Element)
|
||||||
type alias Model = {}
|
type alias Model = {}
|
||||||
type alias Msg = {}
|
type alias Msg = {}
|
||||||
|
|
||||||
init : () -> Model
|
init : () -> (Model, Cmd Msg)
|
||||||
init flags = {}
|
init flags = ({}, Cmd.none)
|
||||||
|
|
||||||
subscriptions : Model -> Sub Msg
|
subscriptions : Model -> Sub Msg
|
||||||
subscriptions _ = Sub.none
|
subscriptions _ = Sub.none
|
||||||
|
|
|
@ -11,8 +11,8 @@ import Element exposing (Element)
|
||||||
type alias Model = {}
|
type alias Model = {}
|
||||||
type alias Msg = {}
|
type alias Msg = {}
|
||||||
|
|
||||||
init : () -> Model
|
init : () -> (Model, Cmd Msg)
|
||||||
init flags = {}
|
init flags = ({}, Cmd.none)
|
||||||
|
|
||||||
subscriptions : Model -> Sub Msg
|
subscriptions : Model -> Sub Msg
|
||||||
subscriptions _ = Sub.none
|
subscriptions _ = Sub.none
|
||||||
|
|
|
@ -15,57 +15,29 @@ import Html.Attributes exposing (placeholder)
|
||||||
import Element.Input
|
import Element.Input
|
||||||
import Element.Background
|
import Element.Background
|
||||||
|
|
||||||
|
import EncodeDecode
|
||||||
|
|
||||||
type alias Model = {
|
type alias Model = {
|
||||||
time : String,
|
time : String,
|
||||||
greetWidgetText : String,
|
greetWidgetText : String,
|
||||||
greeting : String
|
greeting : String
|
||||||
}
|
}
|
||||||
type DownMsgLanding
|
|
||||||
= Greeting String
|
|
||||||
| TimeUpdate String
|
|
||||||
|
|
||||||
decodeDownMsgLanding : Decode.Decoder DownMsgLanding
|
|
||||||
decodeDownMsgLanding =
|
|
||||||
let
|
|
||||||
t = Decode.map Greeting (Decode.field "Greeting" Decode.string)
|
|
||||||
in
|
|
||||||
Decode.oneOf
|
|
||||||
[ Decode.map Greeting (Decode.field "Greeting" Decode.string)
|
|
||||||
, Decode.map TimeUpdate (Decode.field "TimeUpdate" Decode.string)
|
|
||||||
]
|
|
||||||
|
|
||||||
decodeDownMsg : Decode.Decoder Msg
|
|
||||||
decodeDownMsg =
|
|
||||||
let
|
|
||||||
decoder = Decode.field "Landing" decodeDownMsgLanding
|
|
||||||
in
|
|
||||||
Decode.map DownMsg decoder
|
|
||||||
|
|
||||||
type UpMsgLanding = RequestGreet String
|
|
||||||
|
|
||||||
encodeUpMsgLanding : UpMsgLanding -> Value
|
|
||||||
encodeUpMsgLanding msg =
|
|
||||||
case msg of
|
|
||||||
RequestGreet name ->
|
|
||||||
Encode.object
|
|
||||||
[ ( "Landing", Encode.object
|
|
||||||
[ ( "RequestGreet", Encode.string name ) ]
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
type Msg
|
type Msg
|
||||||
= DownMsg DownMsgLanding
|
= DownMsg EncodeDecode.DownMsg
|
||||||
| UpMsg UpMsgLanding
|
| UpMsg EncodeDecode.LandingUpMsg
|
||||||
| DecodeError Decode.Error
|
| DecodeError Decode.Error
|
||||||
| GreetWidgetText String
|
| GreetWidgetText String
|
||||||
| NoOp
|
| NoOp
|
||||||
|
|
||||||
init : () -> Model
|
init : () -> (Model, Cmd Msg)
|
||||||
init flags = {
|
init flags = (
|
||||||
time = "time not yet set",
|
{ time = "time not yet set"
|
||||||
greetWidgetText = "",
|
, greetWidgetText = ""
|
||||||
greeting = ""
|
, greeting = ""
|
||||||
}
|
},
|
||||||
|
Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
subscriptions : Model -> Sub Msg
|
subscriptions : Model -> Sub Msg
|
||||||
subscriptions model =
|
subscriptions model =
|
||||||
|
@ -75,8 +47,8 @@ subscriptions model =
|
||||||
(\_ -> NoOp)
|
(\_ -> NoOp)
|
||||||
(\_ -> NoOp)
|
(\_ -> NoOp)
|
||||||
(\message ->
|
(\message ->
|
||||||
case Decode.decodeString decodeDownMsg message.data of
|
case Decode.decodeString EncodeDecode.downMsgDecoder message.data of
|
||||||
Ok msg -> msg
|
Ok msg -> DownMsg msg
|
||||||
Err err -> DecodeError err
|
Err err -> DecodeError err
|
||||||
)
|
)
|
||||||
(\_ -> NoOp)
|
(\_ -> NoOp)
|
||||||
|
@ -85,12 +57,15 @@ subscriptions model =
|
||||||
update : Msg -> Model -> (Model, Cmd Msg)
|
update : Msg -> Model -> (Model, Cmd Msg)
|
||||||
update msg model =
|
update msg model =
|
||||||
case msg of
|
case msg of
|
||||||
DownMsg (TimeUpdate time) -> ( {model | time = time}, Cmd.none )
|
DownMsg (EncodeDecode.LandingDownMsg (EncodeDecode.TimeUpdate time)) ->
|
||||||
DownMsg (Greeting greeting) -> ( {model | greeting = greeting}, Cmd.none)
|
( {model | time = time}, Cmd.none )
|
||||||
UpMsg upMsgLanding -> (
|
DownMsg (EncodeDecode.LandingDownMsg (EncodeDecode.Greeting greeting)) ->
|
||||||
model,
|
( {model | greeting = greeting}, Cmd.none)
|
||||||
upMsgLanding |> encodeUpMsgLanding |> Ports.socketSend
|
UpMsg upMsg ->
|
||||||
)
|
let cmd = Ports.socketSend <| EncodeDecode.upMsgEncoder <| EncodeDecode.LandingUpMsg upMsg
|
||||||
|
in
|
||||||
|
(model, cmd)
|
||||||
|
|
||||||
GreetWidgetText text -> ( {model | greetWidgetText = text}, Cmd.none )
|
GreetWidgetText text -> ( {model | greetWidgetText = text}, Cmd.none )
|
||||||
_ -> (model, Cmd.none)
|
_ -> (model, Cmd.none)
|
||||||
|
|
||||||
|
@ -106,7 +81,7 @@ greetWidget model =
|
||||||
, Element.width (Element.fill |> Element.maximum 100)
|
, Element.width (Element.fill |> Element.maximum 100)
|
||||||
, Element.height (Element.fill |> Element.maximum 50)
|
, Element.height (Element.fill |> Element.maximum 50)
|
||||||
]
|
]
|
||||||
{ onPress = Just <| UpMsg <| RequestGreet model.greetWidgetText
|
{ onPress = Just <| UpMsg <| EncodeDecode.RequestGreet model.greetWidgetText
|
||||||
, label = Element.text "Greet"
|
, label = Element.text "Greet"
|
||||||
}
|
}
|
||||||
placeholder = case model.greetWidgetText of
|
placeholder = case model.greetWidgetText of
|
||||||
|
|
|
@ -11,8 +11,8 @@ import Element exposing (Element)
|
||||||
type alias Model = {}
|
type alias Model = {}
|
||||||
type alias Msg = {}
|
type alias Msg = {}
|
||||||
|
|
||||||
init : () -> Model
|
init : () -> (Model, Cmd Msg)
|
||||||
init flags = {}
|
init flags = ({}, Cmd.none)
|
||||||
|
|
||||||
subscriptions : Model -> Sub Msg
|
subscriptions : Model -> Sub Msg
|
||||||
subscriptions _ = Sub.none
|
subscriptions _ = Sub.none
|
||||||
|
|
|
@ -11,8 +11,8 @@ import Element exposing (Element)
|
||||||
type alias Model = {}
|
type alias Model = {}
|
||||||
type alias Msg = {}
|
type alias Msg = {}
|
||||||
|
|
||||||
init : () -> Model
|
init : () -> (Model, Cmd Msg)
|
||||||
init flags = {}
|
init flags = ({}, Cmd.none)
|
||||||
|
|
||||||
subscriptions : Model -> Sub Msg
|
subscriptions : Model -> Sub Msg
|
||||||
subscriptions _ = Sub.none
|
subscriptions _ = Sub.none
|
||||||
|
|
|
@ -11,8 +11,8 @@ import Element exposing (Element)
|
||||||
type alias Model = {}
|
type alias Model = {}
|
||||||
type alias Msg = {}
|
type alias Msg = {}
|
||||||
|
|
||||||
init : () -> Model
|
init : () -> (Model, Cmd Msg)
|
||||||
init flags = {}
|
init flags = ({}, Cmd.none)
|
||||||
|
|
||||||
subscriptions : Model -> Sub Msg
|
subscriptions : Model -> Sub Msg
|
||||||
subscriptions _ = Sub.none
|
subscriptions _ = Sub.none
|
||||||
|
|
407
frontend/src/Page/Term.elm
Normal file
407
frontend/src/Page/Term.elm
Normal file
|
@ -0,0 +1,407 @@
|
||||||
|
module Page.Term exposing
|
||||||
|
( Model
|
||||||
|
, Msg
|
||||||
|
, view
|
||||||
|
, init
|
||||||
|
, update
|
||||||
|
, subscriptions
|
||||||
|
)
|
||||||
|
import Element exposing (Element)
|
||||||
|
import Websockets
|
||||||
|
import Ports
|
||||||
|
import Json.Decode as Decode
|
||||||
|
import Json.Encode as Encode exposing (Value)
|
||||||
|
import Html.Attributes exposing (placeholder)
|
||||||
|
import Browser.Events exposing (onKeyDown, onKeyUp)
|
||||||
|
|
||||||
|
import EncodeDecode
|
||||||
|
|
||||||
|
import List.Extra
|
||||||
|
import Element.Font
|
||||||
|
import Element.Input
|
||||||
|
import Element.Background
|
||||||
|
import Debug
|
||||||
|
import Element.Border
|
||||||
|
import String exposing (pad)
|
||||||
|
|
||||||
|
type alias RawKey = String
|
||||||
|
|
||||||
|
type Key
|
||||||
|
= Character RawKey Char
|
||||||
|
| Control RawKey String
|
||||||
|
| Invalid
|
||||||
|
|
||||||
|
prependKey : Key -> List Key -> List Key
|
||||||
|
prependKey key keys =
|
||||||
|
case key of
|
||||||
|
Invalid ->
|
||||||
|
keys
|
||||||
|
_ ->
|
||||||
|
if List.any (sameRawKey key) keys then
|
||||||
|
keys
|
||||||
|
else
|
||||||
|
key :: keys
|
||||||
|
|
||||||
|
removeKey : Key -> List Key -> List Key
|
||||||
|
removeKey key keys =
|
||||||
|
List.filter (not << sameRawKey key) keys
|
||||||
|
|
||||||
|
sameRawKey : Key -> Key -> Bool
|
||||||
|
sameRawKey key1 key2 =
|
||||||
|
case (key1, key2) of
|
||||||
|
(Character raw1 _, Character raw2 _) ->
|
||||||
|
raw1 == raw2
|
||||||
|
|
||||||
|
(Control raw1 _, Control raw2 _) ->
|
||||||
|
raw1 == raw2
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
False
|
||||||
|
|
||||||
|
|
||||||
|
keyDecoder : Decode.Decoder Key
|
||||||
|
keyDecoder =
|
||||||
|
Decode.map2
|
||||||
|
toKey
|
||||||
|
(Decode.field "key" Decode.string)
|
||||||
|
(Decode.field "code" Decode.string)
|
||||||
|
|
||||||
|
toKey : String -> String -> Key
|
||||||
|
toKey key code =
|
||||||
|
if key == "Tab" then
|
||||||
|
Invalid
|
||||||
|
else
|
||||||
|
case String.uncons key of
|
||||||
|
Just (char, "") ->
|
||||||
|
Character code char
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
Control code key
|
||||||
|
|
||||||
|
isLowercaseAlpha : Char -> Bool
|
||||||
|
isLowercaseAlpha c =
|
||||||
|
charIsBetweenInclusive c 'a' 'z'
|
||||||
|
|
||||||
|
|
||||||
|
processForCtrlChar : Char -> Char
|
||||||
|
processForCtrlChar c =
|
||||||
|
if isLowercaseAlpha c then
|
||||||
|
Char.fromCode (Char.toCode c - Char.toCode 'a' + 1)
|
||||||
|
else if charIsBetweenInclusive c '[' '_' then
|
||||||
|
Char.fromCode (Char.toCode c - Char.toCode '[')
|
||||||
|
else
|
||||||
|
c
|
||||||
|
|
||||||
|
|
||||||
|
charIsBetweenInclusive : Char -> Char -> Char -> Bool
|
||||||
|
charIsBetweenInclusive c loChar hiChar =
|
||||||
|
let
|
||||||
|
cCode = Char.toCode c
|
||||||
|
loCode = Char.toCode loChar
|
||||||
|
hiCode = Char.toCode hiChar
|
||||||
|
in
|
||||||
|
cCode >= loCode && cCode <= hiCode
|
||||||
|
|
||||||
|
controlInKeyList : List Key -> Bool
|
||||||
|
controlInKeyList list =
|
||||||
|
case list of
|
||||||
|
[] -> False
|
||||||
|
x :: xs ->
|
||||||
|
case x of
|
||||||
|
(Control _ "Control") -> True
|
||||||
|
_ -> controlInKeyList xs
|
||||||
|
|
||||||
|
|
||||||
|
keyToString : List Key -> Maybe String
|
||||||
|
keyToString keyList =
|
||||||
|
case keyList of
|
||||||
|
(Character _ c) :: _ ->
|
||||||
|
if (controlInKeyList keyList) then
|
||||||
|
Just <| String.fromChar <| processForCtrlChar c
|
||||||
|
else
|
||||||
|
Just <| String.fromChar c
|
||||||
|
(Control _ "Enter") :: _ -> Just "\r"
|
||||||
|
(Control _ "Escape") :: _ -> Just "\u{001B}"
|
||||||
|
(Control _ "Backspace") :: _ -> Just "\u{0008}"
|
||||||
|
(Control _ "ArrowUp") :: _ -> Just "\u{001B}[A"
|
||||||
|
(Control _ "ArrowDown") :: _ -> Just "\u{001B}[B"
|
||||||
|
(Control _ "ArrowLeft") :: _ -> Just "\u{001B}[D"
|
||||||
|
(Control _ "ArrowRight") :: _ -> Just "\u{001B}[C"
|
||||||
|
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
|
||||||
|
init : () -> (Model, Cmd Msg)
|
||||||
|
init flags =
|
||||||
|
let
|
||||||
|
-- Initial model with default terminal screen
|
||||||
|
initialLine =
|
||||||
|
{ line = 0
|
||||||
|
, content = "Spinning up container..."
|
||||||
|
}
|
||||||
|
initialCursor =
|
||||||
|
{ row = 0
|
||||||
|
, col = 0
|
||||||
|
}
|
||||||
|
initialModel =
|
||||||
|
{ termState =
|
||||||
|
{ cols = String.length initialLine.content
|
||||||
|
, rows = 1
|
||||||
|
, lines = [initialLine]
|
||||||
|
, cursorLoc = initialCursor
|
||||||
|
}
|
||||||
|
, heldKeys = []
|
||||||
|
, err = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
upMsg = EncodeDecode.TermUpMsg
|
||||||
|
<| EncodeDecode.RequestFullTermState
|
||||||
|
cmd =
|
||||||
|
Ports.socketSend <| EncodeDecode.upMsgEncoder upMsg
|
||||||
|
in
|
||||||
|
(initialModel, cmd)
|
||||||
|
|
||||||
|
subscriptions : Model -> Sub Msg
|
||||||
|
subscriptions model =
|
||||||
|
let
|
||||||
|
ws = Ports.socketOnEvent
|
||||||
|
(Websockets.EventHandlers
|
||||||
|
(SocketOpened)
|
||||||
|
(\_ -> NoOp)
|
||||||
|
(\_ -> NoOp)
|
||||||
|
(\message ->
|
||||||
|
case Decode.decodeString EncodeDecode.downMsgDecoder message.data of
|
||||||
|
Ok msg -> DownMsg msg
|
||||||
|
Err err -> DecodeError err
|
||||||
|
)
|
||||||
|
(\_ -> NoOp)
|
||||||
|
)
|
||||||
|
keyDownSubscription = onKeyDown (Decode.map KeyDown keyDecoder)
|
||||||
|
keyUpSubscription = onKeyUp (Decode.map KeyUp keyDecoder)
|
||||||
|
in
|
||||||
|
Sub.batch [ ws
|
||||||
|
, keyDownSubscription
|
||||||
|
, keyUpSubscription
|
||||||
|
]
|
||||||
|
|
||||||
|
makeGridWithNewlines : String -> Int -> String
|
||||||
|
makeGridWithNewlines string cols =
|
||||||
|
let
|
||||||
|
rows =
|
||||||
|
string
|
||||||
|
|> String.toList
|
||||||
|
|> List.indexedMap Tuple.pair
|
||||||
|
|> groupByRow cols
|
||||||
|
|> List.map (List.map Tuple.second) -- Extract characters
|
||||||
|
|> List.map String.fromList -- Convert rows to strings
|
||||||
|
in
|
||||||
|
String.join "\n" rows
|
||||||
|
|
||||||
|
|
||||||
|
groupByRow : Int -> List (Int, Char) -> List (List (Int, Char))
|
||||||
|
groupByRow cols list =
|
||||||
|
case list of
|
||||||
|
[] ->
|
||||||
|
[]
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
let
|
||||||
|
(row, rest) =
|
||||||
|
List.Extra.splitAt cols list
|
||||||
|
in
|
||||||
|
row :: groupByRow cols rest
|
||||||
|
|
||||||
|
type alias TermState =
|
||||||
|
{ cols : Int
|
||||||
|
, rows : Int
|
||||||
|
, lines : List (EncodeDecode.Line)
|
||||||
|
, cursorLoc : EncodeDecode.Cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
type alias Model = {
|
||||||
|
termState : TermState,
|
||||||
|
heldKeys : List Key,
|
||||||
|
err : Maybe String
|
||||||
|
}
|
||||||
|
|
||||||
|
type Msg
|
||||||
|
= DownMsg EncodeDecode.DownMsg
|
||||||
|
| DecodeError Decode.Error
|
||||||
|
| GreetWidgetText String
|
||||||
|
| SocketOpened Websockets.WebsocketOpened
|
||||||
|
| KeyDown Key
|
||||||
|
| KeyUp Key
|
||||||
|
| NoOp
|
||||||
|
|
||||||
|
updateTermState : EncodeDecode.TerminalScreen -> TermState -> TermState
|
||||||
|
updateTermState termScreenUpdate currTermState =
|
||||||
|
let
|
||||||
|
fullUpdate = termScreenUpdate.rows == (List.length termScreenUpdate.damagedLines)
|
||||||
|
newLines = case fullUpdate of
|
||||||
|
True -> termScreenUpdate.damagedLines
|
||||||
|
False -> updateLines currTermState.lines termScreenUpdate.damagedLines
|
||||||
|
in
|
||||||
|
{ cols = termScreenUpdate.cols
|
||||||
|
, rows = termScreenUpdate.rows
|
||||||
|
, lines = newLines
|
||||||
|
, cursorLoc = termScreenUpdate.cursorLoc
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLines : List EncodeDecode.Line -> List EncodeDecode.Line -> List EncodeDecode.Line
|
||||||
|
updateLines currLines damagedLines =
|
||||||
|
List.map
|
||||||
|
(\oldLine ->
|
||||||
|
case List.filter (\damaged -> damaged.line == oldLine.line) damagedLines of
|
||||||
|
[] ->
|
||||||
|
oldLine -- No replacement, keep the old line
|
||||||
|
|
||||||
|
damaged :: _ ->
|
||||||
|
damaged -- Replace with the first matching damaged line
|
||||||
|
)
|
||||||
|
currLines
|
||||||
|
|
||||||
|
update : Msg -> Model -> (Model, Cmd Msg)
|
||||||
|
update msg model =
|
||||||
|
case msg of
|
||||||
|
DownMsg (EncodeDecode.TermDownMsg (EncodeDecode.TermUpdate terminalScreen)) ->
|
||||||
|
({ model | termState = updateTermState terminalScreen model.termState }, Cmd.none)
|
||||||
|
DownMsg (EncodeDecode.TermDownMsg (EncodeDecode.BackendTermStartFailure err_msg)) ->
|
||||||
|
({ model | err = Just err_msg }, Cmd.none)
|
||||||
|
SocketOpened _ ->
|
||||||
|
let
|
||||||
|
upMsg = EncodeDecode.TermUpMsg
|
||||||
|
<| EncodeDecode.RequestFullTermState
|
||||||
|
cmd =
|
||||||
|
Ports.socketSend <| EncodeDecode.upMsgEncoder upMsg
|
||||||
|
in
|
||||||
|
(model, cmd)
|
||||||
|
KeyDown key ->
|
||||||
|
let
|
||||||
|
newHeldKeys = prependKey key model.heldKeys
|
||||||
|
processedChar = keyToString newHeldKeys
|
||||||
|
cmd = case processedChar of
|
||||||
|
Just c ->
|
||||||
|
let
|
||||||
|
upMsg =
|
||||||
|
EncodeDecode.TermUpMsg <|
|
||||||
|
EncodeDecode.SendCharacter <|
|
||||||
|
c
|
||||||
|
in
|
||||||
|
Ports.socketSend <| EncodeDecode.upMsgEncoder upMsg
|
||||||
|
Nothing -> Cmd.none
|
||||||
|
in
|
||||||
|
({model | heldKeys = newHeldKeys}, cmd)
|
||||||
|
KeyUp key ->
|
||||||
|
({model | heldKeys = removeKey key model.heldKeys}, Cmd.none)
|
||||||
|
_ -> (model, Cmd.none)
|
||||||
|
|
||||||
|
termFont = Element.Font.family
|
||||||
|
[ Element.Font.typeface "Courier Prime"
|
||||||
|
, Element.Font.monospace
|
||||||
|
]
|
||||||
|
|
||||||
|
lightOrangeBackground = Element.Background.color (Element.rgb255 255 87 51)
|
||||||
|
lightGreenBackground = Element.Background.color (Element.rgb255 175 225 175)
|
||||||
|
|
||||||
|
view : Model -> Element Msg
|
||||||
|
view model =
|
||||||
|
let
|
||||||
|
termContentString = case model.err of
|
||||||
|
Nothing -> String.join "\n" (List.map .content model.termState.lines)
|
||||||
|
Just err -> err
|
||||||
|
useNotes =
|
||||||
|
Element.column
|
||||||
|
-- light orange background
|
||||||
|
-- rounded corners
|
||||||
|
[ lightGreenBackground
|
||||||
|
, Element.padding 10
|
||||||
|
, Element.spacing 5
|
||||||
|
, Element.width (Element.fill)
|
||||||
|
, Element.Border.rounded 10
|
||||||
|
, Element.Font.color (Element.rgb255 255 255 255)
|
||||||
|
]
|
||||||
|
[
|
||||||
|
Element.paragraph [] [Element.text <| "Written in Elm. Works well in Chrome. Your mileage with other browsers may vary!"],
|
||||||
|
Element.paragraph [] [Element.text <| "Container is nix based, so install programs with `nix-shell -p` vim htop for example."],
|
||||||
|
Element.paragraph [] [Element.text <| "Cursor and line higlighting not yet implmented.\n"],
|
||||||
|
Element.paragraph [] [Element.text <| "Container stats: 4 cores, 6gb memory"]
|
||||||
|
]
|
||||||
|
cursorString =
|
||||||
|
let
|
||||||
|
cursorRow = model.termState.cursorLoc.row
|
||||||
|
cursorCol = model.termState.cursorLoc.col
|
||||||
|
|
||||||
|
paddingLeft = String.repeat cursorCol " "
|
||||||
|
cursorChar = "█" -- Cursor box character
|
||||||
|
paddingRight = String.repeat (model.termState.cols - cursorCol - 1) " "
|
||||||
|
activeRow = paddingLeft ++ cursorChar ++ paddingRight
|
||||||
|
|
||||||
|
standardRow = (String.repeat model.termState.cols " ")
|
||||||
|
|
||||||
|
paddingAbove = String.repeat cursorRow standardRow
|
||||||
|
paddingBelow = String.repeat (model.termState.rows - cursorRow - 1) standardRow
|
||||||
|
|
||||||
|
stringBeforeNewlines = paddingAbove ++ activeRow ++ paddingBelow
|
||||||
|
in
|
||||||
|
case model.err of
|
||||||
|
Nothing ->
|
||||||
|
makeGridWithNewlines stringBeforeNewlines model.termState.cols
|
||||||
|
Just _ -> ""
|
||||||
|
cursorLayer =
|
||||||
|
Element.el
|
||||||
|
[ termFont
|
||||||
|
, Element.width (Element.fill |> Element.maximum 770 |> Element.minimum 770)
|
||||||
|
, Element.height (Element.fill |> Element.maximum 390 |> Element.minimum 390)
|
||||||
|
, Element.Font.color (Element.rgba255 0 0 0 0.3) -- White cursor
|
||||||
|
]
|
||||||
|
(cursorString |> Element.text)
|
||||||
|
|
||||||
|
|
||||||
|
mainTermLayer =
|
||||||
|
Element.el
|
||||||
|
[ termFont
|
||||||
|
, Element.width (Element.fill |> Element.maximum 770 |> Element.minimum 770)
|
||||||
|
, Element.height (Element.fill |> Element.maximum 390 |> Element.minimum 390)
|
||||||
|
]
|
||||||
|
(termContentString |> Element.text)
|
||||||
|
keyDebugger =
|
||||||
|
Element.el
|
||||||
|
[ lightOrangeBackground
|
||||||
|
, Element.Border.rounded 10
|
||||||
|
, Element.width (Element.fill)
|
||||||
|
, Element.Font.color (Element.rgb255 255 255 255)
|
||||||
|
, Element.padding 10
|
||||||
|
]
|
||||||
|
(Element.text <| ("Debug keys: " ++ keysToText model.heldKeys))
|
||||||
|
in
|
||||||
|
Element.column
|
||||||
|
[ Element.spacing 20
|
||||||
|
, Element.alignTop
|
||||||
|
, Element.alignLeft
|
||||||
|
, Element.Font.size 16
|
||||||
|
]
|
||||||
|
[ useNotes
|
||||||
|
, Element.el
|
||||||
|
[Element.inFront cursorLayer]
|
||||||
|
(mainTermLayer)
|
||||||
|
, keyDebugger
|
||||||
|
]
|
||||||
|
|
||||||
|
keysToText : List Key -> String
|
||||||
|
keysToText keys =
|
||||||
|
keys
|
||||||
|
|> List.map keyToStringDebug
|
||||||
|
|> String.join ""
|
||||||
|
|
||||||
|
keyToStringDebug : Key -> String
|
||||||
|
keyToStringDebug key =
|
||||||
|
case key of
|
||||||
|
Character _ c ->
|
||||||
|
if c == ' ' then
|
||||||
|
"⊔"
|
||||||
|
else
|
||||||
|
String.fromChar c
|
||||||
|
|
||||||
|
Control _ str ->
|
||||||
|
"<" ++ str ++ ">"
|
||||||
|
_ -> ""
|
|
@ -42,7 +42,19 @@ function initSockets(app) {
|
||||||
var name_2 = command.name, data = command.data;
|
var name_2 = command.name, data = command.data;
|
||||||
var socket = sockets.get(name_2);
|
var socket = sockets.get(name_2);
|
||||||
if (socket) {
|
if (socket) {
|
||||||
|
if (socket.ws.readyState === WebSocket.OPEN) {
|
||||||
socket.ws.send(typeof data === "object" ? JSON.stringify(data) : data);
|
socket.ws.send(typeof data === "object" ? JSON.stringify(data) : data);
|
||||||
|
} else {
|
||||||
|
console.warn(`Cannot send message. WebSocket "${name_2}" is not open. Current state: ${socket.ws.readyState}`);
|
||||||
|
app.ports.webSocketEvent.send({
|
||||||
|
type: "error",
|
||||||
|
name: name_2,
|
||||||
|
meta: socket.meta,
|
||||||
|
error: `WebSocket is not open. Current state: ${socket.ws.readyState}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`WebSocket "${name_2}" does not exist.`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue