Gleam + Relm4 foreign node POC over Erlang distribution
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
6mod app;
7mod dist;
8mod protocol;
9
10use std::sync::OnceLock;
11
12use app::{AppModel, AppMsg};
13use clap::Parser;
14use dist::{DistConfig, Outbound};
15use relm4::prelude::*;
16use relm4::MessageBroker;
17
18/// Global broker so the distribution thread can inject Relm4 inputs.
19pub static BROKER: MessageBroker<AppMsg> = MessageBroker::new();
20
21/// Outbound events from the UI thread → dist thread.
22pub 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")]
26struct 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
44fn 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}