1use serde::{Deserialize, Serialize};
44
45#[cfg(feature = "client")]
46pub mod client;
47
48#[cfg(feature = "client")]
49pub use client::Mount;
50
51pub mod install;
52pub mod ipc;
53pub use install::{
54 ChipSpec, CommandSpec, ContextMenuEntry, IntegrationSpec, MenuBarEntry, NotificationsSpec,
55 OsNotifyPolicy, Requires, SettingsPage, StatuslineSpec, install_integration,
56 integration_manifest_path, list_installed_integrations, sibling_glyphs_dir,
57 uninstall_integration,
58};
59pub use ipc::{
60 NotifyOpts, ProgressStatus, SegmentSide, ToastLevel, notify, progress_end, progress_start,
61 progress_update, register_command, set_activity_badge, statusline_clear_segment,
62 statusline_set_segment, toast, toast_dismiss, toast_error, toast_info, toast_persistent,
63 toast_warn,
64};
65
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
69pub struct Cell {
70 pub symbol: String,
74 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub fg: Option<RgbOrIndex>,
76 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub bg: Option<RgbOrIndex>,
78 #[serde(default, skip_serializing_if = "is_zero_u16")]
80 pub modifiers: u16,
81}
82
83fn is_zero_u16(v: &u16) -> bool {
84 *v == 0
85}
86
87#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
89#[serde(untagged)]
90pub enum RgbOrIndex {
91 Rgb([u8; 3]),
93 Index(u8),
96}
97
98pub mod modifier {
101 pub const BOLD: u16 = 1 << 0;
102 pub const DIM: u16 = 1 << 1;
103 pub const ITALIC: u16 = 1 << 2;
104 pub const UNDERLINED: u16 = 1 << 3;
105 pub const SLOW_BLINK: u16 = 1 << 4;
106 pub const RAPID_BLINK: u16 = 1 << 5;
107 pub const REVERSED: u16 = 1 << 6;
108 pub const HIDDEN: u16 = 1 << 7;
109 pub const CROSSED_OUT: u16 = 1 << 8;
110}
111
112#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
114pub struct Geometry {
115 pub cols: u16,
116 pub rows: u16,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
122#[serde(tag = "kind", rename_all = "snake_case")]
123pub enum InputEvent {
124 Key { spec: String },
126 Click { col: u16, row: u16, button: String },
128 Scroll { col: u16, row: u16, dy: i16 },
130 Hover { col: u16, row: u16 },
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136#[serde(tag = "kind", rename_all = "snake_case")]
137pub enum HostMessage {
138 Hello { geometry: Geometry, theme: String },
141 Resize { geometry: Geometry },
143 Input { event: InputEvent },
145 Goodbye,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151#[serde(tag = "kind", rename_all = "snake_case")]
152pub enum SiblingMessage {
153 Frame { cells: Vec<Vec<Cell>> },
158 Bye,
160}
161
162pub fn read_message<R, T>(r: &mut R) -> std::io::Result<Option<T>>
168where
169 R: std::io::Read,
170 T: serde::de::DeserializeOwned,
171{
172 let mut len_buf = [0u8; 4];
173 match r.read_exact(&mut len_buf) {
174 Ok(()) => {}
175 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
176 Err(e) => return Err(e),
177 }
178 let len = u32::from_le_bytes(len_buf) as usize;
179 if len > 16 * 1024 * 1024 {
180 return Err(std::io::Error::new(
181 std::io::ErrorKind::InvalidData,
182 format!("bridge message too large: {len} bytes"),
183 ));
184 }
185 let mut body = vec![0u8; len];
186 r.read_exact(&mut body)?;
187 let parsed: T = serde_json::from_slice(&body).map_err(|e| {
188 std::io::Error::new(
189 std::io::ErrorKind::InvalidData,
190 format!("bridge JSON parse: {e}"),
191 )
192 })?;
193 Ok(Some(parsed))
194}
195
196pub fn write_message<W, T>(w: &mut W, msg: &T) -> std::io::Result<()>
198where
199 W: std::io::Write,
200 T: Serialize,
201{
202 let body = serde_json::to_vec(msg).map_err(|e| {
203 std::io::Error::new(
204 std::io::ErrorKind::InvalidData,
205 format!("bridge JSON serialize: {e}"),
206 )
207 })?;
208 let len = body.len() as u32;
209 w.write_all(&len.to_le_bytes())?;
210 w.write_all(&body)?;
211 Ok(())
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn frame_roundtrip() {
220 let frame = SiblingMessage::Frame {
221 cells: vec![vec![Cell {
222 symbol: "x".to_string(),
223 fg: Some(RgbOrIndex::Rgb([255, 0, 0])),
224 bg: None,
225 modifiers: modifier::BOLD,
226 }]],
227 };
228 let mut buf = Vec::new();
229 write_message(&mut buf, &frame).unwrap();
230 let mut cursor = std::io::Cursor::new(&buf);
231 let back: SiblingMessage = read_message(&mut cursor).unwrap().unwrap();
232 match back {
233 SiblingMessage::Frame { cells } => {
234 assert_eq!(cells.len(), 1);
235 assert_eq!(cells[0][0].symbol, "x");
236 assert_eq!(cells[0][0].fg, Some(RgbOrIndex::Rgb([255, 0, 0])));
237 assert_eq!(cells[0][0].modifiers, modifier::BOLD);
238 }
239 _ => panic!("wrong variant"),
240 }
241 }
242
243 #[test]
244 fn host_hello_roundtrip() {
245 let hello = HostMessage::Hello {
246 geometry: Geometry { cols: 80, rows: 24 },
247 theme: "cyberdream".to_string(),
248 };
249 let mut buf = Vec::new();
250 write_message(&mut buf, &hello).unwrap();
251 let mut cursor = std::io::Cursor::new(&buf);
252 let back: HostMessage = read_message(&mut cursor).unwrap().unwrap();
253 match back {
254 HostMessage::Hello { geometry, theme } => {
255 assert_eq!(geometry.cols, 80);
256 assert_eq!(geometry.rows, 24);
257 assert_eq!(theme, "cyberdream");
258 }
259 _ => panic!("wrong variant"),
260 }
261 }
262
263 #[test]
264 fn eof_returns_none() {
265 let mut empty = std::io::Cursor::new(Vec::<u8>::new());
266 let res: Option<HostMessage> = read_message(&mut empty).unwrap();
267 assert!(res.is_none());
268 }
269
270 #[test]
271 fn rejects_oversize_length() {
272 let mut buf = (100u32 * 1024 * 1024).to_le_bytes().to_vec();
274 buf.extend_from_slice(b"junk");
275 let mut cursor = std::io::Cursor::new(&buf);
276 let res: std::io::Result<Option<HostMessage>> = read_message(&mut cursor);
277 assert!(res.is_err());
278 }
279}