1use anyhow::{Context, Result};
6use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8use std::process::Command;
9
10pub struct Q8CasterBridge {
12 q8_caster_path: PathBuf,
13 api_port: u16,
14}
15
16impl Q8CasterBridge {
17 pub fn new() -> Result<Self> {
18 let q8_path = PathBuf::from("/aidata/ayeverse/q8-caster");
20 if !q8_path.exists() {
21 anyhow::bail!("q8-caster not found at {:?}", q8_path);
22 }
23
24 Ok(Self {
25 q8_caster_path: q8_path,
26 api_port: 8888, })
28 }
29
30 pub async fn ensure_running(&self) -> Result<()> {
32 if self.is_running().await? {
34 return Ok(());
35 }
36
37 let manage_script = self.q8_caster_path.join("scripts/manage.sh");
39 if !manage_script.exists() {
40 let binary = self.q8_caster_path.join("target/release/q8-caster");
42 if binary.exists() {
43 Command::new(binary)
44 .arg("--port")
45 .arg(self.api_port.to_string())
46 .spawn()
47 .context("Failed to start q8-caster")?;
48 } else {
49 anyhow::bail!("q8-caster binary not found. Run 'cargo build --release' in q8-caster directory");
50 }
51 } else {
52 Command::new("bash")
53 .arg(manage_script)
54 .arg("start")
55 .spawn()
56 .context("Failed to start q8-caster via manage.sh")?;
57 }
58
59 tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
61
62 Ok(())
63 }
64
65 async fn is_running(&self) -> Result<bool> {
67 match reqwest::get(format!("http://localhost:{}/health", self.api_port)).await {
69 Ok(resp) => Ok(resp.status().is_success()),
70 Err(_) => Ok(false),
71 }
72 }
73
74 pub async fn discover_devices(&self) -> Result<Vec<CastDevice>> {
76 self.ensure_running().await?;
77
78 let client = reqwest::Client::new();
79 let resp = client
80 .get(format!("http://localhost:{}/api/devices", self.api_port))
81 .send()
82 .await
83 .context("Failed to query devices")?;
84
85 if !resp.status().is_success() {
86 anyhow::bail!("Failed to get devices: {}", resp.status());
87 }
88
89 let devices: Vec<CastDevice> = resp
90 .json()
91 .await
92 .context("Failed to parse devices response")?;
93
94 Ok(devices)
95 }
96
97 pub async fn cast_to_device(&self, device_id: &str, content: &CastContent) -> Result<()> {
99 self.ensure_running().await?;
100
101 let client = reqwest::Client::new();
102 let resp = client
103 .post(format!("http://localhost:{}/api/cast", self.api_port))
104 .json(&CastRequest {
105 device_id: device_id.to_string(),
106 content: content.clone(),
107 })
108 .send()
109 .await
110 .context("Failed to cast content")?;
111
112 if !resp.status().is_success() {
113 let error_text = resp.text().await.unwrap_or_default();
114 anyhow::bail!("Failed to cast: {}", error_text);
115 }
116
117 Ok(())
118 }
119
120 pub async fn start_dashboard(&self, port: u16) -> Result<String> {
122 self.ensure_running().await?;
123
124 Ok(format!("http://localhost:{}/dashboard", port))
126 }
127
128 pub async fn cast_to_esp32(&self, address: &str, content: &str) -> Result<()> {
130 let esp_content = CastContent::Text {
132 text: content.to_string(),
133 format: "plain".to_string(),
134 };
135
136 self.cast_to_device(&format!("esp32:{}", address), &esp_content)
137 .await
138 }
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct CastDevice {
143 pub id: String,
144 pub name: String,
145 pub device_type: DeviceType,
146 pub address: String,
147 pub capabilities: Vec<String>,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
151#[serde(rename_all = "snake_case")]
152pub enum DeviceType {
153 Chromecast,
154 AppleTv,
155 Miracast,
156 Esp32,
157 WebDashboard,
158}
159
160impl std::fmt::Display for DeviceType {
161 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162 match self {
163 DeviceType::Chromecast => write!(f, "Chromecast"),
164 DeviceType::AppleTv => write!(f, "Apple TV"),
165 DeviceType::Miracast => write!(f, "Miracast"),
166 DeviceType::Esp32 => write!(f, "ESP32"),
167 DeviceType::WebDashboard => write!(f, "Web Dashboard"),
168 }
169 }
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
173#[serde(tag = "type", rename_all = "snake_case")]
174pub enum CastContent {
175 Text {
176 text: String,
177 format: String,
178 },
179 Html {
180 html: String,
181 },
182 Markdown {
183 markdown: String,
184 theme: Option<String>,
185 },
186 Image {
187 url: String,
188 },
189 Video {
190 url: String,
191 },
192 Dashboard {
193 widgets: Vec<serde_json::Value>,
194 },
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
198struct CastRequest {
199 device_id: String,
200 content: CastContent,
201}
202
203impl Q8CasterBridge {
205 pub async fn find_device_for_target(
207 &self,
208 target: &crate::rust_shell::DisplayTarget,
209 ) -> Result<Option<CastDevice>> {
210 let devices = self.discover_devices().await?;
211
212 let device = match target {
213 crate::rust_shell::DisplayTarget::AppleTV { name, .. } => devices
214 .into_iter()
215 .find(|d| d.device_type == DeviceType::AppleTv && d.name == *name),
216 crate::rust_shell::DisplayTarget::Chromecast { name, .. } => devices
217 .into_iter()
218 .find(|d| d.device_type == DeviceType::Chromecast && d.name == *name),
219 crate::rust_shell::DisplayTarget::ESP32Display { address, .. } => devices
220 .into_iter()
221 .find(|d| d.device_type == DeviceType::Esp32 && d.address == *address),
222 _ => None,
223 };
224
225 Ok(device)
226 }
227
228 pub fn adapt_content(
230 &self,
231 content: &str,
232 format: &crate::rust_shell::OutputFormat,
233 ) -> CastContent {
234 match format {
235 crate::rust_shell::OutputFormat::HTML => CastContent::Html {
236 html: content.to_string(),
237 },
238 crate::rust_shell::OutputFormat::Markdown => CastContent::Markdown {
239 markdown: content.to_string(),
240 theme: Some("dark".to_string()),
241 },
242 _ => CastContent::Text {
243 text: content.to_string(),
244 format: "plain".to_string(),
245 },
246 }
247 }
248}
249
250pub async fn enhance_rust_shell_with_q8(_shell: &mut crate::rust_shell::RustShell) -> Result<()> {
252 println!("🚀 Enhancing Rust Shell with Q8-Caster capabilities...");
253
254 let bridge = Q8CasterBridge::new()?;
255
256 bridge.ensure_running().await?;
258
259 let devices = bridge.discover_devices().await?;
261 println!(" Found {} Q8-Caster devices", devices.len());
262
263 for device in devices {
264 println!(
265 " • {} ({}): {}",
266 device.name, device.device_type, device.address
267 );
268 }
269
270 Ok(())
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[tokio::test]
278 async fn test_q8_bridge_creation() {
279 if PathBuf::from("/aidata/ayeverse/q8-caster").exists() {
281 let bridge = Q8CasterBridge::new();
282 assert!(bridge.is_ok());
283 }
284 }
285}