Skip to main content

st/
q8_caster_bridge.rs

1// Q8-Caster Bridge - "Bridging the casting chasm!" 🌉
2// Integrates q8-caster functionality into Smart Tree's Rust Shell
3// "One shell to cast them all!" - Hue
4
5use anyhow::{Context, Result};
6use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8use std::process::Command;
9
10/// Bridge to q8-caster functionality
11pub struct Q8CasterBridge {
12    q8_caster_path: PathBuf,
13    api_port: u16,
14}
15
16impl Q8CasterBridge {
17    pub fn new() -> Result<Self> {
18        // Check if q8-caster is available
19        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, // Default q8-caster port
27        })
28    }
29
30    /// Start q8-caster server if not running
31    pub async fn ensure_running(&self) -> Result<()> {
32        // Check if already running
33        if self.is_running().await? {
34            return Ok(());
35        }
36
37        // Start q8-caster using its manage.sh script
38        let manage_script = self.q8_caster_path.join("scripts/manage.sh");
39        if !manage_script.exists() {
40            // Try direct binary
41            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        // Wait for it to start
60        tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
61
62        Ok(())
63    }
64
65    /// Check if q8-caster is running
66    async fn is_running(&self) -> Result<bool> {
67        // Try to connect to the API port
68        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    /// Discover available cast devices
75    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    /// Cast content to a specific device
98    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    /// Start web dashboard
121    pub async fn start_dashboard(&self, port: u16) -> Result<String> {
122        self.ensure_running().await?;
123
124        // The dashboard is served by q8-caster itself
125        Ok(format!("http://localhost:{}/dashboard", port))
126    }
127
128    /// Cast to ESP32 display
129    pub async fn cast_to_esp32(&self, address: &str, content: &str) -> Result<()> {
130        // ESP32 devices are handled specially through q8-caster
131        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
203/// Integration with rust_shell
204impl Q8CasterBridge {
205    /// Convert rust_shell DisplayTarget to q8-caster device lookup
206    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    /// Adapt rust_shell content for q8-caster
229    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
250/// Q8-Caster enhanced functionality for rust_shell
251pub 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    // Ensure q8-caster is running
257    bridge.ensure_running().await?;
258
259    // Discover and add devices
260    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        // This test will only pass if q8-caster is available
280        if PathBuf::from("/aidata/ayeverse/q8-caster").exists() {
281            let bridge = Q8CasterBridge::new();
282            assert!(bridge.is_ok());
283        }
284    }
285}