Gleam + Relm4 foreign node POC over Erlang distribution
0

Configure Feed

Select the types of activity you want to include in your feed.

Initial gleamtk POC: Gleam controller + Relm4 foreign node

Proof of concept pairing a Gleam BEAM process with a Relm4 (GTK4)
UI over the Erlang distribution protocol. Includes Nix flake,
justfile, smoke scripts, and protocol for title/label/count events.

author
nandi
date (Jul 26, 2026, 4:31 PM -0700) commit c190939f change-id zxnvrowp
+2928
+7
.gitignore
··· 1 + ui/target/ 2 + gleam/build/ 3 + **/*.beam 4 + erl_crash.dump 5 + .direnv/ 6 + result 7 + result-*
+78
README.md
··· 1 + # gleamtk 2 + 3 + Proof of concept: a **Relm4** (GTK4 / libadwaita) process that acts as an 4 + **Erlang foreign node**, controlled by a **Gleam** process over the Erlang 5 + distribution protocol. 6 + 7 + ``` 8 + Gleam (beam) ── cookie + epmd ──► Relm4 foreign node (Rust) 9 + gleamtk@127.0.0.1 ui@127.0.0.1 10 + controller ── {gui, Node} ! ──► registered name: gui 11 + ◄── events ── {controller, …} ── clicks / ready / pong 12 + ``` 13 + 14 + ## Protocol 15 + 16 + **Gleam → UI** (`{gui, 'ui@127.0.0.1'} ! Term`): 17 + 18 + | Term | Effect | 19 + |------|--------| 20 + | `hello` | Handshake; UI records controller pid | 21 + | `{set_title, Binary}` | Window title | 22 + | `{set_label, Binary}` | Main label | 23 + | `{set_count, Int}` | Counter display | 24 + | `ping` | UI replies `pong` | 25 + 26 + **UI → Gleam** (to registered `controller` or the sender pid): 27 + 28 + | Term | Meaning | 29 + |------|---------| 30 + | `{ready, <<"ui@…">>}` | UI node is up | 31 + | `{clicked, Count}` | Primary button | 32 + | `{count_changed, Count}` | − button | 33 + | `pong` | Reply to `ping` | 34 + | `window_closed` | Window closed | 35 + 36 + ## Requirements 37 + 38 + - Nix (recommended) or: Erlang/OTP, Gleam, Rust, GTK4, libadwaita, epmd 39 + - A display (Wayland/X11) for the Relm4 window 40 + 41 + ## Run 42 + 43 + ```bash 44 + cd gleamtk 45 + nix develop # or: nix develop -c bash 46 + just run # starts UI then Gleam controller 47 + ``` 48 + 49 + Two terminals: 50 + 51 + ```bash 52 + # terminal 1 53 + just ui 54 + 55 + # terminal 2 56 + just gleam 57 + ``` 58 + 59 + Click **Click me** / **+** in the window. Gleam prints events and updates the 60 + label and count over distribution. 61 + 62 + ## Layout 63 + 64 + ``` 65 + gleamtk/ 66 + gleam/ # Gleam controller (BEAM node) 67 + ui/ # Relm4 + erl_dist foreign node 68 + justfile 69 + flake.nix 70 + ``` 71 + 72 + ## Notes 73 + 74 + - Cookie defaults to `gleamtk`; both sides must match. 75 + - Long node names on `127.0.0.1` avoid short-name / hostname mismatches. 76 + - The Rust side uses [`erl_dist`](https://crates.io/crates/erl_dist); the UI 77 + thread never blocks on distribution I/O. 78 + - This is a POC, not a full widget toolkit binding.
+27
flake.lock
··· 1 + { 2 + "nodes": { 3 + "nixpkgs": { 4 + "locked": { 5 + "lastModified": 1784796856, 6 + "narHash": "sha256-wWFrV5/Qbm+lyt5x20E/bSbfJiGKMo4RCxZV8cl/WZI=", 7 + "owner": "NixOS", 8 + "repo": "nixpkgs", 9 + "rev": "e2587caef70cea85dd97d7daab492899902dbf5d", 10 + "type": "github" 11 + }, 12 + "original": { 13 + "owner": "NixOS", 14 + "ref": "nixos-unstable", 15 + "repo": "nixpkgs", 16 + "type": "github" 17 + } 18 + }, 19 + "root": { 20 + "inputs": { 21 + "nixpkgs": "nixpkgs" 22 + } 23 + } 24 + }, 25 + "root": "root", 26 + "version": 7 27 + }
+111
flake.nix
··· 1 + { 2 + description = "gleamtk — Gleam controller + Relm4 foreign node (Erlang distribution POC)"; 3 + 4 + inputs = { 5 + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 + }; 7 + 8 + outputs = 9 + { self, nixpkgs }: 10 + let 11 + systems = [ 12 + "x86_64-linux" 13 + "aarch64-linux" 14 + ]; 15 + forAllSystems = nixpkgs.lib.genAttrs systems; 16 + in 17 + { 18 + packages = forAllSystems ( 19 + system: 20 + let 21 + pkgs = nixpkgs.legacyPackages.${system}; 22 + gleamtk-ui = pkgs.rustPlatform.buildRustPackage { 23 + pname = "gleamtk-ui"; 24 + version = "0.1.0"; 25 + src = ./ui; 26 + cargoLock.lockFile = ./ui/Cargo.lock; 27 + 28 + nativeBuildInputs = with pkgs; [ 29 + pkg-config 30 + wrapGAppsHook4 31 + ]; 32 + 33 + buildInputs = with pkgs; [ 34 + gtk4 35 + libadwaita 36 + adwaita-icon-theme 37 + hicolor-icon-theme 38 + ]; 39 + 40 + meta = with pkgs.lib; { 41 + description = "Relm4 foreign node controlled by Gleam over Erlang distribution"; 42 + license = licenses.mit; 43 + mainProgram = "gleamtk-ui"; 44 + platforms = platforms.linux; 45 + }; 46 + }; 47 + in 48 + { 49 + inherit gleamtk-ui; 50 + default = gleamtk-ui; 51 + } 52 + ); 53 + 54 + apps = forAllSystems (system: { 55 + default = { 56 + type = "app"; 57 + program = "${self.packages.${system}.gleamtk-ui}/bin/gleamtk-ui"; 58 + }; 59 + ui = { 60 + type = "app"; 61 + program = "${self.packages.${system}.gleamtk-ui}/bin/gleamtk-ui"; 62 + }; 63 + }); 64 + 65 + devShells = forAllSystems ( 66 + system: 67 + let 68 + pkgs = nixpkgs.legacyPackages.${system}; 69 + in 70 + { 71 + default = pkgs.mkShell { 72 + name = "gleamtk"; 73 + packages = with pkgs; [ 74 + gleam 75 + beam.packages.erlang_27.erlang 76 + rebar3 77 + rustc 78 + cargo 79 + rustfmt 80 + clippy 81 + pkg-config 82 + gtk4 83 + libadwaita 84 + gsettings-desktop-schemas 85 + adwaita-icon-theme 86 + hicolor-icon-theme 87 + just 88 + ]; 89 + 90 + # wrapGAppsHook rewrites packaged bins; bare `cargo run` still needs 91 + # schema/icon paths from the shell (see shellHook). 92 + nativeBuildInputs = with pkgs; [ wrapGAppsHook4 ]; 93 + 94 + shellHook = '' 95 + export ERL_EPMD_ADDRESS=127.0.0.1 96 + export GSETTINGS_SCHEMAS_PATH="${pkgs.gtk4}/share/gsettings-schemas/${pkgs.gtk4.name}:${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}:${pkgs.libadwaita}/share/gsettings-schemas/${pkgs.libadwaita.name}''${GSETTINGS_SCHEMAS_PATH:+:$GSETTINGS_SCHEMAS_PATH}" 97 + export XDG_DATA_DIRS="${pkgs.gsettings-desktop-schemas}/share:${pkgs.gtk4}/share:${pkgs.libadwaita}/share:${pkgs.adwaita-icon-theme}/share:${pkgs.hicolor-icon-theme}/share''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}" 98 + echo "gleamtk dev shell" 99 + echo " just ui # start Relm4 foreign node" 100 + echo " just gleam # start Gleam controller" 101 + echo " just run # ui in bg + gleam (needs display)" 102 + echo " just smoke # headless dist handshake test" 103 + echo " nix build # package gleamtk-ui" 104 + ''; 105 + }; 106 + } 107 + ); 108 + 109 + formatter = forAllSystems (system: nixpkgs.legacyPackages.${system}.nixfmt-rfc-style); 110 + }; 111 + }
+14
gleam/gleam.toml
··· 1 + name = "gleamtk" 2 + version = "0.1.0" 3 + description = "Gleam controller for a Relm4 foreign node (Erlang distribution POC)" 4 + licences = ["MIT"] 5 + target = "erlang" 6 + 7 + [dependencies] 8 + gleam_stdlib = ">= 0.59.0 and < 2.0.0" 9 + gleam_erlang = ">= 0.34.0 and < 2.0.0" 10 + gleam_otp = ">= 0.14.0 and < 2.0.0" 11 + argv = ">= 1.0.0 and < 2.0.0" 12 + 13 + [dev_dependencies] 14 + gleeunit = ">= 1.0.0 and < 2.0.0"
+22
gleam/manifest.toml
··· 1 + # Do not manually edit this file, it is managed by Gleam. 2 + # 3 + # This file locks the dependency versions used, to make your build 4 + # deterministic and to prevent unexpected versions from being included 5 + # in your application. 6 + # 7 + # You should check this file into your source control repository. 8 + 9 + packages = [ 10 + { name = "argv", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "3277D100448BDB4A29B6D58C0F36F631CBC349E8BDD09766C6309DF202831140" }, 11 + { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, 12 + { name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" }, 13 + { name = "gleam_stdlib", version = "1.0.3", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "1F543AFBA5D33DA493E6087F4E4C4F20D899411343512686C98A8ABB2963CF22" }, 14 + { name = "gleeunit", version = "1.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "EC31ABA74256AEA531EDF8169931D775BBB384FED0A8A1BDC4DD9354E3E21826" }, 15 + ] 16 + 17 + [requirements] 18 + argv = { version = ">= 1.0.0 and < 2.0.0" } 19 + gleam_erlang = { version = ">= 0.34.0 and < 2.0.0" } 20 + gleam_otp = { version = ">= 0.14.0 and < 2.0.0" } 21 + gleam_stdlib = { version = ">= 0.59.0 and < 2.0.0" } 22 + gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
+139
gleam/src/gleamtk.gleam
··· 1 + //// Gleam controller for the Relm4 foreign node. 2 + //// 3 + //// Starts as a distributed BEAM node, registers as `controller`, connects to 4 + //// `ui@127.0.0.1`, drives the window via term messages, and prints UI events. 5 + 6 + import argv 7 + import gleam/int 8 + import gleam/io 9 + import gleam/string 10 + import gleamtk/dist 11 + 12 + const default_ui_node = "ui@127.0.0.1" 13 + 14 + const gui_name = "gui" 15 + 16 + const cookie_hint = "gleamtk" 17 + 18 + pub fn main() { 19 + let ui_node = case argv.load().arguments { 20 + [node, ..] -> node 21 + _ -> default_ui_node 22 + } 23 + 24 + io.println("═══ gleamtk controller ═══") 25 + io.println("cookie should be: " <> cookie_hint) 26 + io.println("this node: (see ERL_FLAGS -name)") 27 + io.println("ui node: " <> ui_node) 28 + io.println("") 29 + 30 + // Register so the UI can REG_SEND to `controller`. 31 + case dist.register_name(dist.atom("controller")) { 32 + Ok(Nil) -> io.println("registered as: controller") 33 + Error(Nil) -> io.println("warning: could not register as controller") 34 + } 35 + 36 + // Connect to the foreign node (retries a bit — UI may still be starting). 37 + case wait_for_ui(ui_node, 40) { 38 + True -> { 39 + io.println("connected to " <> ui_node) 40 + drive(ui_node) 41 + } 42 + False -> { 43 + io.println("failed to reach " <> ui_node) 44 + io.println("start the UI first: just ui (or nix develop -c just ui)") 45 + Nil 46 + } 47 + } 48 + } 49 + 50 + fn wait_for_ui(ui_node: String, attempts: Int) -> Bool { 51 + case attempts { 52 + 0 -> False 53 + n -> 54 + case dist.ping(dist.atom(ui_node)) { 55 + dist.Pong -> True 56 + dist.Pang -> { 57 + // ~250ms sleep via receive timeout 58 + let _ = dist.receive_any(250) 59 + wait_for_ui(ui_node, n - 1) 60 + } 61 + } 62 + } 63 + } 64 + 65 + fn drive(ui_node: String) -> Nil { 66 + // Handshake + initial view state. Gleam is the source of truth for count. 67 + cast(ui_node, dist.hello()) 68 + cast(ui_node, dist.set_title("gleamtk · Gleam-controlled")) 69 + cast(ui_node, dist.set_label("Hello from Gleam")) 70 + cast(ui_node, dist.set_count(0)) 71 + cast(ui_node, dist.ping_msg()) 72 + 73 + io.println("sent hello / set_title / set_label / set_count / ping") 74 + io.println("listening for UI events (click the window buttons)…") 75 + io.println("") 76 + 77 + loop(ui_node, 0) 78 + } 79 + 80 + fn cast(ui_node: String, msg: dist.Term) -> Nil { 81 + dist.cast(gui_name, ui_node, msg) 82 + } 83 + 84 + fn loop(ui_node: String, count: Int) -> Nil { 85 + case dist.receive_any(60_000) { 86 + Error(Nil) -> { 87 + io.println("(timeout waiting for UI events — still alive)") 88 + // Keep the node up so the UI can still talk to us. 89 + loop(ui_node, count) 90 + } 91 + Ok(msg) -> { 92 + let text = string.inspect(msg) 93 + io.println("← event: " <> text) 94 + handle_event(ui_node, count, text) 95 + } 96 + } 97 + } 98 + 99 + /// Pattern-match via inspected form for the POC (opaque Term from FFI). 100 + fn handle_event(ui_node: String, count: Int, text: String) -> Nil { 101 + case text { 102 + // `{clicked, N}` from the UI after a button press. 103 + t -> 104 + case string.contains(t, "clicked") { 105 + True -> { 106 + let next = count + 1 107 + io.println("→ Gleam sets count to " <> int.to_string(next)) 108 + cast(ui_node, dist.set_count(next)) 109 + cast( 110 + ui_node, 111 + dist.set_label("Gleam handled click #" <> int.to_string(next)), 112 + ) 113 + loop(ui_node, next) 114 + } 115 + False -> 116 + case string.contains(t, "window_closed") { 117 + True -> { 118 + io.println("window closed — controller exiting") 119 + Nil 120 + } 121 + False -> 122 + case string.contains(t, "ready") { 123 + True -> { 124 + io.println("(ui ready)") 125 + loop(ui_node, count) 126 + } 127 + False -> 128 + case string.contains(t, "pong") { 129 + True -> { 130 + io.println("(pong)") 131 + loop(ui_node, count) 132 + } 133 + False -> loop(ui_node, count) 134 + } 135 + } 136 + } 137 + } 138 + } 139 + }
+71
gleam/src/gleamtk/dist.gleam
··· 1 + //// Erlang distribution FFI used by the gleamtk controller. 2 + 3 + pub type NodeName 4 + 5 + pub type Pid 6 + 7 + pub type Term 8 + 9 + /// `pong` if the node is reachable, `pang` otherwise. 10 + pub type PingResult { 11 + Pong 12 + Pang 13 + } 14 + 15 + @external(erlang, "gleamtk_ffi", "atom") 16 + pub fn atom(name: String) -> NodeName 17 + 18 + @external(erlang, "gleamtk_ffi", "ping") 19 + pub fn ping(node: NodeName) -> PingResult 20 + 21 + @external(erlang, "gleamtk_ffi", "send_to") 22 + pub fn send_to(name: NodeName, node: NodeName, msg: Term) -> Nil 23 + // Note: Erlang side returns `nil` atom for Gleam Nil. 24 + 25 + @external(erlang, "gleamtk_ffi", "register_name") 26 + pub fn register_name(name: NodeName) -> Result(Nil, Nil) 27 + 28 + @external(erlang, "gleamtk_ffi", "self_pid") 29 + pub fn self_pid() -> Pid 30 + 31 + @external(erlang, "gleamtk_ffi", "node_name") 32 + pub fn node_name() -> NodeName 33 + 34 + @external(erlang, "gleamtk_ffi", "nodes") 35 + pub fn nodes() -> List(NodeName) 36 + 37 + @external(erlang, "gleamtk_ffi", "receive_any") 38 + pub fn receive_any(timeout_ms: Int) -> Result(Term, Nil) 39 + 40 + @external(erlang, "gleamtk_ffi", "tuple2_atom_binary") 41 + pub fn cmd_binary(tag: String, value: String) -> Term 42 + 43 + @external(erlang, "gleamtk_ffi", "tuple2_atom_int") 44 + pub fn cmd_int(tag: String, value: Int) -> Term 45 + 46 + @external(erlang, "gleamtk_ffi", "atom") 47 + pub fn atom_term(name: String) -> Term 48 + 49 + /// Send a term to a registered name on a remote node. 50 + pub fn cast(registered: String, node: String, msg: Term) -> Nil { 51 + send_to(atom(registered), atom(node), msg) 52 + } 53 + 54 + @external(erlang, "gleamtk_ffi", "hello_with_self") 55 + pub fn hello() -> Term 56 + 57 + pub fn set_title(title: String) -> Term { 58 + cmd_binary("set_title", title) 59 + } 60 + 61 + pub fn set_label(label: String) -> Term { 62 + cmd_binary("set_label", label) 63 + } 64 + 65 + pub fn set_count(n: Int) -> Term { 66 + cmd_int("set_count", n) 67 + } 68 + 69 + pub fn ping_msg() -> Term { 70 + atom_term("ping") 71 + }
+63
gleam/src/gleamtk_ffi.erl
··· 1 + %% Minimal distribution helpers for the gleamtk POC. 2 + -module(gleamtk_ffi). 3 + -export([ 4 + ping/1, 5 + send_to/3, 6 + register_name/1, 7 + self_pid/0, 8 + node_name/0, 9 + nodes/0, 10 + receive_any/1, 11 + atom/1, 12 + tuple2_atom_binary/2, 13 + tuple2_atom_int/2, 14 + hello_with_self/0 15 + ]). 16 + 17 + %% Gleam PingResult constructors → atoms pong | pang 18 + ping(Node) when is_atom(Node) -> 19 + net_adm:ping(Node). 20 + 21 + %% Gleam Nil → atom nil 22 + send_to(Name, Node, Msg) when is_atom(Name), is_atom(Node) -> 23 + {Name, Node} ! Msg, 24 + nil. 25 + 26 + %% Gleam Result(Nil, Nil) → {ok, nil} | {error, nil} 27 + register_name(Name) when is_atom(Name) -> 28 + case catch erlang:register(Name, self()) of 29 + true -> 30 + {ok, nil}; 31 + {'EXIT', _} -> 32 + case erlang:whereis(Name) of 33 + Pid when Pid =:= self() -> {ok, nil}; 34 + _ -> {error, nil} 35 + end 36 + end. 37 + 38 + self_pid() -> self(). 39 + 40 + node_name() -> node(). 41 + 42 + nodes() -> nodes(connected). 43 + 44 + %% Gleam Result(Term, Nil) → {ok, Msg} | {error, nil} 45 + receive_any(TimeoutMs) when is_integer(TimeoutMs), TimeoutMs >= 0 -> 46 + receive 47 + Msg -> {ok, Msg} 48 + after TimeoutMs -> 49 + {error, nil} 50 + end. 51 + 52 + atom(Bin) when is_binary(Bin) -> 53 + binary_to_atom(Bin, utf8). 54 + 55 + tuple2_atom_binary(AtomBin, Str) when is_binary(AtomBin), is_binary(Str) -> 56 + {binary_to_atom(AtomBin, utf8), Str}. 57 + 58 + tuple2_atom_int(AtomBin, N) when is_binary(AtomBin), is_integer(N) -> 59 + {binary_to_atom(AtomBin, utf8), N}. 60 + 61 + %% `{hello, self()}` so the foreign node can SEND replies to our pid. 62 + hello_with_self() -> 63 + {hello, self()}.
+86
justfile
··· 1 + # gleamtk POC — Relm4 foreign node + Gleam controller 2 + 3 + cookie := "gleamtk" 4 + ui_node := "ui@127.0.0.1" 5 + gleam_node := "gleamtk@127.0.0.1" 6 + 7 + # Build the Relm4 foreign node 8 + build-ui: 9 + cd ui && cargo build 10 + 11 + # Build the Gleam controller 12 + build-gleam: 13 + cd gleam && gleam build 14 + 15 + # Start epmd if needed, then the UI foreign node 16 + ui: build-ui 17 + #!/usr/bin/env bash 18 + set -euo pipefail 19 + if ! pgrep -x epmd >/dev/null 2>&1; then 20 + echo "starting epmd…" 21 + epmd -daemon 22 + sleep 0.3 23 + fi 24 + # Prefer nix-shell GTK schema paths when present 25 + if command -v pkg-config >/dev/null 2>&1; then 26 + _gtk="$(pkg-config --variable=prefix gtk4 2>/dev/null || true)" 27 + if [[ -n "${_gtk}" ]]; then 28 + export GSETTINGS_SCHEMAS_PATH="${_gtk}/share/gsettings-schemas/$(basename "$(echo ${_gtk}/share/gsettings-schemas/*gtk* 2>/dev/null | awk '{print $1}')"):${GSETTINGS_SCHEMAS_PATH:-}" 29 + export XDG_DATA_DIRS="${_gtk}/share:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" 30 + fi 31 + fi 32 + exec ./ui/target/debug/gleamtk-ui \ 33 + --local {{ui_node}} \ 34 + --cookie {{cookie}} \ 35 + --register gui 36 + 37 + # Start the Gleam controller (connects to the UI node) 38 + gleam: build-gleam 39 + #!/usr/bin/env bash 40 + set -euo pipefail 41 + if ! pgrep -x epmd >/dev/null 2>&1; then 42 + epmd -daemon 43 + sleep 0.3 44 + fi 45 + cd gleam 46 + export ERL_FLAGS="-name {{gleam_node}} -setcookie {{cookie}} -kernel inet_dist_use_interface {127,0,0,1}" 47 + exec gleam run -- {{ui_node}} 48 + 49 + # Full demo: UI in background, then controller in foreground 50 + run: 51 + #!/usr/bin/env bash 52 + set -euo pipefail 53 + if ! pgrep -x epmd >/dev/null 2>&1; then 54 + epmd -daemon 55 + sleep 0.3 56 + fi 57 + just build-ui 58 + just build-gleam 59 + ./ui/target/debug/gleamtk-ui --local {{ui_node}} --cookie {{cookie}} & 60 + UI_PID=$! 61 + trap 'kill $UI_PID 2>/dev/null || true' EXIT 62 + sleep 1 63 + cd gleam 64 + export ERL_FLAGS="-name {{gleam_node}} -setcookie {{cookie}} -kernel inet_dist_use_interface {127,0,0,1}" 65 + gleam run -- {{ui_node}} 66 + 67 + # Typecheck / compile only 68 + check: build-ui build-gleam 69 + 70 + # Headless distribution smoke (escript ↔ foreign node) 71 + smoke: build-ui 72 + #!/usr/bin/env bash 73 + set -euo pipefail 74 + if ! pgrep -x epmd >/dev/null 2>&1; then 75 + epmd -daemon 76 + sleep 0.3 77 + fi 78 + if pidof gleamtk-ui >/dev/null 2>&1; then 79 + kill "$(pidof gleamtk-ui)" || true 80 + sleep 0.3 81 + fi 82 + ./ui/target/debug/gleamtk-ui --local {{ui_node}} --cookie {{cookie}} >/tmp/gleamtk-ui.log 2>&1 & 83 + UI_PID=$! 84 + trap 'kill $UI_PID 2>/dev/null || true' EXIT 85 + sleep 1.5 86 + escript scripts/smoke_dist.escript
+32
scripts/smoke_dist.escript
··· 1 + #!/usr/bin/env escript 2 + %%! -name smoke@127.0.0.1 -setcookie gleamtk 3 + -mode(compile). 4 + 5 + main([]) -> 6 + true = register(controller, self()), 7 + io:format("registered controller pid=~p~n", [self()]), 8 + case net_adm:ping('ui@127.0.0.1') of 9 + pong -> 10 + io:format("ping=pong~n"), 11 + {gui, 'ui@127.0.0.1'} ! {hello, self()}, 12 + {gui, 'ui@127.0.0.1'} ! {set_title, <<"from smoke">>}, 13 + {gui, 'ui@127.0.0.1'} ! {set_label, <<"smoke label">>}, 14 + {gui, 'ui@127.0.0.1'} ! {set_count, 7}, 15 + {gui, 'ui@127.0.0.1'} ! ping, 16 + drain(8), 17 + halt(0); 18 + pang -> 19 + io:format("ping=pang~n"), 20 + halt(1) 21 + end. 22 + 23 + drain(0) -> 24 + io:format("drain done~n"); 25 + drain(N) -> 26 + receive 27 + M -> 28 + io:format("got ~p~n", [M]), 29 + drain(N - 1) 30 + after 2000 -> 31 + io:format("drain timeout (~B left)~n", [N]) 32 + end.
+1536
ui/Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "adler32" 7 + version = "1.2.0" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" 10 + 11 + [[package]] 12 + name = "allocator-api2" 13 + version = "0.2.21" 14 + source = "registry+https://github.com/rust-lang/crates.io-index" 15 + checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 16 + 17 + [[package]] 18 + name = "anstream" 19 + version = "1.0.0" 20 + source = "registry+https://github.com/rust-lang/crates.io-index" 21 + checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" 22 + dependencies = [ 23 + "anstyle", 24 + "anstyle-parse", 25 + "anstyle-query", 26 + "anstyle-wincon", 27 + "colorchoice", 28 + "is_terminal_polyfill", 29 + "utf8parse", 30 + ] 31 + 32 + [[package]] 33 + name = "anstyle" 34 + version = "1.0.14" 35 + source = "registry+https://github.com/rust-lang/crates.io-index" 36 + checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" 37 + 38 + [[package]] 39 + name = "anstyle-parse" 40 + version = "1.0.0" 41 + source = "registry+https://github.com/rust-lang/crates.io-index" 42 + checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" 43 + dependencies = [ 44 + "utf8parse", 45 + ] 46 + 47 + [[package]] 48 + name = "anstyle-query" 49 + version = "1.1.5" 50 + source = "registry+https://github.com/rust-lang/crates.io-index" 51 + checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" 52 + dependencies = [ 53 + "windows-sys", 54 + ] 55 + 56 + [[package]] 57 + name = "anstyle-wincon" 58 + version = "3.0.11" 59 + source = "registry+https://github.com/rust-lang/crates.io-index" 60 + checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" 61 + dependencies = [ 62 + "anstyle", 63 + "once_cell_polyfill", 64 + "windows-sys", 65 + ] 66 + 67 + [[package]] 68 + name = "async-channel" 69 + version = "2.5.0" 70 + source = "registry+https://github.com/rust-lang/crates.io-index" 71 + checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" 72 + dependencies = [ 73 + "concurrent-queue", 74 + "event-listener-strategy", 75 + "futures-core", 76 + "pin-project-lite", 77 + ] 78 + 79 + [[package]] 80 + name = "async-executor" 81 + version = "1.14.0" 82 + source = "registry+https://github.com/rust-lang/crates.io-index" 83 + checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" 84 + dependencies = [ 85 + "async-task", 86 + "concurrent-queue", 87 + "fastrand", 88 + "futures-lite", 89 + "pin-project-lite", 90 + "slab", 91 + ] 92 + 93 + [[package]] 94 + name = "async-fs" 95 + version = "2.2.0" 96 + source = "registry+https://github.com/rust-lang/crates.io-index" 97 + checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" 98 + dependencies = [ 99 + "async-lock", 100 + "blocking", 101 + "futures-lite", 102 + ] 103 + 104 + [[package]] 105 + name = "async-io" 106 + version = "2.6.0" 107 + source = "registry+https://github.com/rust-lang/crates.io-index" 108 + checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" 109 + dependencies = [ 110 + "autocfg", 111 + "cfg-if", 112 + "concurrent-queue", 113 + "futures-io", 114 + "futures-lite", 115 + "parking", 116 + "polling", 117 + "rustix", 118 + "slab", 119 + "windows-sys", 120 + ] 121 + 122 + [[package]] 123 + name = "async-lock" 124 + version = "3.4.2" 125 + source = "registry+https://github.com/rust-lang/crates.io-index" 126 + checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" 127 + dependencies = [ 128 + "event-listener", 129 + "event-listener-strategy", 130 + "pin-project-lite", 131 + ] 132 + 133 + [[package]] 134 + name = "async-net" 135 + version = "2.0.0" 136 + source = "registry+https://github.com/rust-lang/crates.io-index" 137 + checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" 138 + dependencies = [ 139 + "async-io", 140 + "blocking", 141 + "futures-lite", 142 + ] 143 + 144 + [[package]] 145 + name = "async-process" 146 + version = "2.5.0" 147 + source = "registry+https://github.com/rust-lang/crates.io-index" 148 + checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" 149 + dependencies = [ 150 + "async-channel", 151 + "async-io", 152 + "async-lock", 153 + "async-signal", 154 + "async-task", 155 + "blocking", 156 + "cfg-if", 157 + "event-listener", 158 + "futures-lite", 159 + "rustix", 160 + ] 161 + 162 + [[package]] 163 + name = "async-signal" 164 + version = "0.2.14" 165 + source = "registry+https://github.com/rust-lang/crates.io-index" 166 + checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" 167 + dependencies = [ 168 + "async-io", 169 + "async-lock", 170 + "atomic-waker", 171 + "cfg-if", 172 + "futures-core", 173 + "futures-io", 174 + "rustix", 175 + "signal-hook-registry", 176 + "slab", 177 + "windows-sys", 178 + ] 179 + 180 + [[package]] 181 + name = "async-task" 182 + version = "4.7.1" 183 + source = "registry+https://github.com/rust-lang/crates.io-index" 184 + checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" 185 + 186 + [[package]] 187 + name = "atomic-waker" 188 + version = "1.1.2" 189 + source = "registry+https://github.com/rust-lang/crates.io-index" 190 + checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 191 + 192 + [[package]] 193 + name = "autocfg" 194 + version = "1.5.1" 195 + source = "registry+https://github.com/rust-lang/crates.io-index" 196 + checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" 197 + 198 + [[package]] 199 + name = "bitflags" 200 + version = "2.13.1" 201 + source = "registry+https://github.com/rust-lang/crates.io-index" 202 + checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" 203 + 204 + [[package]] 205 + name = "blocking" 206 + version = "1.6.2" 207 + source = "registry+https://github.com/rust-lang/crates.io-index" 208 + checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" 209 + dependencies = [ 210 + "async-channel", 211 + "async-task", 212 + "futures-io", 213 + "futures-lite", 214 + "piper", 215 + ] 216 + 217 + [[package]] 218 + name = "bumpalo" 219 + version = "3.20.3" 220 + source = "registry+https://github.com/rust-lang/crates.io-index" 221 + checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" 222 + 223 + [[package]] 224 + name = "cairo-rs" 225 + version = "0.22.0" 226 + source = "registry+https://github.com/rust-lang/crates.io-index" 227 + checksum = "5cc8d9aa793480744cd9a0524fef1a2e197d9eaa0f739cde19d16aba530dcb95" 228 + dependencies = [ 229 + "bitflags", 230 + "cairo-sys-rs", 231 + "glib", 232 + "libc", 233 + ] 234 + 235 + [[package]] 236 + name = "cairo-sys-rs" 237 + version = "0.22.0" 238 + source = "registry+https://github.com/rust-lang/crates.io-index" 239 + checksum = "f8b4985713047f5faee02b8db6a6ef32bbb50269ff53c1aee716d1d195b76d54" 240 + dependencies = [ 241 + "glib-sys", 242 + "libc", 243 + "system-deps", 244 + ] 245 + 246 + [[package]] 247 + name = "cfg-expr" 248 + version = "0.20.8" 249 + source = "registry+https://github.com/rust-lang/crates.io-index" 250 + checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c" 251 + dependencies = [ 252 + "smallvec", 253 + "target-lexicon", 254 + ] 255 + 256 + [[package]] 257 + name = "cfg-if" 258 + version = "1.0.4" 259 + source = "registry+https://github.com/rust-lang/crates.io-index" 260 + checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 261 + 262 + [[package]] 263 + name = "chacha20" 264 + version = "0.10.1" 265 + source = "registry+https://github.com/rust-lang/crates.io-index" 266 + checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" 267 + dependencies = [ 268 + "cfg-if", 269 + "cpufeatures", 270 + "rand_core", 271 + ] 272 + 273 + [[package]] 274 + name = "clap" 275 + version = "4.6.4" 276 + source = "registry+https://github.com/rust-lang/crates.io-index" 277 + checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7" 278 + dependencies = [ 279 + "clap_builder", 280 + "clap_derive", 281 + ] 282 + 283 + [[package]] 284 + name = "clap_builder" 285 + version = "4.6.2" 286 + source = "registry+https://github.com/rust-lang/crates.io-index" 287 + checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" 288 + dependencies = [ 289 + "anstream", 290 + "anstyle", 291 + "clap_lex", 292 + "strsim", 293 + ] 294 + 295 + [[package]] 296 + name = "clap_derive" 297 + version = "4.6.4" 298 + source = "registry+https://github.com/rust-lang/crates.io-index" 299 + checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" 300 + dependencies = [ 301 + "heck", 302 + "proc-macro2", 303 + "quote", 304 + "syn 3.0.3", 305 + ] 306 + 307 + [[package]] 308 + name = "clap_lex" 309 + version = "1.1.0" 310 + source = "registry+https://github.com/rust-lang/crates.io-index" 311 + checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" 312 + 313 + [[package]] 314 + name = "colorchoice" 315 + version = "1.0.5" 316 + source = "registry+https://github.com/rust-lang/crates.io-index" 317 + checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" 318 + 319 + [[package]] 320 + name = "concurrent-queue" 321 + version = "2.5.0" 322 + source = "registry+https://github.com/rust-lang/crates.io-index" 323 + checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 324 + dependencies = [ 325 + "crossbeam-utils", 326 + ] 327 + 328 + [[package]] 329 + name = "cpufeatures" 330 + version = "0.3.0" 331 + source = "registry+https://github.com/rust-lang/crates.io-index" 332 + checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" 333 + dependencies = [ 334 + "libc", 335 + ] 336 + 337 + [[package]] 338 + name = "crc32fast" 339 + version = "1.5.0" 340 + source = "registry+https://github.com/rust-lang/crates.io-index" 341 + checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" 342 + dependencies = [ 343 + "cfg-if", 344 + ] 345 + 346 + [[package]] 347 + name = "crossbeam-utils" 348 + version = "0.8.22" 349 + source = "registry+https://github.com/rust-lang/crates.io-index" 350 + checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" 351 + 352 + [[package]] 353 + name = "dary_heap" 354 + version = "0.3.9" 355 + source = "registry+https://github.com/rust-lang/crates.io-index" 356 + checksum = "8b1e3a325bc115f096c8b77bbf027a7c2592230e70be2d985be950d3d5e60ebe" 357 + 358 + [[package]] 359 + name = "eetf" 360 + version = "0.11.0" 361 + source = "registry+https://github.com/rust-lang/crates.io-index" 362 + checksum = "b59d5a438de6099b4fd27f69d61a67c3b7758e3950fa6b1cef7d679d1e74a7d0" 363 + dependencies = [ 364 + "libflate", 365 + "num-bigint", 366 + "num-traits", 367 + ] 368 + 369 + [[package]] 370 + name = "equivalent" 371 + version = "1.0.2" 372 + source = "registry+https://github.com/rust-lang/crates.io-index" 373 + checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 374 + 375 + [[package]] 376 + name = "erl_dist" 377 + version = "0.8.0" 378 + source = "registry+https://github.com/rust-lang/crates.io-index" 379 + checksum = "61525240382fc8a099fe85ff7ed1aa1a806addf265fe1254867ab3a689d14015" 380 + dependencies = [ 381 + "eetf", 382 + "futures", 383 + "md5", 384 + "rand", 385 + ] 386 + 387 + [[package]] 388 + name = "errno" 389 + version = "0.3.14" 390 + source = "registry+https://github.com/rust-lang/crates.io-index" 391 + checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 392 + dependencies = [ 393 + "libc", 394 + "windows-sys", 395 + ] 396 + 397 + [[package]] 398 + name = "event-listener" 399 + version = "5.4.1" 400 + source = "registry+https://github.com/rust-lang/crates.io-index" 401 + checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" 402 + dependencies = [ 403 + "concurrent-queue", 404 + "parking", 405 + "pin-project-lite", 406 + ] 407 + 408 + [[package]] 409 + name = "event-listener-strategy" 410 + version = "0.5.4" 411 + source = "registry+https://github.com/rust-lang/crates.io-index" 412 + checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" 413 + dependencies = [ 414 + "event-listener", 415 + "pin-project-lite", 416 + ] 417 + 418 + [[package]] 419 + name = "fastrand" 420 + version = "2.5.0" 421 + source = "registry+https://github.com/rust-lang/crates.io-index" 422 + checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" 423 + dependencies = [ 424 + "getrandom", 425 + ] 426 + 427 + [[package]] 428 + name = "field-offset" 429 + version = "0.3.6" 430 + source = "registry+https://github.com/rust-lang/crates.io-index" 431 + checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" 432 + dependencies = [ 433 + "memoffset", 434 + "rustc_version", 435 + ] 436 + 437 + [[package]] 438 + name = "flume" 439 + version = "0.12.0" 440 + source = "registry+https://github.com/rust-lang/crates.io-index" 441 + checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" 442 + dependencies = [ 443 + "fastrand", 444 + "futures-core", 445 + "futures-sink", 446 + "spin", 447 + ] 448 + 449 + [[package]] 450 + name = "foldhash" 451 + version = "0.2.0" 452 + source = "registry+https://github.com/rust-lang/crates.io-index" 453 + checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" 454 + 455 + [[package]] 456 + name = "fragile" 457 + version = "2.1.0" 458 + source = "registry+https://github.com/rust-lang/crates.io-index" 459 + checksum = "8878864ba14bb86e818a412bfd6f18f9eabd4ec0f008a28e8f7eb61db532fcf9" 460 + dependencies = [ 461 + "futures-core", 462 + ] 463 + 464 + [[package]] 465 + name = "futures" 466 + version = "0.3.33" 467 + source = "registry+https://github.com/rust-lang/crates.io-index" 468 + checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" 469 + dependencies = [ 470 + "futures-channel", 471 + "futures-core", 472 + "futures-executor", 473 + "futures-io", 474 + "futures-sink", 475 + "futures-task", 476 + "futures-util", 477 + ] 478 + 479 + [[package]] 480 + name = "futures-channel" 481 + version = "0.3.33" 482 + source = "registry+https://github.com/rust-lang/crates.io-index" 483 + checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" 484 + dependencies = [ 485 + "futures-core", 486 + "futures-sink", 487 + ] 488 + 489 + [[package]] 490 + name = "futures-core" 491 + version = "0.3.33" 492 + source = "registry+https://github.com/rust-lang/crates.io-index" 493 + checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" 494 + 495 + [[package]] 496 + name = "futures-executor" 497 + version = "0.3.33" 498 + source = "registry+https://github.com/rust-lang/crates.io-index" 499 + checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" 500 + dependencies = [ 501 + "futures-core", 502 + "futures-task", 503 + "futures-util", 504 + ] 505 + 506 + [[package]] 507 + name = "futures-io" 508 + version = "0.3.33" 509 + source = "registry+https://github.com/rust-lang/crates.io-index" 510 + checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" 511 + 512 + [[package]] 513 + name = "futures-lite" 514 + version = "2.6.1" 515 + source = "registry+https://github.com/rust-lang/crates.io-index" 516 + checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" 517 + dependencies = [ 518 + "fastrand", 519 + "futures-core", 520 + "futures-io", 521 + "parking", 522 + "pin-project-lite", 523 + ] 524 + 525 + [[package]] 526 + name = "futures-macro" 527 + version = "0.3.33" 528 + source = "registry+https://github.com/rust-lang/crates.io-index" 529 + checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" 530 + dependencies = [ 531 + "proc-macro2", 532 + "quote", 533 + "syn 2.0.119", 534 + ] 535 + 536 + [[package]] 537 + name = "futures-sink" 538 + version = "0.3.33" 539 + source = "registry+https://github.com/rust-lang/crates.io-index" 540 + checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" 541 + 542 + [[package]] 543 + name = "futures-task" 544 + version = "0.3.33" 545 + source = "registry+https://github.com/rust-lang/crates.io-index" 546 + checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" 547 + 548 + [[package]] 549 + name = "futures-util" 550 + version = "0.3.33" 551 + source = "registry+https://github.com/rust-lang/crates.io-index" 552 + checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" 553 + dependencies = [ 554 + "futures-channel", 555 + "futures-core", 556 + "futures-io", 557 + "futures-macro", 558 + "futures-sink", 559 + "futures-task", 560 + "memchr", 561 + "pin-project-lite", 562 + "slab", 563 + ] 564 + 565 + [[package]] 566 + name = "gdk-pixbuf" 567 + version = "0.22.0" 568 + source = "registry+https://github.com/rust-lang/crates.io-index" 569 + checksum = "25f420376dbee041b2db374ce4573892a36222bb3f6c0c43e24f0d67eae9b646" 570 + dependencies = [ 571 + "gdk-pixbuf-sys", 572 + "gio", 573 + "glib", 574 + "libc", 575 + ] 576 + 577 + [[package]] 578 + name = "gdk-pixbuf-sys" 579 + version = "0.22.0" 580 + source = "registry+https://github.com/rust-lang/crates.io-index" 581 + checksum = "48f31b37b1fc4b48b54f6b91b7ef04c18e00b4585d98359dd7b998774bbd91fb" 582 + dependencies = [ 583 + "gio-sys", 584 + "glib-sys", 585 + "gobject-sys", 586 + "libc", 587 + "system-deps", 588 + ] 589 + 590 + [[package]] 591 + name = "gdk4" 592 + version = "0.11.4" 593 + source = "registry+https://github.com/rust-lang/crates.io-index" 594 + checksum = "d81e2a6c6ecba2aab60633a98df1868b03fa0bfdce8105edc27c1bccf71f0e39" 595 + dependencies = [ 596 + "cairo-rs", 597 + "gdk-pixbuf", 598 + "gdk4-sys", 599 + "gio", 600 + "glib", 601 + "libc", 602 + "pango", 603 + ] 604 + 605 + [[package]] 606 + name = "gdk4-sys" 607 + version = "0.11.4" 608 + source = "registry+https://github.com/rust-lang/crates.io-index" 609 + checksum = "3d8f608d8d7d229975c4d0d026f5d3071598c4ddab3c5262b0a31840fec78d13" 610 + dependencies = [ 611 + "cairo-sys-rs", 612 + "gdk-pixbuf-sys", 613 + "gio-sys", 614 + "glib-sys", 615 + "gobject-sys", 616 + "libc", 617 + "pango-sys", 618 + "pkg-config", 619 + "system-deps", 620 + ] 621 + 622 + [[package]] 623 + name = "getrandom" 624 + version = "0.4.3" 625 + source = "registry+https://github.com/rust-lang/crates.io-index" 626 + checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" 627 + dependencies = [ 628 + "cfg-if", 629 + "js-sys", 630 + "libc", 631 + "r-efi", 632 + "rand_core", 633 + "wasm-bindgen", 634 + ] 635 + 636 + [[package]] 637 + name = "gio" 638 + version = "0.22.8" 639 + source = "registry+https://github.com/rust-lang/crates.io-index" 640 + checksum = "8b3e1f669909c326b9413bde5a742097b8c90a7d78f45326db13668984769ded" 641 + dependencies = [ 642 + "futures-channel", 643 + "futures-core", 644 + "futures-io", 645 + "futures-util", 646 + "gio-sys", 647 + "glib", 648 + "libc", 649 + "pin-project-lite", 650 + "smallvec", 651 + ] 652 + 653 + [[package]] 654 + name = "gio-sys" 655 + version = "0.22.8" 656 + source = "registry+https://github.com/rust-lang/crates.io-index" 657 + checksum = "353fdc7da7cd16da916104b1e0e4e7de380ec9c8aaa20d4d742d66310ab4b0d5" 658 + dependencies = [ 659 + "glib-sys", 660 + "gobject-sys", 661 + "libc", 662 + "system-deps", 663 + "windows-sys", 664 + ] 665 + 666 + [[package]] 667 + name = "gleamtk-ui" 668 + version = "0.1.0" 669 + dependencies = [ 670 + "async-channel", 671 + "clap", 672 + "eetf", 673 + "erl_dist", 674 + "futures", 675 + "gtk4", 676 + "libadwaita", 677 + "relm4", 678 + "smol", 679 + ] 680 + 681 + [[package]] 682 + name = "glib" 683 + version = "0.22.8" 684 + source = "registry+https://github.com/rust-lang/crates.io-index" 685 + checksum = "ddbcf514bd1881fc1b960e4e52b4e82873f4da3bceddbd58d42827b508888100" 686 + dependencies = [ 687 + "bitflags", 688 + "futures-channel", 689 + "futures-core", 690 + "futures-executor", 691 + "futures-task", 692 + "futures-util", 693 + "gio-sys", 694 + "glib-macros", 695 + "glib-sys", 696 + "gobject-sys", 697 + "libc", 698 + "memchr", 699 + "smallvec", 700 + ] 701 + 702 + [[package]] 703 + name = "glib-macros" 704 + version = "0.22.6" 705 + source = "registry+https://github.com/rust-lang/crates.io-index" 706 + checksum = "506d23499707c7142898429757e8d9a3871d965239a2cb66dfa05052be6d6f19" 707 + dependencies = [ 708 + "heck", 709 + "proc-macro2", 710 + "quote", 711 + "syn 2.0.119", 712 + ] 713 + 714 + [[package]] 715 + name = "glib-sys" 716 + version = "0.22.8" 717 + source = "registry+https://github.com/rust-lang/crates.io-index" 718 + checksum = "030967459f9f676851872c6304adea7825c6d462ec9b72554c733cf0c5952233" 719 + dependencies = [ 720 + "libc", 721 + "system-deps", 722 + ] 723 + 724 + [[package]] 725 + name = "gobject-sys" 726 + version = "0.22.6" 727 + source = "registry+https://github.com/rust-lang/crates.io-index" 728 + checksum = "22a861859b887a79cf461359c192c97a57d8fb0229dd291232e57aa11f6fa72c" 729 + dependencies = [ 730 + "glib-sys", 731 + "libc", 732 + "system-deps", 733 + ] 734 + 735 + [[package]] 736 + name = "graphene-rs" 737 + version = "0.22.8" 738 + source = "registry+https://github.com/rust-lang/crates.io-index" 739 + checksum = "eb856b9c558971c3f13ab692358926da710b046932a4e087aedcc35b040d7dff" 740 + dependencies = [ 741 + "glib", 742 + "graphene-sys", 743 + ] 744 + 745 + [[package]] 746 + name = "graphene-sys" 747 + version = "0.22.8" 748 + source = "registry+https://github.com/rust-lang/crates.io-index" 749 + checksum = "5c7ffdfde88f3570d3705e0d8a2433e036d387a1f2930bbf47eafcb5f569fd04" 750 + dependencies = [ 751 + "glib-sys", 752 + "libc", 753 + "system-deps", 754 + ] 755 + 756 + [[package]] 757 + name = "gsk4" 758 + version = "0.11.4" 759 + source = "registry+https://github.com/rust-lang/crates.io-index" 760 + checksum = "b867be1c5f14dcb8f552c0eff6e9a9b1da5f8b43943e8efc3a63c889d84952ff" 761 + dependencies = [ 762 + "cairo-rs", 763 + "gdk4", 764 + "glib", 765 + "graphene-rs", 766 + "gsk4-sys", 767 + "libc", 768 + "pango", 769 + ] 770 + 771 + [[package]] 772 + name = "gsk4-sys" 773 + version = "0.11.4" 774 + source = "registry+https://github.com/rust-lang/crates.io-index" 775 + checksum = "5b7c7eb2e681ee896646cfb8872b431f24d09f53ba9283289d9b10caa6707088" 776 + dependencies = [ 777 + "cairo-sys-rs", 778 + "gdk4-sys", 779 + "glib-sys", 780 + "gobject-sys", 781 + "graphene-sys", 782 + "libc", 783 + "pango-sys", 784 + "system-deps", 785 + ] 786 + 787 + [[package]] 788 + name = "gtk4" 789 + version = "0.11.4" 790 + source = "registry+https://github.com/rust-lang/crates.io-index" 791 + checksum = "98a0a0466484f64b07b5b8184d43fa46be78eb0b8e04ae4e179af31d770b76d9" 792 + dependencies = [ 793 + "cairo-rs", 794 + "field-offset", 795 + "futures-channel", 796 + "gdk-pixbuf", 797 + "gdk4", 798 + "gio", 799 + "glib", 800 + "graphene-rs", 801 + "gsk4", 802 + "gtk4-macros", 803 + "gtk4-sys", 804 + "libc", 805 + "pango", 806 + ] 807 + 808 + [[package]] 809 + name = "gtk4-macros" 810 + version = "0.11.4" 811 + source = "registry+https://github.com/rust-lang/crates.io-index" 812 + checksum = "5ac7179400a36a04de039c24206bb841c5596992b907b43b23ee8d5bdc40d00e" 813 + dependencies = [ 814 + "proc-macro-crate", 815 + "proc-macro2", 816 + "quote", 817 + "syn 2.0.119", 818 + ] 819 + 820 + [[package]] 821 + name = "gtk4-sys" 822 + version = "0.11.4" 823 + source = "registry+https://github.com/rust-lang/crates.io-index" 824 + checksum = "82b8f954786af0b1984425c4446b77f5ff6594346181316be3f850caab1c6f01" 825 + dependencies = [ 826 + "cairo-sys-rs", 827 + "gdk-pixbuf-sys", 828 + "gdk4-sys", 829 + "gio-sys", 830 + "glib-sys", 831 + "gobject-sys", 832 + "graphene-sys", 833 + "gsk4-sys", 834 + "libc", 835 + "pango-sys", 836 + "system-deps", 837 + ] 838 + 839 + [[package]] 840 + name = "hashbrown" 841 + version = "0.16.1" 842 + source = "registry+https://github.com/rust-lang/crates.io-index" 843 + checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" 844 + dependencies = [ 845 + "allocator-api2", 846 + "equivalent", 847 + "foldhash", 848 + ] 849 + 850 + [[package]] 851 + name = "hashbrown" 852 + version = "0.17.1" 853 + source = "registry+https://github.com/rust-lang/crates.io-index" 854 + checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" 855 + 856 + [[package]] 857 + name = "heck" 858 + version = "0.5.0" 859 + source = "registry+https://github.com/rust-lang/crates.io-index" 860 + checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 861 + 862 + [[package]] 863 + name = "hermit-abi" 864 + version = "0.5.2" 865 + source = "registry+https://github.com/rust-lang/crates.io-index" 866 + checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" 867 + 868 + [[package]] 869 + name = "indexmap" 870 + version = "2.14.0" 871 + source = "registry+https://github.com/rust-lang/crates.io-index" 872 + checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" 873 + dependencies = [ 874 + "equivalent", 875 + "hashbrown 0.17.1", 876 + ] 877 + 878 + [[package]] 879 + name = "is_terminal_polyfill" 880 + version = "1.70.2" 881 + source = "registry+https://github.com/rust-lang/crates.io-index" 882 + checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" 883 + 884 + [[package]] 885 + name = "js-sys" 886 + version = "0.3.103" 887 + source = "registry+https://github.com/rust-lang/crates.io-index" 888 + checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" 889 + dependencies = [ 890 + "cfg-if", 891 + "wasm-bindgen", 892 + ] 893 + 894 + [[package]] 895 + name = "libadwaita" 896 + version = "0.9.2" 897 + source = "registry+https://github.com/rust-lang/crates.io-index" 898 + checksum = "85b9900e67182a4b5b1f157b448d94f0715c8b9770cce21cf000801917f53bfa" 899 + dependencies = [ 900 + "gdk4", 901 + "gio", 902 + "glib", 903 + "gtk4", 904 + "libadwaita-sys", 905 + "pango", 906 + ] 907 + 908 + [[package]] 909 + name = "libadwaita-sys" 910 + version = "0.9.2" 911 + source = "registry+https://github.com/rust-lang/crates.io-index" 912 + checksum = "28d3c27642b389852aa99341bd4a4c19ec6f8a2b63ebdd7f5ba1952198079ccd" 913 + dependencies = [ 914 + "gdk4-sys", 915 + "gio-sys", 916 + "glib-sys", 917 + "gobject-sys", 918 + "gtk4-sys", 919 + "libc", 920 + "pango-sys", 921 + "system-deps", 922 + ] 923 + 924 + [[package]] 925 + name = "libc" 926 + version = "0.2.189" 927 + source = "registry+https://github.com/rust-lang/crates.io-index" 928 + checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" 929 + 930 + [[package]] 931 + name = "libflate" 932 + version = "2.3.1" 933 + source = "registry+https://github.com/rust-lang/crates.io-index" 934 + checksum = "a4da9b700e758e57152a1fd1c52cbdc5727c1aa6d8743dc1acda917398f1d76c" 935 + dependencies = [ 936 + "adler32", 937 + "crc32fast", 938 + "dary_heap", 939 + "libflate_lz77", 940 + "no_std_io2", 941 + ] 942 + 943 + [[package]] 944 + name = "libflate_lz77" 945 + version = "2.3.0" 946 + source = "registry+https://github.com/rust-lang/crates.io-index" 947 + checksum = "ff7a10e427698aef6eef269482776debfef63384d30f13aad39a1a95e0e098fd" 948 + dependencies = [ 949 + "hashbrown 0.16.1", 950 + "no_std_io2", 951 + "rle-decode-fast", 952 + ] 953 + 954 + [[package]] 955 + name = "linux-raw-sys" 956 + version = "0.12.1" 957 + source = "registry+https://github.com/rust-lang/crates.io-index" 958 + checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" 959 + 960 + [[package]] 961 + name = "lock_api" 962 + version = "0.4.14" 963 + source = "registry+https://github.com/rust-lang/crates.io-index" 964 + checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" 965 + dependencies = [ 966 + "scopeguard", 967 + ] 968 + 969 + [[package]] 970 + name = "md5" 971 + version = "0.8.1" 972 + source = "registry+https://github.com/rust-lang/crates.io-index" 973 + checksum = "7ebb8d8732c6a6df3d8f032a82911cfc747e00efb95cc46e8d0acd5b5b88570c" 974 + 975 + [[package]] 976 + name = "memchr" 977 + version = "2.8.3" 978 + source = "registry+https://github.com/rust-lang/crates.io-index" 979 + checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" 980 + 981 + [[package]] 982 + name = "memoffset" 983 + version = "0.9.1" 984 + source = "registry+https://github.com/rust-lang/crates.io-index" 985 + checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" 986 + dependencies = [ 987 + "autocfg", 988 + ] 989 + 990 + [[package]] 991 + name = "no_std_io2" 992 + version = "0.9.4" 993 + source = "registry+https://github.com/rust-lang/crates.io-index" 994 + checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" 995 + dependencies = [ 996 + "memchr", 997 + ] 998 + 999 + [[package]] 1000 + name = "num-bigint" 1001 + version = "0.4.8" 1002 + source = "registry+https://github.com/rust-lang/crates.io-index" 1003 + checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" 1004 + dependencies = [ 1005 + "num-integer", 1006 + "num-traits", 1007 + ] 1008 + 1009 + [[package]] 1010 + name = "num-integer" 1011 + version = "0.1.46" 1012 + source = "registry+https://github.com/rust-lang/crates.io-index" 1013 + checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 1014 + dependencies = [ 1015 + "num-traits", 1016 + ] 1017 + 1018 + [[package]] 1019 + name = "num-traits" 1020 + version = "0.2.19" 1021 + source = "registry+https://github.com/rust-lang/crates.io-index" 1022 + checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1023 + dependencies = [ 1024 + "autocfg", 1025 + ] 1026 + 1027 + [[package]] 1028 + name = "once_cell" 1029 + version = "1.21.4" 1030 + source = "registry+https://github.com/rust-lang/crates.io-index" 1031 + checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" 1032 + 1033 + [[package]] 1034 + name = "once_cell_polyfill" 1035 + version = "1.70.2" 1036 + source = "registry+https://github.com/rust-lang/crates.io-index" 1037 + checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" 1038 + 1039 + [[package]] 1040 + name = "pango" 1041 + version = "0.22.8" 1042 + source = "registry+https://github.com/rust-lang/crates.io-index" 1043 + checksum = "5d800d8d0de2ad5d0fb046f5344dbaba14a003cf3dd27cc21d85893d35ea316c" 1044 + dependencies = [ 1045 + "gio", 1046 + "glib", 1047 + "pango-sys", 1048 + ] 1049 + 1050 + [[package]] 1051 + name = "pango-sys" 1052 + version = "0.22.0" 1053 + source = "registry+https://github.com/rust-lang/crates.io-index" 1054 + checksum = "bbd111a20ca90fedf03e09c59783c679c00900f1d8491cca5399f5e33609d5d6" 1055 + dependencies = [ 1056 + "glib-sys", 1057 + "gobject-sys", 1058 + "libc", 1059 + "system-deps", 1060 + ] 1061 + 1062 + [[package]] 1063 + name = "parking" 1064 + version = "2.2.1" 1065 + source = "registry+https://github.com/rust-lang/crates.io-index" 1066 + checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 1067 + 1068 + [[package]] 1069 + name = "pin-project-lite" 1070 + version = "0.2.17" 1071 + source = "registry+https://github.com/rust-lang/crates.io-index" 1072 + checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" 1073 + 1074 + [[package]] 1075 + name = "piper" 1076 + version = "0.2.5" 1077 + source = "registry+https://github.com/rust-lang/crates.io-index" 1078 + checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" 1079 + dependencies = [ 1080 + "atomic-waker", 1081 + "fastrand", 1082 + "futures-io", 1083 + ] 1084 + 1085 + [[package]] 1086 + name = "pkg-config" 1087 + version = "0.3.33" 1088 + source = "registry+https://github.com/rust-lang/crates.io-index" 1089 + checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" 1090 + 1091 + [[package]] 1092 + name = "polling" 1093 + version = "3.11.0" 1094 + source = "registry+https://github.com/rust-lang/crates.io-index" 1095 + checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" 1096 + dependencies = [ 1097 + "cfg-if", 1098 + "concurrent-queue", 1099 + "hermit-abi", 1100 + "pin-project-lite", 1101 + "rustix", 1102 + "windows-sys", 1103 + ] 1104 + 1105 + [[package]] 1106 + name = "proc-macro-crate" 1107 + version = "3.5.0" 1108 + source = "registry+https://github.com/rust-lang/crates.io-index" 1109 + checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" 1110 + dependencies = [ 1111 + "toml_edit", 1112 + ] 1113 + 1114 + [[package]] 1115 + name = "proc-macro2" 1116 + version = "1.0.107" 1117 + source = "registry+https://github.com/rust-lang/crates.io-index" 1118 + checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" 1119 + dependencies = [ 1120 + "unicode-ident", 1121 + ] 1122 + 1123 + [[package]] 1124 + name = "quote" 1125 + version = "1.0.47" 1126 + source = "registry+https://github.com/rust-lang/crates.io-index" 1127 + checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" 1128 + dependencies = [ 1129 + "proc-macro2", 1130 + ] 1131 + 1132 + [[package]] 1133 + name = "r-efi" 1134 + version = "6.0.0" 1135 + source = "registry+https://github.com/rust-lang/crates.io-index" 1136 + checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" 1137 + 1138 + [[package]] 1139 + name = "rand" 1140 + version = "0.10.2" 1141 + source = "registry+https://github.com/rust-lang/crates.io-index" 1142 + checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" 1143 + dependencies = [ 1144 + "chacha20", 1145 + "getrandom", 1146 + "rand_core", 1147 + ] 1148 + 1149 + [[package]] 1150 + name = "rand_core" 1151 + version = "0.10.1" 1152 + source = "registry+https://github.com/rust-lang/crates.io-index" 1153 + checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" 1154 + 1155 + [[package]] 1156 + name = "relm4" 1157 + version = "0.11.0" 1158 + source = "registry+https://github.com/rust-lang/crates.io-index" 1159 + checksum = "6420f090f0545e9ec9656469d139a4e1b66ff9c30b808fe2247892724f71a198" 1160 + dependencies = [ 1161 + "flume", 1162 + "fragile", 1163 + "futures", 1164 + "gtk4", 1165 + "libadwaita", 1166 + "once_cell", 1167 + "relm4-css", 1168 + "relm4-macros", 1169 + "tokio", 1170 + "tracing", 1171 + ] 1172 + 1173 + [[package]] 1174 + name = "relm4-css" 1175 + version = "0.11.0" 1176 + source = "registry+https://github.com/rust-lang/crates.io-index" 1177 + checksum = "f3b81d263f784b103c815afa29124486b59741eca069ce7a5999efb14f13c368" 1178 + 1179 + [[package]] 1180 + name = "relm4-macros" 1181 + version = "0.11.0" 1182 + source = "registry+https://github.com/rust-lang/crates.io-index" 1183 + checksum = "36c9dbf50a60c82375e66b61d522c936b187a11b25c0a42e91c516326ad24a4f" 1184 + dependencies = [ 1185 + "proc-macro2", 1186 + "quote", 1187 + "syn 2.0.119", 1188 + ] 1189 + 1190 + [[package]] 1191 + name = "rle-decode-fast" 1192 + version = "1.0.3" 1193 + source = "registry+https://github.com/rust-lang/crates.io-index" 1194 + checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" 1195 + 1196 + [[package]] 1197 + name = "rustc_version" 1198 + version = "0.4.1" 1199 + source = "registry+https://github.com/rust-lang/crates.io-index" 1200 + checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 1201 + dependencies = [ 1202 + "semver", 1203 + ] 1204 + 1205 + [[package]] 1206 + name = "rustix" 1207 + version = "1.1.4" 1208 + source = "registry+https://github.com/rust-lang/crates.io-index" 1209 + checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" 1210 + dependencies = [ 1211 + "bitflags", 1212 + "errno", 1213 + "libc", 1214 + "linux-raw-sys", 1215 + "windows-sys", 1216 + ] 1217 + 1218 + [[package]] 1219 + name = "rustversion" 1220 + version = "1.0.23" 1221 + source = "registry+https://github.com/rust-lang/crates.io-index" 1222 + checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" 1223 + 1224 + [[package]] 1225 + name = "scopeguard" 1226 + version = "1.2.0" 1227 + source = "registry+https://github.com/rust-lang/crates.io-index" 1228 + checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1229 + 1230 + [[package]] 1231 + name = "semver" 1232 + version = "1.0.28" 1233 + source = "registry+https://github.com/rust-lang/crates.io-index" 1234 + checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" 1235 + 1236 + [[package]] 1237 + name = "serde_core" 1238 + version = "1.0.229" 1239 + source = "registry+https://github.com/rust-lang/crates.io-index" 1240 + checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" 1241 + dependencies = [ 1242 + "serde_derive", 1243 + ] 1244 + 1245 + [[package]] 1246 + name = "serde_derive" 1247 + version = "1.0.229" 1248 + source = "registry+https://github.com/rust-lang/crates.io-index" 1249 + checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" 1250 + dependencies = [ 1251 + "proc-macro2", 1252 + "quote", 1253 + "syn 3.0.3", 1254 + ] 1255 + 1256 + [[package]] 1257 + name = "serde_spanned" 1258 + version = "1.1.1" 1259 + source = "registry+https://github.com/rust-lang/crates.io-index" 1260 + checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" 1261 + dependencies = [ 1262 + "serde_core", 1263 + ] 1264 + 1265 + [[package]] 1266 + name = "signal-hook-registry" 1267 + version = "1.4.8" 1268 + source = "registry+https://github.com/rust-lang/crates.io-index" 1269 + checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" 1270 + dependencies = [ 1271 + "errno", 1272 + "libc", 1273 + ] 1274 + 1275 + [[package]] 1276 + name = "slab" 1277 + version = "0.4.12" 1278 + source = "registry+https://github.com/rust-lang/crates.io-index" 1279 + checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" 1280 + 1281 + [[package]] 1282 + name = "smallvec" 1283 + version = "1.15.2" 1284 + source = "registry+https://github.com/rust-lang/crates.io-index" 1285 + checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" 1286 + 1287 + [[package]] 1288 + name = "smol" 1289 + version = "2.0.2" 1290 + source = "registry+https://github.com/rust-lang/crates.io-index" 1291 + checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" 1292 + dependencies = [ 1293 + "async-channel", 1294 + "async-executor", 1295 + "async-fs", 1296 + "async-io", 1297 + "async-lock", 1298 + "async-net", 1299 + "async-process", 1300 + "blocking", 1301 + "futures-lite", 1302 + ] 1303 + 1304 + [[package]] 1305 + name = "spin" 1306 + version = "0.9.9" 1307 + source = "registry+https://github.com/rust-lang/crates.io-index" 1308 + checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" 1309 + dependencies = [ 1310 + "lock_api", 1311 + ] 1312 + 1313 + [[package]] 1314 + name = "strsim" 1315 + version = "0.11.1" 1316 + source = "registry+https://github.com/rust-lang/crates.io-index" 1317 + checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1318 + 1319 + [[package]] 1320 + name = "syn" 1321 + version = "2.0.119" 1322 + source = "registry+https://github.com/rust-lang/crates.io-index" 1323 + checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" 1324 + dependencies = [ 1325 + "proc-macro2", 1326 + "quote", 1327 + "unicode-ident", 1328 + ] 1329 + 1330 + [[package]] 1331 + name = "syn" 1332 + version = "3.0.3" 1333 + source = "registry+https://github.com/rust-lang/crates.io-index" 1334 + checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" 1335 + dependencies = [ 1336 + "proc-macro2", 1337 + "quote", 1338 + "unicode-ident", 1339 + ] 1340 + 1341 + [[package]] 1342 + name = "system-deps" 1343 + version = "7.0.8" 1344 + source = "registry+https://github.com/rust-lang/crates.io-index" 1345 + checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" 1346 + dependencies = [ 1347 + "cfg-expr", 1348 + "heck", 1349 + "pkg-config", 1350 + "toml", 1351 + "version-compare", 1352 + ] 1353 + 1354 + [[package]] 1355 + name = "target-lexicon" 1356 + version = "0.13.5" 1357 + source = "registry+https://github.com/rust-lang/crates.io-index" 1358 + checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" 1359 + 1360 + [[package]] 1361 + name = "tokio" 1362 + version = "1.53.1" 1363 + source = "registry+https://github.com/rust-lang/crates.io-index" 1364 + checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" 1365 + dependencies = [ 1366 + "pin-project-lite", 1367 + ] 1368 + 1369 + [[package]] 1370 + name = "toml" 1371 + version = "1.1.3+spec-1.1.0" 1372 + source = "registry+https://github.com/rust-lang/crates.io-index" 1373 + checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" 1374 + dependencies = [ 1375 + "indexmap", 1376 + "serde_core", 1377 + "serde_spanned", 1378 + "toml_datetime", 1379 + "toml_parser", 1380 + "toml_writer", 1381 + "winnow", 1382 + ] 1383 + 1384 + [[package]] 1385 + name = "toml_datetime" 1386 + version = "1.1.1+spec-1.1.0" 1387 + source = "registry+https://github.com/rust-lang/crates.io-index" 1388 + checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" 1389 + dependencies = [ 1390 + "serde_core", 1391 + ] 1392 + 1393 + [[package]] 1394 + name = "toml_edit" 1395 + version = "0.25.13+spec-1.1.0" 1396 + source = "registry+https://github.com/rust-lang/crates.io-index" 1397 + checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" 1398 + dependencies = [ 1399 + "indexmap", 1400 + "toml_datetime", 1401 + "toml_parser", 1402 + "winnow", 1403 + ] 1404 + 1405 + [[package]] 1406 + name = "toml_parser" 1407 + version = "1.1.2+spec-1.1.0" 1408 + source = "registry+https://github.com/rust-lang/crates.io-index" 1409 + checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" 1410 + dependencies = [ 1411 + "winnow", 1412 + ] 1413 + 1414 + [[package]] 1415 + name = "toml_writer" 1416 + version = "1.1.2+spec-1.1.0" 1417 + source = "registry+https://github.com/rust-lang/crates.io-index" 1418 + checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" 1419 + 1420 + [[package]] 1421 + name = "tracing" 1422 + version = "0.1.44" 1423 + source = "registry+https://github.com/rust-lang/crates.io-index" 1424 + checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" 1425 + dependencies = [ 1426 + "pin-project-lite", 1427 + "tracing-attributes", 1428 + "tracing-core", 1429 + ] 1430 + 1431 + [[package]] 1432 + name = "tracing-attributes" 1433 + version = "0.1.31" 1434 + source = "registry+https://github.com/rust-lang/crates.io-index" 1435 + checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" 1436 + dependencies = [ 1437 + "proc-macro2", 1438 + "quote", 1439 + "syn 2.0.119", 1440 + ] 1441 + 1442 + [[package]] 1443 + name = "tracing-core" 1444 + version = "0.1.36" 1445 + source = "registry+https://github.com/rust-lang/crates.io-index" 1446 + checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" 1447 + dependencies = [ 1448 + "once_cell", 1449 + ] 1450 + 1451 + [[package]] 1452 + name = "unicode-ident" 1453 + version = "1.0.24" 1454 + source = "registry+https://github.com/rust-lang/crates.io-index" 1455 + checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" 1456 + 1457 + [[package]] 1458 + name = "utf8parse" 1459 + version = "0.2.2" 1460 + source = "registry+https://github.com/rust-lang/crates.io-index" 1461 + checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1462 + 1463 + [[package]] 1464 + name = "version-compare" 1465 + version = "0.2.1" 1466 + source = "registry+https://github.com/rust-lang/crates.io-index" 1467 + checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" 1468 + 1469 + [[package]] 1470 + name = "wasm-bindgen" 1471 + version = "0.2.126" 1472 + source = "registry+https://github.com/rust-lang/crates.io-index" 1473 + checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" 1474 + dependencies = [ 1475 + "cfg-if", 1476 + "once_cell", 1477 + "rustversion", 1478 + "wasm-bindgen-macro", 1479 + "wasm-bindgen-shared", 1480 + ] 1481 + 1482 + [[package]] 1483 + name = "wasm-bindgen-macro" 1484 + version = "0.2.126" 1485 + source = "registry+https://github.com/rust-lang/crates.io-index" 1486 + checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" 1487 + dependencies = [ 1488 + "quote", 1489 + "wasm-bindgen-macro-support", 1490 + ] 1491 + 1492 + [[package]] 1493 + name = "wasm-bindgen-macro-support" 1494 + version = "0.2.126" 1495 + source = "registry+https://github.com/rust-lang/crates.io-index" 1496 + checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" 1497 + dependencies = [ 1498 + "bumpalo", 1499 + "proc-macro2", 1500 + "quote", 1501 + "syn 2.0.119", 1502 + "wasm-bindgen-shared", 1503 + ] 1504 + 1505 + [[package]] 1506 + name = "wasm-bindgen-shared" 1507 + version = "0.2.126" 1508 + source = "registry+https://github.com/rust-lang/crates.io-index" 1509 + checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" 1510 + dependencies = [ 1511 + "unicode-ident", 1512 + ] 1513 + 1514 + [[package]] 1515 + name = "windows-link" 1516 + version = "0.2.1" 1517 + source = "registry+https://github.com/rust-lang/crates.io-index" 1518 + checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 1519 + 1520 + [[package]] 1521 + name = "windows-sys" 1522 + version = "0.61.2" 1523 + source = "registry+https://github.com/rust-lang/crates.io-index" 1524 + checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 1525 + dependencies = [ 1526 + "windows-link", 1527 + ] 1528 + 1529 + [[package]] 1530 + name = "winnow" 1531 + version = "1.0.4" 1532 + source = "registry+https://github.com/rust-lang/crates.io-index" 1533 + checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" 1534 + dependencies = [ 1535 + "memchr", 1536 + ]
+17
ui/Cargo.toml
··· 1 + [package] 2 + name = "gleamtk-ui" 3 + version = "0.1.0" 4 + edition = "2021" 5 + description = "Relm4 foreign node controlled by Gleam over Erlang distribution" 6 + license = "MIT" 7 + 8 + [dependencies] 9 + relm4 = { version = "0.11", features = ["libadwaita"] } 10 + gtk4 = { version = "0.11", features = ["v4_10"] } 11 + libadwaita = { version = "0.9", features = ["v1_5"] } 12 + erl_dist = "0.8" 13 + eetf = "0.11" 14 + futures = "0.3" 15 + smol = "2" 16 + async-channel = "2" 17 + clap = { version = "4", features = ["derive"] }
+175
ui/src/app.rs
··· 1 + //! Relm4 + libadwaita counter window, driven by distribution messages. 2 + 3 + use relm4::gtk; 4 + use relm4::gtk::glib; 5 + use relm4::gtk::prelude::*; 6 + use relm4::prelude::*; 7 + use relm4::{ComponentParts, ComponentSender, RelmWidgetExt, SimpleComponent}; 8 + 9 + // libadwaita widget traits (AdwApplicationWindowExt, etc.) 10 + use relm4::adw::prelude::*; 11 + 12 + use crate::dist::Outbound; 13 + use crate::OUTBOUND; 14 + 15 + #[derive(Debug)] 16 + pub enum AppMsg { 17 + /// Controller handshake / first message received. 18 + Connected, 19 + SetTitle(String), 20 + SetLabel(String), 21 + SetCount(i64), 22 + /// UI-local: increment and notify controller. 23 + Increment, 24 + Decrement, 25 + /// Dist asked for pong. 26 + PongRequested, 27 + DistDied(String), 28 + Quit, 29 + } 30 + 31 + pub struct AppModel { 32 + title: String, 33 + label: String, 34 + count: i64, 35 + status: String, 36 + } 37 + 38 + #[relm4::component(pub)] 39 + impl SimpleComponent for AppModel { 40 + type Init = (); 41 + type Input = AppMsg; 42 + type Output = (); 43 + 44 + view! { 45 + #[root] 46 + adw::ApplicationWindow { 47 + #[watch] 48 + set_title: Some(&model.title), 49 + set_default_width: 420, 50 + set_default_height: 280, 51 + 52 + connect_close_request[sender] => move |_| { 53 + emit_outbound(Outbound::WindowClosed); 54 + sender.input(AppMsg::Quit); 55 + glib::Propagation::Proceed 56 + }, 57 + 58 + #[wrap(Some)] 59 + set_content = &adw::ToolbarView { 60 + add_top_bar = &adw::HeaderBar {}, 61 + 62 + #[wrap(Some)] 63 + set_content = &gtk::Box { 64 + set_orientation: gtk::Orientation::Vertical, 65 + set_spacing: 16, 66 + set_margin_all: 24, 67 + set_halign: gtk::Align::Center, 68 + set_valign: gtk::Align::Center, 69 + 70 + gtk::Label { 71 + #[watch] 72 + set_label: &model.label, 73 + add_css_class: "title-1", 74 + }, 75 + 76 + gtk::Label { 77 + #[watch] 78 + set_label: &format!("Count: {}", model.count), 79 + add_css_class: "title-2", 80 + }, 81 + 82 + gtk::Box { 83 + set_orientation: gtk::Orientation::Horizontal, 84 + set_spacing: 12, 85 + set_halign: gtk::Align::Center, 86 + 87 + gtk::Button { 88 + set_label: "−", 89 + add_css_class: "circular", 90 + connect_clicked => AppMsg::Decrement, 91 + }, 92 + 93 + gtk::Button { 94 + set_label: "Click me", 95 + add_css_class: "suggested-action", 96 + add_css_class: "pill", 97 + connect_clicked => AppMsg::Increment, 98 + }, 99 + 100 + gtk::Button { 101 + set_label: "+", 102 + add_css_class: "circular", 103 + connect_clicked => AppMsg::Increment, 104 + }, 105 + }, 106 + 107 + gtk::Label { 108 + #[watch] 109 + set_label: &model.status, 110 + add_css_class: "dim-label", 111 + }, 112 + }, 113 + }, 114 + } 115 + } 116 + 117 + fn init( 118 + _init: Self::Init, 119 + root: Self::Root, 120 + sender: ComponentSender<Self>, 121 + ) -> ComponentParts<Self> { 122 + let _ = &sender; 123 + let model = AppModel { 124 + title: "gleamtk".into(), 125 + label: "Waiting for Gleam controller…".into(), 126 + count: 0, 127 + status: "foreign node up · registered as gui".into(), 128 + }; 129 + 130 + let widgets = view_output!(); 131 + ComponentParts { model, widgets } 132 + } 133 + 134 + fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) { 135 + match msg { 136 + AppMsg::Connected => { 137 + self.status = "controller connected".into(); 138 + } 139 + AppMsg::SetTitle(t) => { 140 + self.title = t; 141 + } 142 + AppMsg::SetLabel(l) => { 143 + self.label = l; 144 + } 145 + AppMsg::SetCount(n) => { 146 + self.count = n; 147 + } 148 + AppMsg::Increment => { 149 + self.count += 1; 150 + emit_outbound(Outbound::Clicked(self.count)); 151 + } 152 + AppMsg::Decrement => { 153 + self.count -= 1; 154 + emit_outbound(Outbound::CountChanged(self.count)); 155 + } 156 + AppMsg::PongRequested => { 157 + emit_outbound(Outbound::Pong); 158 + } 159 + AppMsg::DistDied(err) => { 160 + self.status = format!("dist error: {err}"); 161 + } 162 + AppMsg::Quit => { 163 + relm4::main_application().quit(); 164 + } 165 + } 166 + } 167 + } 168 + 169 + fn emit_outbound(msg: Outbound) { 170 + if let Some(tx) = OUTBOUND.get() { 171 + if let Err(e) = tx.try_send(msg) { 172 + eprintln!("[app] outbound send failed: {e}"); 173 + } 174 + } 175 + }
+314
ui/src/dist.rs
··· 1 + //! Erlang distribution server: epmd registration + handshake + message loop. 2 + 3 + use std::error::Error; 4 + use std::sync::Arc; 5 + 6 + use erl_dist::epmd::{EpmdClient, NodeEntry, DEFAULT_EPMD_PORT}; 7 + use erl_dist::handshake::{HandshakeStatus, ServerSideHandshake}; 8 + use erl_dist::message::{self, Message}; 9 + use erl_dist::node::{Creation, LocalNode, NodeName}; 10 + use erl_dist::term::{Atom, Pid}; 11 + use erl_dist::DistributionFlags; 12 + use futures::future::{self, Either}; 13 + use futures::StreamExt; 14 + use smol::net::{TcpListener, TcpStream}; 15 + 16 + use crate::protocol::{ 17 + parse_inbound, reply_pid_from_term, term_clicked, term_count_changed, term_pong, term_ready, 18 + term_window_closed, ControllerDest, Inbound, 19 + }; 20 + use crate::BROKER; 21 + 22 + #[derive(Debug, Clone)] 23 + pub struct DistConfig { 24 + pub local_node: NodeName, 25 + pub cookie: String, 26 + pub register_as: String, 27 + pub published: bool, 28 + } 29 + 30 + /// Events produced by the Relm4 UI for the dist thread to encode & send. 31 + #[derive(Debug, Clone)] 32 + pub enum Outbound { 33 + Clicked(i64), 34 + CountChanged(i64), 35 + Pong, 36 + WindowClosed, 37 + } 38 + 39 + pub fn run( 40 + cfg: DistConfig, 41 + outbound: async_channel::Receiver<Outbound>, 42 + ) -> Result<(), Box<dyn Error + Send + Sync>> { 43 + smol::block_on(async move { run_async(cfg, outbound).await }) 44 + } 45 + 46 + async fn run_async( 47 + cfg: DistConfig, 48 + outbound: async_channel::Receiver<Outbound>, 49 + ) -> Result<(), Box<dyn Error + Send + Sync>> { 50 + let listener = TcpListener::bind("0.0.0.0:0").await?; 51 + let listening_port = listener.local_addr()?.port(); 52 + println!( 53 + "[dist] listening on port {listening_port} as {}", 54 + cfg.local_node 55 + ); 56 + 57 + let entry = if cfg.published { 58 + NodeEntry::new(cfg.local_node.name(), listening_port) 59 + } else { 60 + NodeEntry::new_hidden(cfg.local_node.name(), listening_port) 61 + }; 62 + 63 + let epmd_addr = (cfg.local_node.host(), DEFAULT_EPMD_PORT); 64 + let stream = TcpStream::connect(epmd_addr).await.map_err(|e| { 65 + format!( 66 + "cannot connect to epmd at {}:{} — is epmd running? ({e})", 67 + cfg.local_node.host(), 68 + DEFAULT_EPMD_PORT 69 + ) 70 + })?; 71 + let epmd = EpmdClient::new(stream); 72 + let (keepalive, creation) = epmd.register(entry).await?; 73 + println!("[dist] registered with epmd (creation={creation:?})"); 74 + println!( 75 + "[dist] Gleam should send: {{gui, '{}'}} ! {{hello, self()}}", 76 + cfg.local_node 77 + ); 78 + 79 + let (peer_out_tx, peer_out_rx) = async_channel::unbounded::<Outbound>(); 80 + smol::spawn(async move { 81 + while let Ok(ev) = outbound.recv().await { 82 + let _ = peer_out_tx.send(ev).await; 83 + } 84 + }) 85 + .detach(); 86 + 87 + let peer_out_rx = Arc::new(peer_out_rx); 88 + 89 + let mut incoming = listener.incoming(); 90 + while let Some(stream) = incoming.next().await.transpose()? { 91 + let mut local = LocalNode::new(cfg.local_node.clone(), creation); 92 + if cfg.published { 93 + local.flags |= DistributionFlags::PUBLISHED; 94 + } 95 + let cookie = cfg.cookie.clone(); 96 + let register_as = cfg.register_as.clone(); 97 + let node_display = cfg.local_node.to_string(); 98 + let peer_out_rx = Arc::clone(&peer_out_rx); 99 + 100 + smol::spawn(async move { 101 + if let Err(e) = 102 + handle_peer(local, cookie, register_as, stream, node_display, peer_out_rx).await 103 + { 104 + eprintln!("[dist] peer session ended: {e}"); 105 + } 106 + }) 107 + .detach(); 108 + } 109 + 110 + drop(keepalive); 111 + Ok(()) 112 + } 113 + 114 + async fn handle_peer( 115 + local_node: LocalNode, 116 + cookie: String, 117 + register_as: String, 118 + stream: TcpStream, 119 + node_display: String, 120 + outbound: Arc<async_channel::Receiver<Outbound>>, 121 + ) -> Result<(), Box<dyn Error + Send + Sync>> { 122 + let mut handshake = ServerSideHandshake::new(stream, local_node.clone(), &cookie); 123 + let status = if handshake.execute_recv_name().await?.is_some() { 124 + HandshakeStatus::Ok 125 + } else { 126 + HandshakeStatus::Named { 127 + name: "generated_name".to_owned(), 128 + creation: Creation::random(), 129 + } 130 + }; 131 + let (stream, peer_node) = handshake.execute_rest(status).await?; 132 + println!("[dist] connected peer: {:?}", peer_node.name); 133 + 134 + let (mut tx, mut rx) = message::channel(stream, local_node.flags & peer_node.flags); 135 + let local_pid = Pid::new( 136 + local_node.name.to_string(), 137 + 0, 138 + 0, 139 + local_node.creation.get(), 140 + ); 141 + 142 + let mut controller: Option<ControllerDest> = None; 143 + let mut pending_ready = Some(term_ready(&node_display)); 144 + 145 + let mut tick = smol::Timer::after(std::time::Duration::from_secs(15)); 146 + 147 + loop { 148 + let msg_fut = rx.recv(); 149 + let out_fut = outbound.recv(); 150 + let tick_fut = &mut tick; 151 + 152 + futures::pin_mut!(msg_fut); 153 + futures::pin_mut!(out_fut); 154 + 155 + let race = future::select(msg_fut, future::select(out_fut, tick_fut)).await; 156 + 157 + match race { 158 + Either::Left((msg_result, _)) => { 159 + let msg = msg_result?; 160 + match msg { 161 + Message::RegSend(reg) => { 162 + // BEAM system traffic (net_kernel / rex / global_*) must not become controller. 163 + if reg.to_name.name != register_as { 164 + handle_system_regsend(&mut tx, &reg).await?; 165 + continue; 166 + } 167 + 168 + controller = Some( 169 + reply_pid_from_term(&reg.message) 170 + .map(ControllerDest::Pid) 171 + .unwrap_or_else(|| ControllerDest::Pid(reg.from_pid.clone())), 172 + ); 173 + if let Inbound::Hello { 174 + reply_as: Some(name), 175 + } = parse_inbound(&reg.message) 176 + { 177 + controller = Some(ControllerDest::Name(name)); 178 + } 179 + apply_inbound(parse_inbound(&reg.message)); 180 + 181 + if let Some(ready) = pending_ready.take() { 182 + if let Some(dest) = controller.as_ref() { 183 + eprintln!("[dist] sending ready → {dest:?}"); 184 + send_controller(&mut tx, &local_pid, dest, ready).await?; 185 + } 186 + } 187 + } 188 + Message::Send(send) => { 189 + // Only honor explicit app SEND if we somehow get one. 190 + if let Some(pid) = reply_pid_from_term(&send.message) { 191 + controller = Some(ControllerDest::Pid(pid)); 192 + } 193 + apply_inbound(parse_inbound(&send.message)); 194 + } 195 + Message::Tick => {} 196 + other => { 197 + eprintln!("[dist] ignore: {other:?}"); 198 + } 199 + } 200 + } 201 + Either::Right((inner, _)) => match inner { 202 + Either::Left((out_result, _)) => match out_result { 203 + Ok(ev) => { 204 + let term = match ev { 205 + Outbound::Clicked(n) => term_clicked(n), 206 + Outbound::CountChanged(n) => term_count_changed(n), 207 + Outbound::Pong => term_pong(), 208 + Outbound::WindowClosed => term_window_closed(), 209 + }; 210 + if let Some(dest) = controller.as_ref() { 211 + eprintln!("[dist] outbound {ev:?} → {dest:?}"); 212 + send_controller(&mut tx, &local_pid, dest, term).await?; 213 + } else { 214 + eprintln!("[dist] drop outbound (no controller yet): {ev:?}"); 215 + } 216 + } 217 + Err(_) => {} 218 + }, 219 + Either::Right((_, _)) => { 220 + tx.send(Message::Tick).await?; 221 + tick.set_after(std::time::Duration::from_secs(15)); 222 + } 223 + }, 224 + } 225 + } 226 + } 227 + 228 + /// Reply to BEAM control messages that would otherwise hang `net_adm:ping` / global. 229 + async fn handle_system_regsend( 230 + tx: &mut message::Sender<TcpStream>, 231 + reg: &erl_dist::message::RegSend, 232 + ) -> Result<(), Box<dyn Error + Send + Sync>> { 233 + let name = reg.to_name.name.as_str(); 234 + match name { 235 + "net_kernel" => { 236 + if let Some((from, tag)) = parse_gen_call(&reg.message) { 237 + if is_is_auth(&reg.message) { 238 + // gen:reply(From, yes) ≈ FromPid ! {Tag, yes} 239 + let reply = eetf::Term::Tuple(eetf::Tuple { 240 + elements: vec![tag, eetf::Term::Atom(Atom::from("yes"))], 241 + }); 242 + eprintln!("[dist] is_auth → yes"); 243 + tx.send(Message::send(from, reply)).await?; 244 + } else { 245 + eprintln!("[dist] net_kernel call (ignored): {:?}", reg.message); 246 + } 247 + } else { 248 + eprintln!("[dist] net_kernel msg (ignored): {:?}", reg.message); 249 + } 250 + } 251 + "rex" | "global_name_server" | "global_group" | "timer_server" | "erpc_handler" => { 252 + // Not needed for the POC; ignore. 253 + } 254 + other => { 255 + eprintln!("[dist] system reg_send to '{other}' (ignored)"); 256 + } 257 + } 258 + Ok(()) 259 + } 260 + 261 + fn is_is_auth(term: &eetf::Term) -> bool { 262 + match term { 263 + eetf::Term::Tuple(t) => match t.elements.as_slice() { 264 + [eetf::Term::Atom(a), _, eetf::Term::Tuple(req)] if a.name == "$gen_call" => { 265 + matches!( 266 + req.elements.as_slice(), 267 + [eetf::Term::Atom(op), _] if op.name == "is_auth" 268 + ) 269 + } 270 + _ => false, 271 + }, 272 + _ => false, 273 + } 274 + } 275 + 276 + /// Parse `{'$gen_call', {FromPid, Tag}, Request}` → (FromPid, Tag). 277 + fn parse_gen_call(term: &eetf::Term) -> Option<(Pid, eetf::Term)> { 278 + let eetf::Term::Tuple(t) = term else { 279 + return None; 280 + }; 281 + let [eetf::Term::Atom(tag), eetf::Term::Tuple(from), _req] = t.elements.as_slice() else { 282 + return None; 283 + }; 284 + if tag.name != "$gen_call" { 285 + return None; 286 + } 287 + match from.elements.as_slice() { 288 + [eetf::Term::Pid(pid), tag_term] => Some((pid.clone(), tag_term.clone())), 289 + _ => None, 290 + } 291 + } 292 + 293 + fn apply_inbound(inbound: Inbound) { 294 + if let Some(app_msg) = inbound.to_app_msg() { 295 + eprintln!("[dist] → UI {app_msg:?}"); 296 + BROKER.send(app_msg); 297 + } 298 + } 299 + 300 + async fn send_controller( 301 + tx: &mut message::Sender<TcpStream>, 302 + local_pid: &Pid, 303 + controller: &ControllerDest, 304 + term: eetf::Term, 305 + ) -> Result<(), Box<dyn Error + Send + Sync>> { 306 + let msg = match controller { 307 + ControllerDest::Pid(pid) => Message::send(pid.clone(), term), 308 + ControllerDest::Name(name) => { 309 + Message::reg_send(local_pid.clone(), Atom::from(name.as_str()), term) 310 + } 311 + }; 312 + tx.send(msg).await?; 313 + Ok(()) 314 + }
+80
ui/src/main.rs
··· 1 + //! Relm4 foreign node (Erlang distribution peer) for the gleamtk POC. 2 + //! 3 + //! Registers with epmd, accepts BEAM connections, and maps distribution 4 + //! messages onto Relm4 `Input`s. UI events go back as Erlang terms. 5 + 6 + mod app; 7 + mod dist; 8 + mod protocol; 9 + 10 + use std::sync::OnceLock; 11 + 12 + use app::{AppModel, AppMsg}; 13 + use clap::Parser; 14 + use dist::{DistConfig, Outbound}; 15 + use relm4::prelude::*; 16 + use relm4::MessageBroker; 17 + 18 + /// Global broker so the distribution thread can inject Relm4 inputs. 19 + pub static BROKER: MessageBroker<AppMsg> = MessageBroker::new(); 20 + 21 + /// Outbound events from the UI thread → dist thread. 22 + pub static OUTBOUND: OnceLock<async_channel::Sender<Outbound>> = OnceLock::new(); 23 + 24 + #[derive(Parser, Debug)] 25 + #[command(name = "gleamtk-ui", about = "Relm4 foreign node for Gleam")] 26 + struct Args { 27 + /// Local node name, e.g. ui@127.0.0.1 28 + #[arg(long, default_value = "ui@127.0.0.1")] 29 + local: String, 30 + 31 + /// Erlang cookie (must match the Gleam/BEAM node) 32 + #[arg(long, default_value = "gleamtk")] 33 + cookie: String, 34 + 35 + /// Registered name Gleam should send to (`{gui, Node} ! Msg`) 36 + #[arg(long, default_value = "gui")] 37 + register: String, 38 + 39 + /// Publish as a visible node (vs hidden) 40 + #[arg(long, default_value_t = true)] 41 + published: bool, 42 + } 43 + 44 + fn main() { 45 + let args = Args::parse(); 46 + 47 + let (out_tx, out_rx) = async_channel::unbounded::<Outbound>(); 48 + OUTBOUND.set(out_tx).expect("OUTBOUND already set"); 49 + 50 + let cfg = DistConfig { 51 + local_node: args 52 + .local 53 + .parse() 54 + .unwrap_or_else(|e| panic!("bad --local node name: {e}")), 55 + cookie: args.cookie, 56 + register_as: args.register, 57 + published: args.published, 58 + }; 59 + 60 + // Distribution server on a dedicated OS thread (smol runtime). 61 + std::thread::Builder::new() 62 + .name("erl-dist".into()) 63 + .spawn(move || { 64 + if let Err(e) = dist::run(cfg, out_rx) { 65 + eprintln!("[dist] fatal: {e}"); 66 + BROKER.send(AppMsg::DistDied(e.to_string())); 67 + } 68 + }) 69 + .expect("spawn dist thread"); 70 + 71 + // Don't pass clap flags to GApplication (it treats unknown options as fatal). 72 + let prog = std::env::args() 73 + .next() 74 + .unwrap_or_else(|| "gleamtk-ui".into()); 75 + let app = RelmApp::new("org.gleamtk.poc") 76 + .with_broker(&BROKER) 77 + .with_args(vec![prog]); 78 + app.allow_multiple_instances(true); 79 + app.run::<AppModel>(()); 80 + }
+156
ui/src/protocol.rs
··· 1 + //! Shared term protocol between Gleam (controller) and Relm4 (view). 2 + //! 3 + //! ## Gleam → UI (`{gui, ui@…} ! Term`) 4 + //! - `hello` | `{hello, ReplyToName}` — handshake; UI remembers controller 5 + //! - `{set_title, Binary}` 6 + //! - `{set_label, Binary}` 7 + //! - `{set_count, Integer}` 8 + //! - `ping` 9 + //! 10 + //! ## UI → Gleam (`{controller, gleamtk@…} ! Term` or direct pid) 11 + //! - `{ready, <<"ui@…">>}` 12 + //! - `{clicked, Count}` 13 + //! - `{count_changed, Count}` (local +/− from UI) 14 + //! - `pong` 15 + //! - `{window_closed}` 16 + 17 + use eetf::{Atom, Binary, FixInteger, Term}; 18 + use erl_dist::term::Pid; 19 + 20 + use crate::app::AppMsg; 21 + 22 + /// Where to send events back to the Gleam controller. 23 + #[derive(Debug, Clone)] 24 + pub enum ControllerDest { 25 + Pid(Pid), 26 + /// Registered name on the peer BEAM node (e.g. `controller`). 27 + Name(String), 28 + } 29 + 30 + #[derive(Debug, Clone)] 31 + pub enum Inbound { 32 + Hello { reply_as: Option<String> }, 33 + SetTitle(String), 34 + SetLabel(String), 35 + SetCount(i64), 36 + Ping, 37 + Unknown(String), 38 + } 39 + 40 + pub fn parse_inbound(term: &Term) -> Inbound { 41 + match term { 42 + Term::Atom(a) if a.name == "hello" => Inbound::Hello { reply_as: None }, 43 + Term::Atom(a) if a.name == "ping" => Inbound::Ping, 44 + Term::Tuple(t) => match t.elements.as_slice() { 45 + // `{hello, controller}` — registered name for replies 46 + [Term::Atom(tag), Term::Atom(name)] if tag.name == "hello" => Inbound::Hello { 47 + reply_as: Some(name.name.clone()), 48 + }, 49 + // `{hello, Pid}` — direct pid for replies (preferred) 50 + [Term::Atom(tag), Term::Pid(_)] if tag.name == "hello" => Inbound::Hello { 51 + reply_as: None, 52 + }, 53 + [Term::Atom(tag), Term::Binary(b)] if tag.name == "set_title" => { 54 + Inbound::SetTitle(String::from_utf8_lossy(&b.bytes).into_owned()) 55 + } 56 + [Term::Atom(tag), Term::Binary(b)] if tag.name == "set_label" => { 57 + Inbound::SetLabel(String::from_utf8_lossy(&b.bytes).into_owned()) 58 + } 59 + // Gleam lists of ints sometimes show up as binaries; also accept lists. 60 + [Term::Atom(tag), Term::List(list)] if tag.name == "set_title" => { 61 + Inbound::SetTitle(list_to_string(list)) 62 + } 63 + [Term::Atom(tag), Term::List(list)] if tag.name == "set_label" => { 64 + Inbound::SetLabel(list_to_string(list)) 65 + } 66 + [Term::Atom(tag), Term::FixInteger(n)] if tag.name == "set_count" => { 67 + Inbound::SetCount(n.value as i64) 68 + } 69 + [Term::Atom(tag), Term::BigInteger(n)] if tag.name == "set_count" => { 70 + Inbound::SetCount(n.to_string().parse().unwrap_or(0)) 71 + } 72 + _ => Inbound::Unknown(format!("{term:?}")), 73 + }, 74 + other => Inbound::Unknown(format!("{other:?}")), 75 + } 76 + } 77 + 78 + /// Extract an explicit reply pid from `{hello, Pid}` if present. 79 + pub fn reply_pid_from_term(term: &Term) -> Option<Pid> { 80 + match term { 81 + Term::Tuple(t) => match t.elements.as_slice() { 82 + [Term::Atom(tag), Term::Pid(pid)] if tag.name == "hello" => Some(pid.clone()), 83 + _ => None, 84 + }, 85 + _ => None, 86 + } 87 + } 88 + 89 + fn list_to_string(list: &eetf::List) -> String { 90 + let bytes: Vec<u8> = list 91 + .elements 92 + .iter() 93 + .filter_map(|t| match t { 94 + Term::FixInteger(n) if (0..=255).contains(&n.value) => Some(n.value as u8), 95 + _ => None, 96 + }) 97 + .collect(); 98 + String::from_utf8_lossy(&bytes).into_owned() 99 + } 100 + 101 + impl Inbound { 102 + pub fn to_app_msg(self) -> Option<AppMsg> { 103 + match self { 104 + Inbound::Hello { .. } => Some(AppMsg::Connected), 105 + Inbound::SetTitle(s) => Some(AppMsg::SetTitle(s)), 106 + Inbound::SetLabel(s) => Some(AppMsg::SetLabel(s)), 107 + Inbound::SetCount(n) => Some(AppMsg::SetCount(n)), 108 + Inbound::Ping => Some(AppMsg::PongRequested), 109 + Inbound::Unknown(s) => { 110 + eprintln!("[protocol] unknown inbound: {s}"); 111 + None 112 + } 113 + } 114 + } 115 + } 116 + 117 + pub fn term_ready(node_name: &str) -> Term { 118 + Term::Tuple(eetf::Tuple { 119 + elements: vec![ 120 + Term::Atom(Atom::from("ready")), 121 + Term::Binary(Binary { 122 + bytes: node_name.as_bytes().to_vec(), 123 + }), 124 + ], 125 + }) 126 + } 127 + 128 + pub fn term_clicked(count: i64) -> Term { 129 + Term::Tuple(eetf::Tuple { 130 + elements: vec![ 131 + Term::Atom(Atom::from("clicked")), 132 + Term::FixInteger(FixInteger { 133 + value: count as i32, 134 + }), 135 + ], 136 + }) 137 + } 138 + 139 + pub fn term_count_changed(count: i64) -> Term { 140 + Term::Tuple(eetf::Tuple { 141 + elements: vec![ 142 + Term::Atom(Atom::from("count_changed")), 143 + Term::FixInteger(FixInteger { 144 + value: count as i32, 145 + }), 146 + ], 147 + }) 148 + } 149 + 150 + pub fn term_pong() -> Term { 151 + Term::Atom(Atom::from("pong")) 152 + } 153 + 154 + pub fn term_window_closed() -> Term { 155 + Term::Atom(Atom::from("window_closed")) 156 + }