scarab_nav_client/
lib.rs

1use prost::Message;
2use std::path::PathBuf;
3use tokio::io::AsyncWriteExt;
4use tokio::net::UnixStream;
5use tokio::sync::mpsc;
6use tracing::{debug, error, warn};
7
8pub use scarab_nav_protocol::v1;
9use scarab_nav_protocol::v1::{ElementType, InteractiveElement, UpdateLayout};
10
11#[derive(Debug, thiserror::Error)]
12pub enum NavError {
13    #[error("IO Error: {0}")]
14    Io(#[from] std::io::Error),
15    #[error("Protobuf Error: {0}")]
16    Prost(#[from] prost::EncodeError),
17    #[error("Channel Closed")]
18    ChannelClosed,
19}
20
21/// A client for the Scarab Navigation Protocol.
22/// It handles connection management and message buffering in a background task.
23#[derive(Clone)]
24pub struct NavClient {
25    sender: mpsc::UnboundedSender<UpdateLayout>,
26}
27
28impl Default for NavClient {
29    fn default() -> Self {
30        Self::new()
31    }
32}
33
34impl NavClient {
35    /// Create a new NavClient.
36    /// It attempts to connect to the socket at `SCARAB_NAV_SOCKET`.
37    /// If the env var is not set, it logs a warning and returns a dummy client (no-op).
38    pub fn new() -> Self {
39        let (tx, mut rx) = mpsc::unbounded_channel::<UpdateLayout>();
40
41        let socket_path = std::env::var("SCARAB_NAV_SOCKET").map(PathBuf::from).ok();
42
43        if let Some(path) = socket_path {
44            tokio::spawn(async move {
45                let mut stream = match UnixStream::connect(&path).await {
46                    Ok(s) => s,
47                    Err(e) => {
48                        warn!(
49                            "Failed to connect to Scarab Nav Socket at {:?}: {}",
50                            path, e
51                        );
52                        // In a real implementation, we might want a retry loop here
53                        return;
54                    }
55                };
56
57                debug!("Connected to Scarab Nav Socket at {:?}", path);
58
59                while let Some(msg) = rx.recv().await {
60                    let mut buf = Vec::new();
61                    // Simple length-prefixed framing? Or just raw protobuf?
62                    // For a Unix socket, we usually need some framing if we send multiple messages.
63                    // Let's use a simple u32 length prefix (Little Endian).
64
65                    if let Err(e) = msg.encode(&mut buf) {
66                        error!("Failed to encode UpdateLayout: {}", e);
67                        continue;
68                    }
69
70                    let len = buf.len() as u32;
71                    let len_bytes = len.to_le_bytes();
72
73                    if let Err(e) = stream.write_all(&len_bytes).await {
74                        error!("Failed to write message length: {}", e);
75                        break; // Connection likely broken
76                    }
77
78                    if let Err(e) = stream.write_all(&buf).await {
79                        error!("Failed to write message body: {}", e);
80                        break;
81                    }
82                }
83            });
84        } else {
85            debug!("SCARAB_NAV_SOCKET not set. Navigation updates will be ignored.");
86            // Consume the channel to prevent memory leak if the user keeps sending
87            tokio::spawn(async move { while (rx.recv().await).is_some() {} });
88        }
89
90        Self { sender: tx }
91    }
92
93    pub fn update(&self, layout: UpdateLayout) {
94        let _ = self.sender.send(layout);
95    }
96}
97
98#[cfg(feature = "ratatui")]
99pub mod ratatui_helper {
100    use super::*;
101    // We don't import ratatui::layout::Rect to avoid version conflicts.
102    // Users pass coordinates explicitly.
103
104    /// A helper to build UpdateLayout messages.
105    pub struct NavRecorder {
106        elements: Vec<InteractiveElement>,
107    }
108
109    impl Default for NavRecorder {
110        fn default() -> Self {
111            Self::new()
112        }
113    }
114
115    impl NavRecorder {
116        pub fn new() -> Self {
117            Self {
118                elements: Vec::new(),
119            }
120        }
121
122        /// Register an interactive element with its position and type.
123        ///
124        /// # Arguments
125        /// * `x`, `y` - Top-left position of the element
126        /// * `width`, `height` - Size of the element
127        /// * `element_type` - Type of interactive element
128        /// * `description` - Human-readable description
129        /// * `key_hint` - Optional keyboard shortcut hint
130        #[allow(clippy::too_many_arguments)]
131        pub fn register(
132            &mut self,
133            x: u16,
134            y: u16,
135            width: u16,
136            height: u16,
137            element_type: ElementType,
138            description: impl Into<String>,
139            key_hint: Option<String>,
140        ) {
141            self.elements.push(InteractiveElement {
142                id: uuid::Uuid::new_v4().to_string(), // Random ID for now
143                x: x as u32,
144                y: y as u32,
145                width: width as u32,
146                height: height as u32,
147                r#type: element_type as i32,
148                description: description.into(),
149                key_hint: key_hint.unwrap_or_default(),
150            });
151        }
152
153        pub fn finish(self) -> UpdateLayout {
154            UpdateLayout {
155                window_id: std::process::id().to_string(), // Use PID as window ID for simple correlation
156                elements: self.elements,
157            }
158        }
159    }
160}