term now supported and subcriptions now properly lifted

This commit is contained in:
Yehowshua Immanuel 2025-01-26 16:12:06 -05:00
parent cf3c395850
commit b285e2364e
25 changed files with 1700 additions and 153 deletions

View file

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

View file

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

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

View file

@ -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
View 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))
]

View file

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

View file

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

View 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
View file

Binary file not shown.

View file

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

View file

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

View file

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

View 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))
]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
View 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 ++ ">"
_ -> ""

View file

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