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#[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 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 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 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; }
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 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 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 #[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(), 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(), elements: self.elements,
157 }
158 }
159 }
160}