Gleam + Relm4 foreign node POC over Erlang distribution
0

Configure Feed

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

gleamtk / ui / src / protocol.rs
5.1 kB 156 lines
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 17use eetf::{Atom, Binary, FixInteger, Term}; 18use erl_dist::term::Pid; 19 20use crate::app::AppMsg; 21 22/// Where to send events back to the Gleam controller. 23#[derive(Debug, Clone)] 24pub 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)] 31pub enum Inbound { 32 Hello { reply_as: Option<String> }, 33 SetTitle(String), 34 SetLabel(String), 35 SetCount(i64), 36 Ping, 37 Unknown(String), 38} 39 40pub 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. 79pub 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 89fn 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 101impl 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 117pub 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 128pub 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 139pub 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 150pub fn term_pong() -> Term { 151 Term::Atom(Atom::from("pong")) 152} 153 154pub fn term_window_closed() -> Term { 155 Term::Atom(Atom::from("window_closed")) 156}