1use serde::{Deserialize, Serialize};
16use serde_json::Value;
17use std::path::PathBuf;
18use std::sync::Arc;
19use tokio::sync::{mpsc, oneshot};
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct Envelope {
24 pub id: u64,
25 pub cmd: String,
26 #[serde(default)]
27 pub args: Value,
28}
29
30#[derive(Debug, Clone, Default, Serialize, Deserialize)]
34pub struct Reply {
35 pub id: u64,
36 pub frame: u64,
37 pub ok: bool,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub result: Option<Value>,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub error: Option<String>,
42 #[serde(default, skip_serializing_if = "Vec::is_empty")]
43 pub notices: Vec<Value>,
44 #[serde(skip)]
45 pub blob: Option<Vec<u8>>,
46}
47
48impl Reply {
49 pub fn err(id: u64, frame: u64, error: impl Into<String>) -> Self {
50 Self { id, frame, ok: false, result: None, error: Some(error.into()), notices: Vec::new(), blob: None }
51 }
52
53 pub fn from_app(value: Value, blob: Option<Vec<u8>>) -> Result<Self, String> {
56 let mut reply: Reply = serde_json::from_value(value).map_err(|e| format!("app reply: {e}"))?;
57 reply.blob = blob;
58 Ok(reply)
59 }
60}
61
62pub enum HostRequest {
64 Command { envelope: Envelope, reply: oneshot::Sender<Reply> },
66 Screenshot { region: Value, reply: oneshot::Sender<Result<(Value, Vec<u8>), String>> },
69 Stop,
70}
71
72#[derive(Debug, Clone, serde::Serialize)]
73pub struct HostInfo {
74 pub backend: &'static str,
75 pub adapter: String,
76 pub platform: &'static str,
77 pub width: f32,
78 pub height: f32,
79 pub ppp: f32,
80}
81
82#[derive(Debug, Clone)]
84pub struct HostConfig {
85 pub width: f32,
86 pub height: f32,
87 pub ppp: f32,
88 pub seed: bool,
89 pub store_dir: PathBuf,
91}
92
93pub type SpawnFn = Arc<dyn Fn(HostConfig) -> Result<HostHandle, String> + Send + Sync>;
94
95#[derive(Clone)]
97pub enum Backend {
98 Spawn { name: &'static str, spawn: SpawnFn },
101 Attached { tx: mpsc::UnboundedSender<HostRequest>, info: HostInfo },
104}
105
106impl Backend {
107 pub fn name(&self) -> &'static str {
108 match self {
109 Backend::Spawn { name, .. } => name,
110 Backend::Attached { info, .. } => info.backend,
111 }
112 }
113
114 pub fn is_attached(&self) -> bool {
115 matches!(self, Backend::Attached { .. })
116 }
117
118 pub fn attached_handle(&self) -> Option<HostHandle> {
120 match self {
121 Backend::Attached { tx, info } => Some(HostHandle::attached(tx.clone(), info.clone())),
122 Backend::Spawn { .. } => None,
123 }
124 }
125}
126
127pub struct HostHandle {
128 pub tx: mpsc::UnboundedSender<HostRequest>,
129 pub info: HostInfo,
130 join: Option<std::thread::JoinHandle<()>>,
131 owned: bool,
134 next_id: std::sync::atomic::AtomicU64,
135}
136
137impl HostHandle {
138 pub fn new(tx: mpsc::UnboundedSender<HostRequest>, info: HostInfo, join: std::thread::JoinHandle<()>) -> Self {
140 Self { tx, info, join: Some(join), owned: true, next_id: std::sync::atomic::AtomicU64::new(1) }
141 }
142
143 pub fn attached(tx: mpsc::UnboundedSender<HostRequest>, info: HostInfo) -> Self {
145 Self { tx, info, join: None, owned: false, next_id: std::sync::atomic::AtomicU64::new(1) }
146 }
147
148 pub fn is_owned(&self) -> bool {
149 self.owned
150 }
151
152 pub async fn call(&self, cmd: &str, args: Value) -> Result<Reply, String> {
154 let id = self.next_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
155 let (tx, rx) = oneshot::channel();
156 self.tx
157 .send(HostRequest::Command { envelope: Envelope { id, cmd: cmd.to_string(), args }, reply: tx })
158 .map_err(|_| "the app host has stopped".to_string())?;
159 rx.await.map_err(|_| "the app host dropped the reply (did it panic?)".to_string())
160 }
161
162 pub async fn call_ok(&self, cmd: &str, args: Value) -> Result<Reply, String> {
164 let r = self.call(cmd, args).await?;
165 if r.ok {
166 Ok(r)
167 } else {
168 Err(r.error.unwrap_or_else(|| "command failed".into()))
169 }
170 }
171
172 pub async fn screenshot(&self, region: Value) -> Result<(Value, Vec<u8>), String> {
173 let (tx, rx) = oneshot::channel();
174 self.tx
175 .send(HostRequest::Screenshot { region, reply: tx })
176 .map_err(|_| "the app host has stopped".to_string())?;
177 rx.await.map_err(|_| "the app host dropped the screenshot reply".to_string())?
178 }
179
180 pub fn stop(mut self) {
182 if !self.owned {
183 return;
184 }
185 let _ = self.tx.send(HostRequest::Stop);
186 if let Some(j) = self.join.take() {
187 let _ = j.join();
188 }
189 }
190}
191
192