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