smart-tree 8.0.1

Smart Tree - An intelligent, AI-friendly directory visualization tool
Documentation
// Q8-Caster Bridge - "Bridging the casting chasm!" 🌉
// Integrates q8-caster functionality into Smart Tree's Rust Shell
// "One shell to cast them all!" - Hue

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::process::Command;

/// Bridge to q8-caster functionality
pub struct Q8CasterBridge {
    q8_caster_path: PathBuf,
    api_port: u16,
}

impl Q8CasterBridge {
    pub fn new() -> Result<Self> {
        // Check if q8-caster is available
        let q8_path = PathBuf::from("/aidata/ayeverse/q8-caster");
        if !q8_path.exists() {
            anyhow::bail!("q8-caster not found at {:?}", q8_path);
        }

        Ok(Self {
            q8_caster_path: q8_path,
            api_port: 8888, // Default q8-caster port
        })
    }

    /// Start q8-caster server if not running
    pub async fn ensure_running(&self) -> Result<()> {
        // Check if already running
        if self.is_running().await? {
            return Ok(());
        }

        // Start q8-caster using its manage.sh script
        let manage_script = self.q8_caster_path.join("scripts/manage.sh");
        if !manage_script.exists() {
            // Try direct binary
            let binary = self.q8_caster_path.join("target/release/q8-caster");
            if binary.exists() {
                Command::new(binary)
                    .arg("--port")
                    .arg(self.api_port.to_string())
                    .spawn()
                    .context("Failed to start q8-caster")?;
            } else {
                anyhow::bail!("q8-caster binary not found. Run 'cargo build --release' in q8-caster directory");
            }
        } else {
            Command::new("bash")
                .arg(manage_script)
                .arg("start")
                .spawn()
                .context("Failed to start q8-caster via manage.sh")?;
        }

        // Wait for it to start
        tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;

        Ok(())
    }

    /// Check if q8-caster is running
    async fn is_running(&self) -> Result<bool> {
        // Try to connect to the API port
        match reqwest::get(format!("http://localhost:{}/health", self.api_port)).await {
            Ok(resp) => Ok(resp.status().is_success()),
            Err(_) => Ok(false),
        }
    }

    /// Discover available cast devices
    pub async fn discover_devices(&self) -> Result<Vec<CastDevice>> {
        self.ensure_running().await?;

        let client = reqwest::Client::new();
        let resp = client
            .get(format!("http://localhost:{}/api/devices", self.api_port))
            .send()
            .await
            .context("Failed to query devices")?;

        if !resp.status().is_success() {
            anyhow::bail!("Failed to get devices: {}", resp.status());
        }

        let devices: Vec<CastDevice> = resp
            .json()
            .await
            .context("Failed to parse devices response")?;

        Ok(devices)
    }

    /// Cast content to a specific device
    pub async fn cast_to_device(&self, device_id: &str, content: &CastContent) -> Result<()> {
        self.ensure_running().await?;

        let client = reqwest::Client::new();
        let resp = client
            .post(format!("http://localhost:{}/api/cast", self.api_port))
            .json(&CastRequest {
                device_id: device_id.to_string(),
                content: content.clone(),
            })
            .send()
            .await
            .context("Failed to cast content")?;

        if !resp.status().is_success() {
            let error_text = resp.text().await.unwrap_or_default();
            anyhow::bail!("Failed to cast: {}", error_text);
        }

        Ok(())
    }

    /// Start web dashboard
    pub async fn start_dashboard(&self, port: u16) -> Result<String> {
        self.ensure_running().await?;

        // The dashboard is served by q8-caster itself
        Ok(format!("http://localhost:{}/dashboard", port))
    }

    /// Cast to ESP32 display
    pub async fn cast_to_esp32(&self, address: &str, content: &str) -> Result<()> {
        // ESP32 devices are handled specially through q8-caster
        let esp_content = CastContent::Text {
            text: content.to_string(),
            format: "plain".to_string(),
        };

        self.cast_to_device(&format!("esp32:{}", address), &esp_content)
            .await
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CastDevice {
    pub id: String,
    pub name: String,
    pub device_type: DeviceType,
    pub address: String,
    pub capabilities: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum DeviceType {
    Chromecast,
    AppleTv,
    Miracast,
    Esp32,
    WebDashboard,
}

impl std::fmt::Display for DeviceType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            DeviceType::Chromecast => write!(f, "Chromecast"),
            DeviceType::AppleTv => write!(f, "Apple TV"),
            DeviceType::Miracast => write!(f, "Miracast"),
            DeviceType::Esp32 => write!(f, "ESP32"),
            DeviceType::WebDashboard => write!(f, "Web Dashboard"),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum CastContent {
    Text {
        text: String,
        format: String,
    },
    Html {
        html: String,
    },
    Markdown {
        markdown: String,
        theme: Option<String>,
    },
    Image {
        url: String,
    },
    Video {
        url: String,
    },
    Dashboard {
        widgets: Vec<serde_json::Value>,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct CastRequest {
    device_id: String,
    content: CastContent,
}

/// Integration with rust_shell
impl Q8CasterBridge {
    /// Convert rust_shell DisplayTarget to q8-caster device lookup
    pub async fn find_device_for_target(
        &self,
        target: &crate::rust_shell::DisplayTarget,
    ) -> Result<Option<CastDevice>> {
        let devices = self.discover_devices().await?;

        let device = match target {
            crate::rust_shell::DisplayTarget::AppleTV { name, .. } => devices
                .into_iter()
                .find(|d| d.device_type == DeviceType::AppleTv && d.name == *name),
            crate::rust_shell::DisplayTarget::Chromecast { name, .. } => devices
                .into_iter()
                .find(|d| d.device_type == DeviceType::Chromecast && d.name == *name),
            crate::rust_shell::DisplayTarget::ESP32Display { address, .. } => devices
                .into_iter()
                .find(|d| d.device_type == DeviceType::Esp32 && d.address == *address),
            _ => None,
        };

        Ok(device)
    }

    /// Adapt rust_shell content for q8-caster
    pub fn adapt_content(
        &self,
        content: &str,
        format: &crate::rust_shell::OutputFormat,
    ) -> CastContent {
        match format {
            crate::rust_shell::OutputFormat::HTML => CastContent::Html {
                html: content.to_string(),
            },
            crate::rust_shell::OutputFormat::Markdown => CastContent::Markdown {
                markdown: content.to_string(),
                theme: Some("dark".to_string()),
            },
            _ => CastContent::Text {
                text: content.to_string(),
                format: "plain".to_string(),
            },
        }
    }
}

/// Q8-Caster enhanced functionality for rust_shell
pub async fn enhance_rust_shell_with_q8(_shell: &mut crate::rust_shell::RustShell) -> Result<()> {
    println!("🚀 Enhancing Rust Shell with Q8-Caster capabilities...");

    let bridge = Q8CasterBridge::new()?;

    // Ensure q8-caster is running
    bridge.ensure_running().await?;

    // Discover and add devices
    let devices = bridge.discover_devices().await?;
    println!("  Found {} Q8-Caster devices", devices.len());

    for device in devices {
        println!(
            "  • {} ({}): {}",
            device.name, device.device_type, device.address
        );
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_q8_bridge_creation() {
        // This test will only pass if q8-caster is available
        if PathBuf::from("/aidata/ayeverse/q8-caster").exists() {
            let bridge = Q8CasterBridge::new();
            assert!(bridge.is_ok());
        }
    }
}