use serde::{Deserialize, Serialize};
#[cfg(feature = "client")]
pub mod client;
#[cfg(feature = "client")]
pub use client::Mount;
pub mod install;
pub mod ipc;
pub use install::{
ChipSpec, CommandSpec, ContextMenuEntry, IntegrationSpec, MenuBarEntry, NotificationsSpec,
OsNotifyPolicy, Requires, SettingsPage, StatuslineSpec, install_integration,
integration_manifest_path, list_installed_integrations, uninstall_integration,
};
pub use ipc::{
NotifyOpts, ProgressStatus, SegmentSide, ToastLevel, notify, progress_end, progress_start,
progress_update, register_command, set_activity_badge, statusline_clear_segment,
statusline_set_segment, toast, toast_dismiss, toast_error, toast_info, toast_persistent,
toast_warn,
};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Cell {
pub symbol: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fg: Option<RgbOrIndex>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bg: Option<RgbOrIndex>,
#[serde(default, skip_serializing_if = "is_zero_u16")]
pub modifiers: u16,
}
fn is_zero_u16(v: &u16) -> bool {
*v == 0
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum RgbOrIndex {
Rgb([u8; 3]),
Index(u8),
}
pub mod modifier {
pub const BOLD: u16 = 1 << 0;
pub const DIM: u16 = 1 << 1;
pub const ITALIC: u16 = 1 << 2;
pub const UNDERLINED: u16 = 1 << 3;
pub const SLOW_BLINK: u16 = 1 << 4;
pub const RAPID_BLINK: u16 = 1 << 5;
pub const REVERSED: u16 = 1 << 6;
pub const HIDDEN: u16 = 1 << 7;
pub const CROSSED_OUT: u16 = 1 << 8;
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct Geometry {
pub cols: u16,
pub rows: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum InputEvent {
Key { spec: String },
Click { col: u16, row: u16, button: String },
Scroll { col: u16, row: u16, dy: i16 },
Hover { col: u16, row: u16 },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum HostMessage {
Hello { geometry: Geometry, theme: String },
Resize { geometry: Geometry },
Input { event: InputEvent },
Goodbye,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum SiblingMessage {
Frame { cells: Vec<Vec<Cell>> },
Bye,
}
pub fn read_message<R, T>(r: &mut R) -> std::io::Result<Option<T>>
where
R: std::io::Read,
T: serde::de::DeserializeOwned,
{
let mut len_buf = [0u8; 4];
match r.read_exact(&mut len_buf) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
Err(e) => return Err(e),
}
let len = u32::from_le_bytes(len_buf) as usize;
if len > 16 * 1024 * 1024 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("bridge message too large: {len} bytes"),
));
}
let mut body = vec![0u8; len];
r.read_exact(&mut body)?;
let parsed: T = serde_json::from_slice(&body).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("bridge JSON parse: {e}"),
)
})?;
Ok(Some(parsed))
}
pub fn write_message<W, T>(w: &mut W, msg: &T) -> std::io::Result<()>
where
W: std::io::Write,
T: Serialize,
{
let body = serde_json::to_vec(msg).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("bridge JSON serialize: {e}"),
)
})?;
let len = body.len() as u32;
w.write_all(&len.to_le_bytes())?;
w.write_all(&body)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frame_roundtrip() {
let frame = SiblingMessage::Frame {
cells: vec![vec![Cell {
symbol: "x".to_string(),
fg: Some(RgbOrIndex::Rgb([255, 0, 0])),
bg: None,
modifiers: modifier::BOLD,
}]],
};
let mut buf = Vec::new();
write_message(&mut buf, &frame).unwrap();
let mut cursor = std::io::Cursor::new(&buf);
let back: SiblingMessage = read_message(&mut cursor).unwrap().unwrap();
match back {
SiblingMessage::Frame { cells } => {
assert_eq!(cells.len(), 1);
assert_eq!(cells[0][0].symbol, "x");
assert_eq!(cells[0][0].fg, Some(RgbOrIndex::Rgb([255, 0, 0])));
assert_eq!(cells[0][0].modifiers, modifier::BOLD);
}
_ => panic!("wrong variant"),
}
}
#[test]
fn host_hello_roundtrip() {
let hello = HostMessage::Hello {
geometry: Geometry { cols: 80, rows: 24 },
theme: "cyberdream".to_string(),
};
let mut buf = Vec::new();
write_message(&mut buf, &hello).unwrap();
let mut cursor = std::io::Cursor::new(&buf);
let back: HostMessage = read_message(&mut cursor).unwrap().unwrap();
match back {
HostMessage::Hello { geometry, theme } => {
assert_eq!(geometry.cols, 80);
assert_eq!(geometry.rows, 24);
assert_eq!(theme, "cyberdream");
}
_ => panic!("wrong variant"),
}
}
#[test]
fn eof_returns_none() {
let mut empty = std::io::Cursor::new(Vec::<u8>::new());
let res: Option<HostMessage> = read_message(&mut empty).unwrap();
assert!(res.is_none());
}
#[test]
fn rejects_oversize_length() {
let mut buf = (100u32 * 1024 * 1024).to_le_bytes().to_vec();
buf.extend_from_slice(b"junk");
let mut cursor = std::io::Cursor::new(&buf);
let res: std::io::Result<Option<HostMessage>> = read_message(&mut cursor);
assert!(res.is_err());
}
}