use std::collections::HashMap;
use std::os::unix::io::RawFd;
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime};
use crate::parser::WasmaConfig;
use crate::wasma_client_unix_posix_raw_app::posix;
pub const LETTING_CMD_MAGIC: [u8; 4] = [0x4C, 0x45, 0x54, 0x4C]; pub const LETTING_RSP_MAGIC: [u8; 4] = [0x4C, 0x52, 0x53, 0x50]; pub const LETTING_HDR_SIZE: usize = 20;
#[repr(u32)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LettingMsgType {
Register = 0x01,
Unregister = 0x02,
Subscribe = 0x03,
Unsubscribe = 0x04,
GetField = 0x05,
FieldChanged = 0x06,
PushData = 0x07,
Snapshot = 0x08,
Ping = 0xFE,
Disconnect = 0xFF,
}
impl LettingMsgType {
pub fn from_u32(v: u32) -> Option<Self> {
match v {
0x01 => Some(Self::Register),
0x02 => Some(Self::Unregister),
0x03 => Some(Self::Subscribe),
0x04 => Some(Self::Unsubscribe),
0x05 => Some(Self::GetField),
0x06 => Some(Self::FieldChanged),
0x07 => Some(Self::PushData),
0x08 => Some(Self::Snapshot),
0xFE => Some(Self::Ping),
0xFF => Some(Self::Disconnect),
_ => None,
}
}
pub fn to_u32(self) -> u32 {
self as u32
}
}
#[repr(u32)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LettingStatus {
Ok = 0x00,
Accepted = 0x01,
ErrNotFound = 0x10,
ErrPermission = 0x11,
ErrInvalidField = 0x12,
ErrInternal = 0x13,
ErrAlreadyReg = 0x14,
Pong = 0xFE,
Disconnected = 0xFF,
}
impl LettingStatus {
pub fn from_u32(v: u32) -> Self {
match v {
0x00 => Self::Ok,
0x01 => Self::Accepted,
0x10 => Self::ErrNotFound,
0x11 => Self::ErrPermission,
0x12 => Self::ErrInvalidField,
0x13 => Self::ErrInternal,
0x14 => Self::ErrAlreadyReg,
0xFE => Self::Pong,
0xFF => Self::Disconnected,
_ => Self::ErrInternal,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum AppletKind {
System(SystemAppletRole),
Embedded(EmbeddedAppletRole),
}
#[derive(Debug, Clone, PartialEq)]
pub enum SystemAppletRole {
TrayIcon,
PanelWidget,
StatusBarItem,
NotificationArea,
QuickSettings,
Custom(String),
}
#[derive(Debug, Clone, PartialEq)]
pub enum EmbeddedAppletRole {
MiniView,
SidePanel,
ToolbarExtension,
FloatingOverlay,
Inspector,
Custom(String),
}
impl AppletKind {
pub fn display_name(&self) -> String {
match self {
Self::System(role) => format!("system::{:?}", role),
Self::Embedded(role) => format!("embedded::{:?}", role),
}
}
pub fn is_system(&self) -> bool {
matches!(self, Self::System(_))
}
pub fn is_embedded(&self) -> bool {
matches!(self, Self::Embedded(_))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct FieldKey {
pub namespace: String,
pub name: String,
}
impl FieldKey {
pub fn new(namespace: impl Into<String>, name: impl Into<String>) -> Self {
Self {
namespace: namespace.into(),
name: name.into(),
}
}
pub fn encode(&self) -> Vec<u8> {
let ns = self.namespace.as_bytes();
let nm = self.name.as_bytes();
let mut buf = Vec::with_capacity(2 + ns.len() + 2 + nm.len());
buf.extend_from_slice(&(ns.len() as u16).to_le_bytes());
buf.extend_from_slice(ns);
buf.extend_from_slice(&(nm.len() as u16).to_le_bytes());
buf.extend_from_slice(nm);
buf
}
pub fn decode(buf: &[u8]) -> Option<(Self, usize)> {
if buf.len() < 2 {
return None;
}
let ns_len = u16::from_le_bytes(buf[0..2].try_into().ok()?) as usize;
if buf.len() < 2 + ns_len + 2 {
return None;
}
let namespace = String::from_utf8(buf[2..2 + ns_len].to_vec()).ok()?;
let nm_len = u16::from_le_bytes(buf[2 + ns_len..4 + ns_len].try_into().ok()?) as usize;
if buf.len() < 4 + ns_len + nm_len {
return None;
}
let name = String::from_utf8(buf[4 + ns_len..4 + ns_len + nm_len].to_vec()).ok()?;
let consumed = 4 + ns_len + nm_len;
Some((Self { namespace, name }, consumed))
}
}
#[derive(Debug, Clone)]
pub struct FieldValue {
pub raw: Vec<u8>,
pub type_hint: FieldTypeHint,
pub updated_at: SystemTime,
}
impl FieldValue {
pub fn new(raw: Vec<u8>, type_hint: FieldTypeHint) -> Self {
Self {
raw,
type_hint,
updated_at: SystemTime::now(),
}
}
pub fn as_str(&self) -> Option<&str> {
std::str::from_utf8(&self.raw).ok()
}
pub fn as_u64(&self) -> Option<u64> {
self.raw
.get(0..8)
.and_then(|b| b.try_into().ok())
.map(u64::from_le_bytes)
}
pub fn as_f64(&self) -> Option<f64> {
self.raw
.get(0..8)
.and_then(|b| b.try_into().ok())
.map(f64::from_le_bytes)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum FieldTypeHint {
Bytes,
String,
U64,
F64,
Bool,
Json,
}
#[derive(Debug, Clone)]
pub struct LettingEvent {
pub field: FieldKey,
pub value: FieldValue,
pub previous: Option<FieldValue>,
pub seq: u32,
pub received_at: SystemTime,
}
pub type LettingHandler = Arc<dyn Fn(&LettingEvent) + Send + Sync>;
pub struct LettingFd {
fd: Option<RawFd>,
seq: u32,
connected: bool,
}
impl LettingFd {
pub fn new() -> Self {
Self {
fd: None,
seq: 0,
connected: false,
}
}
pub fn connect_unix(&mut self, path: &str) -> Result<(), std::io::Error> {
use std::os::unix::io::AsRawFd;
use std::os::unix::net::UnixStream;
let stream = UnixStream::connect(path)?;
let fd = stream.as_raw_fd();
let _ = std::mem::ManuallyDrop::new(stream);
self.fd = Some(fd);
self.connected = true;
Ok(())
}
pub fn connect_tcp(&mut self, ip: &str, port: u16) -> Result<(), std::io::Error> {
use std::net::TcpStream;
use std::os::unix::io::AsRawFd;
let stream = TcpStream::connect(format!("{}:{}", ip, port))?;
let fd = stream.as_raw_fd();
let _ = std::mem::ManuallyDrop::new(stream);
self.fd = Some(fd);
self.connected = true;
Ok(())
}
fn next_seq(&mut self) -> u32 {
let s = self.seq;
self.seq = self.seq.wrapping_add(1);
s
}
pub fn send(
&mut self,
msg_type: LettingMsgType,
applet_id: u32,
payload: &[u8],
) -> Result<(LettingStatus, u32, Vec<u8>), std::io::Error> {
let fd = self.fd.ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::NotConnected, "Letting channel not open")
})?;
let seq = self.next_seq();
let mut hdr = [0u8; LETTING_HDR_SIZE];
hdr[0..4].copy_from_slice(&LETTING_CMD_MAGIC);
hdr[4..8].copy_from_slice(&msg_type.to_u32().to_le_bytes());
hdr[8..12].copy_from_slice(&applet_id.to_le_bytes());
hdr[12..16].copy_from_slice(&seq.to_le_bytes());
hdr[16..20].copy_from_slice(&(payload.len() as u32).to_le_bytes());
self.write_all(fd, &hdr)?;
if !payload.is_empty() {
self.write_all(fd, payload)?;
}
let mut rsp_hdr = [0u8; LETTING_HDR_SIZE];
posix::posix_read_exact(fd, &mut rsp_hdr)?;
if rsp_hdr[0..4] != LETTING_RSP_MAGIC {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Invalid response magic: {:?}", &rsp_hdr[0..4]),
));
}
let status = LettingStatus::from_u32(u32::from_le_bytes(rsp_hdr[4..8].try_into().unwrap()));
let resp_seq = u32::from_le_bytes(rsp_hdr[12..16].try_into().unwrap());
let payload_len = u32::from_le_bytes(rsp_hdr[16..20].try_into().unwrap()) as usize;
let mut resp_payload = vec![0u8; payload_len];
if payload_len > 0 {
posix::posix_read_exact(fd, &mut resp_payload)?;
}
Ok((status, resp_seq, resp_payload))
}
pub fn poll_event(
&self,
timeout_ms: i32,
) -> Result<Option<(LettingMsgType, u32, Vec<u8>)>, std::io::Error> {
let fd = self.fd.ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::NotConnected, "Letting channel not open")
})?;
match posix::posix_poll_readable(fd, timeout_ms)? {
false => return Ok(None),
true => {}
}
let mut hdr = [0u8; LETTING_HDR_SIZE];
posix::posix_read_exact(fd, &mut hdr)?;
if hdr[0..4] != LETTING_CMD_MAGIC {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Invalid event magic",
));
}
let msg_type = LettingMsgType::from_u32(u32::from_le_bytes(hdr[4..8].try_into().unwrap()))
.ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::InvalidData, "Unknown message type")
})?;
let seq = u32::from_le_bytes(hdr[12..16].try_into().unwrap());
let payload_len = u32::from_le_bytes(hdr[16..20].try_into().unwrap()) as usize;
let mut payload = vec![0u8; payload_len];
if payload_len > 0 {
posix::posix_read_exact(fd, &mut payload)?;
}
Ok(Some((msg_type, seq, payload)))
}
fn write_all(&self, fd: RawFd, buf: &[u8]) -> Result<(), std::io::Error> {
let mut written = 0;
while written < buf.len() {
let n = unsafe {
libc::write(
fd,
buf[written..].as_ptr() as *const libc::c_void,
buf.len() - written,
)
};
match n {
-1 => return Err(std::io::Error::last_os_error()),
0 => {
return Err(std::io::Error::new(
std::io::ErrorKind::WriteZero,
"write() returned zero",
))
}
n => written += n as usize,
}
}
Ok(())
}
pub fn fd(&self) -> Option<RawFd> {
self.fd
}
pub fn is_connected(&self) -> bool {
self.connected
}
}
impl Drop for LettingFd {
fn drop(&mut self) {
if let Some(fd) = self.fd.take() {
if fd != 0 {
let _ = posix::posix_close(fd);
}
}
}
}
impl Default for LettingFd {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct AppletDescriptor {
pub id: u32,
pub name: String,
pub version: String,
pub kind: AppletKind,
pub host_app_id: String,
pub host_window_id: Option<u64>,
pub registered_at: SystemTime,
}
impl AppletDescriptor {
pub fn new(
name: impl Into<String>,
version: impl Into<String>,
kind: AppletKind,
host_app_id: impl Into<String>,
) -> Self {
Self {
id: 0, name: name.into(),
version: version.into(),
kind,
host_app_id: host_app_id.into(),
host_window_id: None,
registered_at: SystemTime::now(),
}
}
pub fn encode(&self) -> Vec<u8> {
let name = self.name.as_bytes();
let ver = self.version.as_bytes();
let host = self.host_app_id.as_bytes();
let kind_byte: u8 = if self.kind.is_system() { 0x01 } else { 0x02 };
let win_id = self.host_window_id.unwrap_or(0u64);
let mut buf = Vec::new();
buf.extend_from_slice(&(name.len() as u16).to_le_bytes());
buf.extend_from_slice(name);
buf.extend_from_slice(&(ver.len() as u16).to_le_bytes());
buf.extend_from_slice(ver);
buf.extend_from_slice(&(host.len() as u16).to_le_bytes());
buf.extend_from_slice(host);
buf.push(kind_byte);
buf.extend_from_slice(&win_id.to_le_bytes());
buf
}
}
pub struct FieldCache {
fields: HashMap<FieldKey, FieldValue>,
}
impl FieldCache {
pub fn new() -> Self {
Self {
fields: HashMap::new(),
}
}
pub fn update(&mut self, key: FieldKey, value: FieldValue) -> Option<FieldValue> {
self.fields.insert(key, value)
}
pub fn get(&self, key: &FieldKey) -> Option<&FieldValue> {
self.fields.get(key)
}
pub fn remove(&mut self, key: &FieldKey) -> Option<FieldValue> {
self.fields.remove(key)
}
pub fn keys(&self) -> impl Iterator<Item = &FieldKey> {
self.fields.keys()
}
pub fn len(&self) -> usize {
self.fields.len()
}
pub fn is_empty(&self) -> bool {
self.fields.is_empty()
}
}
impl Default for FieldCache {
fn default() -> Self {
Self::new()
}
}
pub struct ApplettelClient {
config: Arc<WasmaConfig>,
descriptor: AppletDescriptor,
fd: LettingFd,
cache: Arc<Mutex<FieldCache>>,
handlers: Arc<Mutex<HashMap<FieldKey, Vec<LettingHandler>>>>,
subscriptions: Arc<Mutex<Vec<FieldKey>>>,
running: Arc<Mutex<bool>>,
socket_path: String,
}
impl ApplettelClient {
pub fn new(config: WasmaConfig, descriptor: AppletDescriptor) -> Self {
let socket_path = if let Some(proto) = config.uri_handling.protocols.first() {
format!("/run/wasma/letting_{}.sock", proto.port)
} else {
"/run/wasma/letting.sock".to_string()
};
Self {
descriptor,
fd: LettingFd::new(),
cache: Arc::new(Mutex::new(FieldCache::new())),
handlers: Arc::new(Mutex::new(HashMap::new())),
subscriptions: Arc::new(Mutex::new(Vec::new())),
running: Arc::new(Mutex::new(false)),
socket_path,
config: Arc::new(config),
}
}
pub fn from_config(config: Arc<WasmaConfig>, descriptor: AppletDescriptor) -> Self {
let socket_path = if let Some(proto) = config.uri_handling.protocols.first() {
format!("/run/wasma/letting_{}.sock", proto.port)
} else {
"/run/wasma/letting.sock".to_string()
};
Self {
descriptor,
fd: LettingFd::new(),
cache: Arc::new(Mutex::new(FieldCache::new())),
handlers: Arc::new(Mutex::new(HashMap::new())),
subscriptions: Arc::new(Mutex::new(Vec::new())),
running: Arc::new(Mutex::new(false)),
socket_path,
config,
}
}
pub fn with_socket(mut self, path: impl Into<String>) -> Self {
self.socket_path = path.into();
self
}
pub fn connect(&mut self) -> Result<(), String> {
self.fd
.connect_unix(&self.socket_path)
.map_err(|e| format!("Letting connect failed ({}): {}", self.socket_path, e))?;
println!("🔌 ApplettelClient: Connected → {}", self.socket_path);
Ok(())
}
pub fn connect_tcp(&mut self, ip: &str, port: u16) -> Result<(), String> {
self.fd
.connect_tcp(ip, port)
.map_err(|e| format!("Letting TCP connect failed ({}:{}): {}", ip, port, e))?;
println!("🔌 ApplettelClient: Connected via TCP → {}:{}", ip, port);
Ok(())
}
pub fn register(&mut self) -> Result<u32, String> {
if !self.fd.is_connected() {
return Err("Not connected — call connect() first".to_string());
}
let payload = self.descriptor.encode();
match self.fd.send(LettingMsgType::Register, 0, &payload) {
Ok((LettingStatus::Accepted, _, resp)) => {
if resp.len() >= 4 {
let applet_id = u32::from_le_bytes(resp[0..4].try_into().unwrap());
self.descriptor.id = applet_id;
self.descriptor.registered_at = SystemTime::now();
println!(
"✅ ApplettelClient: Registered → id={} kind={}",
applet_id,
self.descriptor.kind.display_name()
);
Ok(applet_id)
} else {
Err("Register: invalid response payload".to_string())
}
}
Ok((LettingStatus::ErrAlreadyReg, _, _)) => {
println!(
"⚠️ ApplettelClient: Already registered (id={})",
self.descriptor.id
);
Ok(self.descriptor.id)
}
Ok((status, _, _)) => Err(format!("Register failed: {:?}", status)),
Err(e) => Err(format!("Register error: {}", e)),
}
}
pub fn unregister(&mut self) -> Result<(), String> {
if !self.fd.is_connected() {
return Ok(());
}
let _ = self
.fd
.send(LettingMsgType::Unregister, self.descriptor.id, &[]);
println!(
"👋 ApplettelClient: Unregistered (id={})",
self.descriptor.id
);
Ok(())
}
pub fn subscribe(&mut self, field: FieldKey, handler: LettingHandler) -> Result<(), String> {
if self.fd.is_connected() {
let payload = field.encode();
match self
.fd
.send(LettingMsgType::Subscribe, self.descriptor.id, &payload)
{
Ok((LettingStatus::Ok, _, _)) | Ok((LettingStatus::Accepted, _, _)) => {}
Ok((status, _, _)) => {
return Err(format!("Subscribe failed for {:?}: {:?}", field, status));
}
Err(e) => {
eprintln!("⚠️ Subscribe send error: {} — registered locally only", e);
}
}
}
let mut handlers = self.handlers.lock().unwrap();
handlers.entry(field.clone()).or_default().push(handler);
let mut subs = self.subscriptions.lock().unwrap();
if !subs.contains(&field) {
subs.push(field.clone());
}
println!(
"📡 ApplettelClient: Subscribed → {}/{}",
field.namespace, field.name
);
Ok(())
}
pub fn unsubscribe(&mut self, field: &FieldKey) -> Result<(), String> {
if self.fd.is_connected() {
let payload = field.encode();
let _ = self
.fd
.send(LettingMsgType::Unsubscribe, self.descriptor.id, &payload);
}
let mut handlers = self.handlers.lock().unwrap();
handlers.remove(field);
let mut subs = self.subscriptions.lock().unwrap();
subs.retain(|f| f != field);
println!(
"🔕 ApplettelClient: Unsubscribed → {}/{}",
field.namespace, field.name
);
Ok(())
}
pub fn get_field(&mut self, field: &FieldKey) -> Result<FieldValue, String> {
{
let cache = self.cache.lock().unwrap();
if let Some(val) = cache.get(field) {
return Ok(val.clone());
}
}
if self.fd.is_connected() {
let payload = field.encode();
match self
.fd
.send(LettingMsgType::GetField, self.descriptor.id, &payload)
{
Ok((LettingStatus::Ok, _, raw)) => {
let value = FieldValue::new(raw.clone(), FieldTypeHint::Bytes);
self.cache
.lock()
.unwrap()
.update(field.clone(), value.clone());
Ok(value)
}
Ok((status, _, _)) => Err(format!("GetField failed: {:?}", status)),
Err(e) => Err(format!("GetField error: {}", e)),
}
} else {
Err(format!(
"Field {}/{} not in cache and not connected",
field.namespace, field.name
))
}
}
pub fn push_data(&mut self, field: &FieldKey, data: &[u8]) -> Result<(), String> {
if !self.fd.is_connected() {
return Err("Not connected".to_string());
}
let mut payload = field.encode();
payload.extend_from_slice(&(data.len() as u32).to_le_bytes());
payload.extend_from_slice(data);
match self
.fd
.send(LettingMsgType::PushData, self.descriptor.id, &payload)
{
Ok((LettingStatus::Ok, _, _)) | Ok((LettingStatus::Accepted, _, _)) => Ok(()),
Ok((status, _, _)) => Err(format!("PushData failed: {:?}", status)),
Err(e) => Err(format!("PushData error: {}", e)),
}
}
pub fn snapshot(&mut self) -> Result<HashMap<String, Vec<u8>>, String> {
if !self.fd.is_connected() {
return Err("Not connected".to_string());
}
match self
.fd
.send(LettingMsgType::Snapshot, self.descriptor.id, &[])
{
Ok((LettingStatus::Ok, _, payload)) => {
let mut result = HashMap::new();
let mut offset = 0;
if payload.len() < 4 {
return Ok(result);
}
let count = u32::from_le_bytes(payload[0..4].try_into().unwrap()) as usize;
offset += 4;
for _ in 0..count {
if offset + 2 > payload.len() {
break;
}
let key_len =
u16::from_le_bytes(payload[offset..offset + 2].try_into().unwrap())
as usize;
offset += 2;
if offset + key_len + 4 > payload.len() {
break;
}
let key =
String::from_utf8_lossy(&payload[offset..offset + key_len]).to_string();
offset += key_len;
let val_len =
u32::from_le_bytes(payload[offset..offset + 4].try_into().unwrap())
as usize;
offset += 4;
if offset + val_len > payload.len() {
break;
}
let val = payload[offset..offset + val_len].to_vec();
offset += val_len;
result.insert(key, val);
}
println!(
"📸 ApplettelClient: Snapshot received — {} fields",
result.len()
);
Ok(result)
}
Ok((status, _, _)) => Err(format!("Snapshot failed: {:?}", status)),
Err(e) => Err(format!("Snapshot error: {}", e)),
}
}
pub fn run_loop(&self) -> Result<(), Box<dyn std::error::Error>> {
*self.running.lock().unwrap() = true;
println!(
"🔄 ApplettelClient: Reactive loop started (id={} kind={})",
self.descriptor.id,
self.descriptor.kind.display_name()
);
loop {
if !*self.running.lock().unwrap() {
println!("🛑 ApplettelClient: Reactive loop stopped");
break;
}
let event = match self.fd.poll_event(50) {
Ok(Some(ev)) => ev,
Ok(None) => continue,
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
Err(e) => {
eprintln!("⚠️ ApplettelClient: Poll error: {}", e);
break;
}
};
let (msg_type, seq, payload) = event;
match msg_type {
LettingMsgType::FieldChanged => {
self.handle_field_changed(seq, &payload);
}
LettingMsgType::Ping => {
println!("🏓 ApplettelClient: Keepalive ping (seq={})", seq);
}
LettingMsgType::Disconnect => {
println!("📭 ApplettelClient: Host disconnected");
*self.running.lock().unwrap() = false;
break;
}
other => {
println!(
"📨 ApplettelClient: Unhandled msg type {:?} (seq={})",
other, seq
);
}
}
}
Ok(())
}
pub fn stop(&self) {
*self.running.lock().unwrap() = false;
println!("🛑 ApplettelClient: Stop requested");
}
fn handle_field_changed(&self, seq: u32, payload: &[u8]) {
let (field, consumed) = match FieldKey::decode(payload) {
Some(r) => r,
None => {
eprintln!("⚠️ ApplettelClient: Failed to decode field key");
return;
}
};
if consumed + 4 > payload.len() {
eprintln!("⚠️ ApplettelClient: Payload too short for value");
return;
}
let val_len =
u32::from_le_bytes(payload[consumed..consumed + 4].try_into().unwrap()) as usize;
if consumed + 4 + val_len > payload.len() {
eprintln!("⚠️ ApplettelClient: Value truncated");
return;
}
let raw_val = payload[consumed + 4..consumed + 4 + val_len].to_vec();
let new_value = FieldValue::new(raw_val, FieldTypeHint::Bytes);
let previous = {
let mut cache = self.cache.lock().unwrap();
cache.update(field.clone(), new_value.clone())
};
let event = LettingEvent {
field: field.clone(),
value: new_value,
previous,
seq,
received_at: SystemTime::now(),
};
let handlers = self.handlers.lock().unwrap();
if let Some(field_handlers) = handlers.get(&field) {
for handler in field_handlers {
handler(&event);
}
}
}
pub fn ping(&mut self) -> Result<Duration, String> {
if !self.fd.is_connected() {
return Err("Not connected".to_string());
}
let start = std::time::Instant::now();
match self.fd.send(LettingMsgType::Ping, self.descriptor.id, &[]) {
Ok((LettingStatus::Pong, _, _)) => Ok(start.elapsed()),
Ok((status, _, _)) => Err(format!("Ping unexpected status: {:?}", status)),
Err(e) => Err(format!("Ping error: {}", e)),
}
}
pub fn applet_id(&self) -> u32 {
self.descriptor.id
}
pub fn descriptor(&self) -> &AppletDescriptor {
&self.descriptor
}
pub fn is_connected(&self) -> bool {
self.fd.is_connected()
}
pub fn is_running(&self) -> bool {
*self.running.lock().unwrap()
}
pub fn subscription_count(&self) -> usize {
self.subscriptions.lock().unwrap().len()
}
pub fn cached_field_count(&self) -> usize {
self.cache.lock().unwrap().len()
}
pub fn get_config(&self) -> &WasmaConfig {
&self.config
}
}
pub struct ApplettelClientBuilder {
config: Option<WasmaConfig>,
descriptor: Option<AppletDescriptor>,
socket_path: Option<String>,
initial_subscriptions: Vec<(FieldKey, LettingHandler)>,
}
impl ApplettelClientBuilder {
pub fn new() -> Self {
Self {
config: None,
descriptor: None,
socket_path: None,
initial_subscriptions: Vec::new(),
}
}
pub fn with_config(mut self, config: WasmaConfig) -> Self {
self.config = Some(config);
self
}
pub fn with_descriptor(mut self, desc: AppletDescriptor) -> Self {
self.descriptor = Some(desc);
self
}
pub fn with_socket(mut self, path: impl Into<String>) -> Self {
self.socket_path = Some(path.into());
self
}
pub fn subscribe_on_start(mut self, field: FieldKey, handler: LettingHandler) -> Self {
self.initial_subscriptions.push((field, handler));
self
}
pub fn build(self) -> Result<ApplettelClient, String> {
let config = self.config.ok_or("Config required")?;
let descriptor = self.descriptor.ok_or("AppletDescriptor required")?;
let mut client = ApplettelClient::new(config, descriptor);
if let Some(path) = self.socket_path {
client.socket_path = path;
}
for (field, handler) in self.initial_subscriptions {
let mut handlers = client.handlers.lock().unwrap();
handlers.entry(field.clone()).or_default().push(handler);
let mut subs = client.subscriptions.lock().unwrap();
if !subs.contains(&field) {
subs.push(field);
}
}
Ok(client)
}
}
impl Default for ApplettelClientBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::ConfigParser;
use std::sync::atomic::{AtomicU32, Ordering};
fn make_config() -> WasmaConfig {
let parser = ConfigParser::new(None);
let content = parser.generate_default_config();
parser.parse(&content).unwrap()
}
fn make_system_descriptor() -> AppletDescriptor {
AppletDescriptor::new(
"test-tray",
"1.0.0",
AppletKind::System(SystemAppletRole::TrayIcon),
"host.app",
)
}
fn make_embedded_descriptor() -> AppletDescriptor {
AppletDescriptor::new(
"test-embed",
"1.0.0",
AppletKind::Embedded(EmbeddedAppletRole::SidePanel),
"host.app",
)
}
#[test]
fn test_client_creation_system() {
let config = make_config();
let desc = make_system_descriptor();
let client = ApplettelClient::new(config, desc);
assert!(!client.is_connected());
assert!(!client.is_running());
assert_eq!(client.applet_id(), 0);
assert!(client.descriptor().kind.is_system());
println!("✅ System applet creation working");
}
#[test]
fn test_client_creation_embedded() {
let config = make_config();
let desc = make_embedded_descriptor();
let client = ApplettelClient::new(config, desc);
assert!(client.descriptor().kind.is_embedded());
println!("✅ Embedded applet creation working");
}
#[test]
fn test_field_key_codec() {
let key = FieldKey::new("window", "title");
let encoded = key.encode();
let (decoded, consumed) = FieldKey::decode(&encoded).unwrap();
assert_eq!(decoded.namespace, "window");
assert_eq!(decoded.name, "title");
assert_eq!(consumed, encoded.len());
println!("✅ FieldKey codec working");
}
#[test]
fn test_field_key_codec_unicode() {
let key = FieldKey::new("resource_manager", "cpu_usage_percentage");
let encoded = key.encode();
let (decoded, _) = FieldKey::decode(&encoded).unwrap();
assert_eq!(decoded.namespace, "resource_manager");
assert_eq!(decoded.name, "cpu_usage_percentage");
println!("✅ FieldKey unicode/long codec working");
}
#[test]
fn test_field_cache_operations() {
let mut cache = FieldCache::new();
let key = FieldKey::new("app", "state");
assert!(cache.get(&key).is_none());
let val1 = FieldValue::new(b"running".to_vec(), FieldTypeHint::String);
let prev = cache.update(key.clone(), val1);
assert!(prev.is_none());
let val2 = FieldValue::new(b"paused".to_vec(), FieldTypeHint::String);
let prev = cache.update(key.clone(), val2);
assert!(prev.is_some());
assert_eq!(prev.unwrap().as_str(), Some("running"));
let current = cache.get(&key).unwrap();
assert_eq!(current.as_str(), Some("paused"));
cache.remove(&key);
assert!(cache.get(&key).is_none());
println!("✅ FieldCache operations working");
}
#[test]
fn test_field_value_accessors() {
let u64_val = FieldValue::new(42u64.to_le_bytes().to_vec(), FieldTypeHint::U64);
assert_eq!(u64_val.as_u64(), Some(42));
let f64_val = FieldValue::new(3.14f64.to_le_bytes().to_vec(), FieldTypeHint::F64);
assert!((f64_val.as_f64().unwrap() - 3.14).abs() < f64::EPSILON);
let str_val = FieldValue::new(b"hello".to_vec(), FieldTypeHint::String);
assert_eq!(str_val.as_str(), Some("hello"));
println!("✅ FieldValue accessors working");
}
#[test]
fn test_handler_registration_via_builder() {
let config = make_config();
let desc = make_system_descriptor();
let counter = Arc::new(AtomicU32::new(0));
let counter_clone = counter.clone();
let client = ApplettelClientBuilder::new()
.with_config(config)
.with_descriptor(desc)
.with_socket("/run/wasma/test.sock")
.subscribe_on_start(
FieldKey::new("window", "title"),
Arc::new(move |_event| {
counter_clone.fetch_add(1, Ordering::SeqCst);
}),
)
.build()
.unwrap();
assert_eq!(client.subscription_count(), 1);
println!("✅ Builder handler registration working");
}
#[test]
fn test_handle_field_changed_triggers_handler() {
let config = make_config();
let desc = make_system_descriptor();
let fired = Arc::new(AtomicU32::new(0));
let fired_clone = fired.clone();
let client = ApplettelClient::new(config, desc);
let field = FieldKey::new("window", "state");
{
let mut handlers = client.handlers.lock().unwrap();
handlers
.entry(field.clone())
.or_default()
.push(Arc::new(move |event| {
fired_clone.fetch_add(1, Ordering::SeqCst);
assert_eq!(event.field.namespace, "window");
assert_eq!(event.field.name, "state");
}));
}
let mut payload = field.encode();
let val = b"maximized";
payload.extend_from_slice(&(val.len() as u32).to_le_bytes());
payload.extend_from_slice(val);
client.handle_field_changed(1, &payload);
assert_eq!(fired.load(Ordering::SeqCst), 1);
assert_eq!(client.cached_field_count(), 1);
println!("✅ Reactive handler trigger working");
}
#[test]
fn test_handle_field_changed_updates_cache_with_previous() {
let config = make_config();
let desc = make_embedded_descriptor();
let prev_seen = Arc::new(Mutex::new(Option::<Vec<u8>>::None));
let prev_clone = prev_seen.clone();
let client = ApplettelClient::new(config, desc);
let field = FieldKey::new("resource", "cpu_usage");
{
let mut handlers = client.handlers.lock().unwrap();
handlers
.entry(field.clone())
.or_default()
.push(Arc::new(move |event| {
if let Some(ref prev) = event.previous {
*prev_clone.lock().unwrap() = Some(prev.raw.clone());
}
}));
}
let mut p1 = field.encode();
p1.extend_from_slice(&4u32.to_le_bytes());
p1.extend_from_slice(&42u32.to_le_bytes());
client.handle_field_changed(1, &p1);
assert!(prev_seen.lock().unwrap().is_none());
let mut p2 = field.encode();
p2.extend_from_slice(&4u32.to_le_bytes());
p2.extend_from_slice(&99u32.to_le_bytes());
client.handle_field_changed(2, &p2);
let prev = prev_seen.lock().unwrap().clone();
assert!(prev.is_some());
println!("✅ Previous value tracking working");
}
#[test]
fn test_applet_kind_display() {
let sys = AppletKind::System(SystemAppletRole::TrayIcon);
let emb = AppletKind::Embedded(EmbeddedAppletRole::Inspector);
assert!(sys.display_name().starts_with("system::"));
assert!(emb.display_name().starts_with("embedded::"));
assert!(sys.is_system());
assert!(emb.is_embedded());
assert!(!sys.is_embedded());
assert!(!emb.is_system());
println!("✅ AppletKind display working");
}
#[test]
fn test_descriptor_encode() {
let mut desc = make_system_descriptor();
desc.host_window_id = Some(42);
let encoded = desc.encode();
assert!(!encoded.is_empty());
println!(
"✅ AppletDescriptor encode working ({} bytes)",
encoded.len()
);
}
#[test]
fn test_stop_flag() {
let config = make_config();
let desc = make_system_descriptor();
let client = ApplettelClient::new(config, desc);
assert!(!client.is_running());
*client.running.lock().unwrap() = true;
assert!(client.is_running());
client.stop();
assert!(!client.is_running());
println!("✅ Stop flag working");
}
}