use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::{mpsc, oneshot};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Envelope {
pub id: u64,
pub cmd: String,
#[serde(default)]
pub args: Value,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Reply {
pub id: u64,
pub frame: u64,
pub ok: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub notices: Vec<Value>,
#[serde(skip)]
pub blob: Option<Vec<u8>>,
}
impl Reply {
pub fn err(id: u64, frame: u64, error: impl Into<String>) -> Self {
Self { id, frame, ok: false, result: None, error: Some(error.into()), notices: Vec::new(), blob: None }
}
pub fn from_app(value: Value, blob: Option<Vec<u8>>) -> Result<Self, String> {
let mut reply: Reply = serde_json::from_value(value).map_err(|e| format!("app reply: {e}"))?;
reply.blob = blob;
Ok(reply)
}
}
pub enum HostRequest {
Command { envelope: Envelope, reply: oneshot::Sender<Reply> },
Screenshot { region: Value, reply: oneshot::Sender<Result<(Value, Vec<u8>), String>> },
Stop,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct HostInfo {
pub backend: &'static str,
pub adapter: String,
pub platform: &'static str,
pub width: f32,
pub height: f32,
pub ppp: f32,
}
#[derive(Debug, Clone)]
pub struct HostConfig {
pub width: f32,
pub height: f32,
pub ppp: f32,
pub seed: bool,
pub store_dir: PathBuf,
}
pub type SpawnFn = Arc<dyn Fn(HostConfig) -> Result<HostHandle, String> + Send + Sync>;
#[derive(Clone)]
pub enum Backend {
Spawn { name: &'static str, spawn: SpawnFn },
Attached { tx: mpsc::UnboundedSender<HostRequest>, info: HostInfo },
}
impl Backend {
pub fn name(&self) -> &'static str {
match self {
Backend::Spawn { name, .. } => name,
Backend::Attached { info, .. } => info.backend,
}
}
pub fn is_attached(&self) -> bool {
matches!(self, Backend::Attached { .. })
}
pub fn attached_handle(&self) -> Option<HostHandle> {
match self {
Backend::Attached { tx, info } => Some(HostHandle::attached(tx.clone(), info.clone())),
Backend::Spawn { .. } => None,
}
}
}
pub struct HostHandle {
pub tx: mpsc::UnboundedSender<HostRequest>,
pub info: HostInfo,
join: Option<std::thread::JoinHandle<()>>,
owned: bool,
next_id: std::sync::atomic::AtomicU64,
}
impl HostHandle {
pub fn new(tx: mpsc::UnboundedSender<HostRequest>, info: HostInfo, join: std::thread::JoinHandle<()>) -> Self {
Self { tx, info, join: Some(join), owned: true, next_id: std::sync::atomic::AtomicU64::new(1) }
}
pub fn attached(tx: mpsc::UnboundedSender<HostRequest>, info: HostInfo) -> Self {
Self { tx, info, join: None, owned: false, next_id: std::sync::atomic::AtomicU64::new(1) }
}
pub fn is_owned(&self) -> bool {
self.owned
}
pub async fn call(&self, cmd: &str, args: Value) -> Result<Reply, String> {
let id = self.next_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let (tx, rx) = oneshot::channel();
self.tx
.send(HostRequest::Command { envelope: Envelope { id, cmd: cmd.to_string(), args }, reply: tx })
.map_err(|_| "the app host has stopped".to_string())?;
rx.await.map_err(|_| "the app host dropped the reply (did it panic?)".to_string())
}
pub async fn call_ok(&self, cmd: &str, args: Value) -> Result<Reply, String> {
let r = self.call(cmd, args).await?;
if r.ok {
Ok(r)
} else {
Err(r.error.unwrap_or_else(|| "command failed".into()))
}
}
pub async fn screenshot(&self, region: Value) -> Result<(Value, Vec<u8>), String> {
let (tx, rx) = oneshot::channel();
self.tx
.send(HostRequest::Screenshot { region, reply: tx })
.map_err(|_| "the app host has stopped".to_string())?;
rx.await.map_err(|_| "the app host dropped the screenshot reply".to_string())?
}
pub fn stop(mut self) {
if !self.owned {
return;
}
let _ = self.tx.send(HostRequest::Stop);
if let Some(j) = self.join.take() {
let _ = j.join();
}
}
}