use std::{collections::BTreeMap, fmt};
use base64::Engine as _;
use serde::{Deserialize, Serialize, Serializer, de::DeserializeOwned, ser::SerializeSeq};
use serde_json::{Map, Value};
use sha2::{Digest as _, Sha256};
use crate::Error;
pub const VERSION: u32 = 1;
pub const APC_LIMIT: usize = 65_536;
pub const CREDITS: u32 = 2;
pub const MAX_BLOB: usize = 16 << 20;
pub mod kind {
pub const COL: &str = "col";
pub const ROW: &str = "row";
pub const CARD: &str = "card";
pub const SECTION: &str = "section";
pub const RULE: &str = "rule";
pub const SPACER: &str = "spacer";
pub const TEXT: &str = "text";
pub const MD: &str = "md";
pub const CODE: &str = "code";
pub const DIFF: &str = "diff";
pub const ANSI: &str = "ansi";
pub const ROWS: &str = "rows";
pub const MATH: &str = "math";
pub const KV: &str = "kv";
pub const TABLE: &str = "table";
pub const TREE: &str = "tree";
pub const BADGE: &str = "badge";
pub const KBD: &str = "kbd";
pub const ICON: &str = "icon";
pub const IMAGE: &str = "image";
pub const LIST: &str = "list";
pub const ITEM: &str = "item";
pub const TABS: &str = "tabs";
pub const PICKER: &str = "picker";
pub const SPINNER: &str = "spinner";
pub const SHIMMER: &str = "shimmer";
pub const ELAPSED: &str = "elapsed";
pub const RATE: &str = "rate";
pub const PROGRESS: &str = "progress";
pub const METER: &str = "meter";
pub const CHART: &str = "chart";
pub const EFFORT: &str = "effort";
pub const EDITOR: &str = "editor";
pub const INPUT: &str = "input";
pub const STATUS: &str = "status";
pub const SEG: &str = "seg";
pub const TOAST: &str = "toast";
pub const OVERLAY: &str = "overlay";
pub const TOOL: &str = "tool";
pub const AGENT: &str = "agent";
pub const CHECKLIST: &str = "checklist";
pub const BLOCK: &str = "block";
pub const PREFS: &str = "prefs";
pub const EL: &str = "el";
pub const ALL: [&str; 44] = [
COL, ROW, CARD, SECTION, RULE, SPACER, TEXT, MD, CODE, DIFF, ANSI, ROWS, MATH, KV, TABLE,
TREE, BADGE, KBD, ICON, IMAGE, LIST, ITEM, TABS, PICKER, SPINNER, SHIMMER, ELAPSED, RATE,
PROGRESS, METER, CHART, EFFORT, EDITOR, INPUT, STATUS, SEG, TOAST, OVERLAY, TOOL, AGENT,
CHECKLIST, BLOCK, PREFS, EL,
];
pub const TEXT_KINDS: [&str; 9] = [TEXT, MD, CODE, ANSI, MATH, EDITOR, INPUT, SHIMMER, EL];
pub fn has_text(kind: &str) -> bool {
TEXT_KINDS.contains(&kind)
}
}
pub mod feature {
pub const EDIT: &str = "edit";
pub const UNDO: &str = "undo";
pub const SEND: &str = "send";
pub const BLOBS: &str = "blobs";
pub const SETTLE: &str = "settle";
pub const ADOPT: &str = "adopt";
pub const DOCK: &str = "dock";
pub const PROGRAM_PALETTE: &str = "program-palette";
pub const REDUCE_MOTION: &str = "reduce-motion";
pub const ASIDE: &str = "aside";
pub const SCROLL: &str = "scroll";
pub const STYLES: &str = "styles";
pub const FLOW: &str = "flow";
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Mode {
#[default]
Inline,
Screen,
Flow,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Query {
Hello {
app: Option<String>,
ver: Option<String>,
features: Vec<String>,
},
Blobs {
ids: Vec<String>,
},
}
impl Serialize for Query {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap as _;
match self {
Self::Hello { app, ver, features } => {
let mut m = s.serialize_map(None)?;
m.serialize_entry("q", "hello")?;
m.serialize_entry("v", &[VERSION])?;
if let Some(app) = app {
m.serialize_entry("app", app)?;
}
if let Some(ver) = ver {
m.serialize_entry("ver", ver)?;
}
if !features.is_empty() {
m.serialize_entry("features", features)?;
}
m.end()
},
Self::Blobs { ids } => {
let mut m = s.serialize_map(Some(2))?;
m.serialize_entry("q", "blobs")?;
m.serialize_entry("ids", ids)?;
m.end()
},
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct Open {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub mode: Option<Mode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub listen: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub adopt: Option<bool>,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Frame {
pub sf: String,
pub s: u64,
pub ops: Vec<Op>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum TextMode {
Append,
Replace,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum RevealAt {
Start,
End,
#[default]
Nearest,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ScrollBy {
LineUp,
LineDown,
PageUp,
PageDown,
Start,
End,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct WireNode {
pub id: String,
pub k: String,
#[serde(default, skip_serializing_if = "Map::is_empty")]
pub p: Map<String, Value>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub c: Vec<Self>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Op {
Add {
parent: String,
before: Option<String>,
node: WireNode,
},
Set {
id: String,
props: Map<String, Value>,
},
Text {
id: String,
mode: TextMode,
text: String,
},
Splice {
id: String,
at: usize,
del: usize,
text: String,
},
Move {
id: String,
parent: String,
before: Option<String>,
},
Del {
id: String,
},
Settle {
id: String,
},
Focus {
id: Option<String>,
},
Reveal {
id: String,
at: RevealAt,
},
Scroll {
id: String,
by: ScrollBy,
},
Suspend,
Resume,
}
impl Serialize for Op {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
let mut seq = s.serialize_seq(None)?;
match self {
Self::Add { parent, before, node } => {
seq.serialize_element("add")?;
seq.serialize_element(&node.id)?;
seq.serialize_element(parent)?;
seq.serialize_element(before)?;
seq.serialize_element(node)?;
},
Self::Set { id, props } => {
seq.serialize_element("set")?;
seq.serialize_element(id)?;
seq.serialize_element(props)?;
},
Self::Text { id, mode, text } => {
seq.serialize_element("text")?;
seq.serialize_element(id)?;
seq.serialize_element(mode)?;
seq.serialize_element(text)?;
},
Self::Splice { id, at, del, text } => {
seq.serialize_element("splice")?;
seq.serialize_element(id)?;
seq.serialize_element(at)?;
seq.serialize_element(del)?;
seq.serialize_element(text)?;
},
Self::Move { id, parent, before } => {
seq.serialize_element("move")?;
seq.serialize_element(id)?;
seq.serialize_element(parent)?;
seq.serialize_element(before)?;
},
Self::Del { id } => {
seq.serialize_element("del")?;
seq.serialize_element(id)?;
},
Self::Settle { id } => {
seq.serialize_element("settle")?;
seq.serialize_element(id)?;
},
Self::Focus { id } => {
seq.serialize_element("focus")?;
seq.serialize_element(id)?;
},
Self::Reveal { id, at } => {
seq.serialize_element("reveal")?;
seq.serialize_element(id)?;
seq.serialize_element(at)?;
},
Self::Scroll { id, by } => {
seq.serialize_element("scroll")?;
seq.serialize_element(id)?;
seq.serialize_element(by)?;
},
Self::Suspend => seq.serialize_element("suspend")?,
Self::Resume => seq.serialize_element("resume")?,
}
seq.end()
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct PaletteNames {
#[serde(skip_serializing_if = "Option::is_none")]
pub dark: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub light: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct Palette {
#[serde(skip_serializing_if = "Option::is_none")]
pub sf: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dark: Option<BTreeMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub light: Option<BTreeMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<PaletteNames>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct Sheet {
#[serde(skip_serializing_if = "Option::is_none")]
pub sf: Option<String>,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub css: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct Close {
pub id: String,
pub keep: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Blob {
pub id: String,
pub mime: Option<String>,
pub data: Vec<u8>,
}
impl Blob {
pub fn new(data: Vec<u8>, mime: Option<String>) -> Result<Self, Error> {
if data.len() > MAX_BLOB {
return Err(Error::BlobTooLarge(data.len()));
}
Ok(Self { id: blob_id(&data), mime, data })
}
}
pub fn blob_id(data: &[u8]) -> String {
use fmt::Write as _;
let digest = Sha256::digest(data);
let mut out = String::with_capacity(64);
for b in digest {
let _ = write!(out, "{b:02x}");
}
out
}
#[derive(Clone, Debug, PartialEq)]
pub enum Message {
Query(Query),
Open(Open),
Frame(Frame),
Blob(Blob),
Palette(Palette),
Sheet(Sheet),
Close(Close),
}
impl Message {
pub const fn verb(&self) -> &'static str {
match self {
Self::Query(_) => "q",
Self::Open(_) => "o",
Self::Frame(_) => "f",
Self::Blob(_) => "b",
Self::Palette(_) => "t",
Self::Sheet(_) => "s",
Self::Close(_) => "x",
}
}
pub fn params(&self) -> Vec<(&'static str, &str)> {
match self {
Self::Blob(b) => {
let mut out = vec![("id", b.id.as_str())];
if let Some(mime) = &b.mime {
out.push(("mime", mime.as_str()));
}
out
},
_ => Vec::new(),
}
}
pub fn body(&self) -> Result<String, Error> {
Ok(match self {
Self::Query(m) => serde_json::to_string(m)?,
Self::Open(m) => serde_json::to_string(m)?,
Self::Frame(m) => serde_json::to_string(m)?,
Self::Blob(b) => base64::engine::general_purpose::STANDARD.encode(&b.data),
Self::Palette(m) => serde_json::to_string(m)?,
Self::Sheet(m) => serde_json::to_string(m)?,
Self::Close(m) => serde_json::to_string(m)?,
})
}
}
const fn key_byte(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_' || b == b'-'
}
const fn value_byte(b: u8) -> bool {
b >= 0x21 && b <= 0x7e && b != b';'
}
fn leads_with_parameter(rest: &[u8]) -> bool {
let key = rest.iter().take_while(|&&b| key_byte(b)).count();
if key == 0 || rest.get(key) != Some(&b'=') {
return false;
}
rest[key + 1..].iter().find(|&&b| !value_byte(b)) == Some(&b';')
}
fn chunk_end(body: &[u8], start: usize, limit: usize) -> usize {
let target = start.saturating_add(limit.max(1));
if target >= body.len() {
return body.len();
}
let safe = |at: usize| body[at] & 0xc0 != 0x80 && !leads_with_parameter(&body[at..]);
(start + 1..=target)
.rev()
.find(|&at| safe(at))
.or_else(|| (target + 1..body.len()).find(|&at| safe(at)))
.unwrap_or(body.len())
}
pub fn chunks(body: &[u8], limit: usize) -> impl Iterator<Item = &[u8]> {
let mut start = 0;
std::iter::from_fn(move || {
(start < body.len()).then(|| {
let end = chunk_end(body, start, limit);
let piece = &body[start..end];
start = end;
piece
})
})
}
fn push_apc(out: &mut Vec<u8>, verb: &str, params: &[(&str, &str)], body: &[u8]) {
out.extend_from_slice(b"\x1b_tsp;");
out.extend_from_slice(verb.as_bytes());
out.push(b';');
for (k, v) in params {
out.extend_from_slice(k.as_bytes());
out.push(b'=');
out.extend_from_slice(v.as_bytes());
out.push(b';');
}
out.extend_from_slice(body);
out.extend_from_slice(b"\x1b\\");
}
pub fn encode(
verb: &str,
params: &[(&str, &str)],
body: &[u8],
limit: usize,
chunk: &str,
) -> Vec<u8> {
let mut out = Vec::with_capacity(body.len() + 16);
if body.len() <= limit {
push_apc(&mut out, verb, params, body);
return out;
}
let pieces: Vec<&[u8]> = chunks(body, limit).collect();
let last = pieces.len() - 1;
for (i, piece) in pieces.into_iter().enumerate() {
let mut ps: Vec<(&str, &str)> = if i == 0 { params.to_vec() } else { Vec::new() };
ps.push(("c", chunk));
if i < last {
ps.push(("m", "1"));
}
push_apc(&mut out, verb, &ps, piece);
}
out
}
#[derive(Clone, Debug)]
pub struct Encoder {
limit: usize,
next_chunk: u64,
}
impl Default for Encoder {
fn default() -> Self {
Self::new(APC_LIMIT)
}
}
impl Encoder {
pub const fn new(limit: usize) -> Self {
Self { limit, next_chunk: 1 }
}
pub const fn limit(&self) -> usize {
self.limit
}
pub const fn set_limit(&mut self, limit: usize) {
self.limit = limit;
}
pub fn encode(&mut self, message: &Message) -> Result<Vec<u8>, Error> {
let body = message.body()?;
let chunk = if body.len() > self.limit {
let id = base36(self.next_chunk);
self.next_chunk += 1;
id
} else {
String::new()
};
Ok(encode(message.verb(), &message.params(), body.as_bytes(), self.limit, &chunk))
}
}
fn base36(mut n: u64) -> String {
const DIGITS: &[u8; 36] = b"0123456789abcdefghijklmnopqrstuvwxyz";
let mut out = Vec::new();
loop {
out.push(DIGITS[(n % 36) as usize]);
n /= 36;
if n == 0 {
break;
}
}
out.reverse();
String::from_utf8(out).unwrap_or_default()
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Cell {
#[serde(default)]
pub w: f64,
#[serde(default)]
pub h: f64,
}
#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Hello {
#[serde(default)]
pub v: u32,
#[serde(default)]
pub term: String,
#[serde(default)]
pub ver: String,
#[serde(default)]
pub kinds: Vec<String>,
#[serde(default)]
pub features: Vec<String>,
#[serde(default = "default_apc")]
pub apc: usize,
#[serde(default = "default_credits")]
pub credits: u32,
#[serde(default)]
pub cols: u32,
#[serde(default)]
pub cell: Option<Cell>,
#[serde(default)]
pub dark: bool,
#[serde(default)]
pub reduce_motion: bool,
#[serde(default)]
pub hour12: bool,
#[serde(skip)]
pub raw: Map<String, Value>,
}
const fn default_apc() -> usize {
APC_LIMIT
}
const fn default_credits() -> u32 {
CREDITS
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
pub struct BlobsReply {
#[serde(default)]
pub have: Vec<String>,
#[serde(skip)]
pub raw: Map<String, Value>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Reply {
Hello(Hello),
Blobs(BlobsReply),
Unknown(Map<String, Value>),
}
trait Raw: DeserializeOwned {
fn set_raw(&mut self, raw: Map<String, Value>);
}
fn typed<T: Raw>(map: Map<String, Value>) -> Result<T, Map<String, Value>> {
let value = Value::Object(map);
let decoded = T::deserialize(&value);
let Value::Object(map) = value else {
unreachable!("built as an object")
};
match decoded {
Ok(mut t) => {
t.set_raw(map);
Ok(t)
},
Err(_) => Err(map),
}
}
impl Reply {
pub fn decode(map: Map<String, Value>) -> Self {
match map.get("r").and_then(Value::as_str) {
Some("hello") => typed(map).map_or_else(Self::Unknown, Self::Hello),
Some("blobs") => typed(map).map_or_else(Self::Unknown, Self::Blobs),
_ => Self::Unknown(map),
}
}
pub const fn raw(&self) -> &Map<String, Value> {
match self {
Self::Hello(r) => &r.raw,
Self::Blobs(r) => &r.raw,
Self::Unknown(r) => r,
}
}
}
macro_rules! event_struct {
($(#[$m:meta])* $name:ident { $($(#[$fm:meta])* $field:ident : $ty:ty),* $(,)? }) => {
$(#[$m])*
#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
#[allow(clippy::derive_partial_eq_without_eq, reason = "some payloads hold floats")]
pub struct $name {
$($(#[$fm])* #[serde(default)] pub $field: $ty,)*
#[serde(skip)]
pub raw: Map<String, Value>,
}
impl Raw for $name {
fn set_raw(&mut self, raw: Map<String, Value>) {
self.raw = raw;
}
}
};
}
impl Raw for Hello {
fn set_raw(&mut self, raw: Map<String, Value>) {
self.raw = raw;
}
}
impl Raw for BlobsReply {
fn set_raw(&mut self, raw: Map<String, Value>) {
self.raw = raw;
}
}
event_struct!(
Ack {
sf: String,
s: u64,
}
);
event_struct!(
Resize {
sf: Option<String>,
cols: u32,
cell: Option<Cell>,
visible: Option<bool>,
}
);
event_struct!(
Theme {
dark: bool,
}
);
event_struct!(
Motion {
reduce: bool,
}
);
event_struct!(
Visible {
sf: Option<String>,
visible: bool,
}
);
event_struct!(
Toggle {
sf: Option<String>,
id: String,
collapsed: bool,
key: Option<Value>,
}
);
event_struct!(
Select {
sf: Option<String>,
id: String,
item: String,
values: Option<Map<String, Value>>,
}
);
event_struct!(
Action {
sf: Option<String>,
id: String,
act: String,
value: Option<String>,
mods: Vec<String>,
values: Option<Map<String, Value>>,
}
);
event_struct!(
Change {
sf: Option<String>,
id: String,
value: Value,
checked: Option<bool>,
name: Option<String>,
item: Option<String>,
values: Option<Map<String, Value>>,
}
);
event_struct!(
Focus {
sf: Option<String>,
id: String,
}
);
event_struct!(
Edit {
sf: Option<String>,
id: String,
from: usize,
to: usize,
text: String,
cursor: usize,
len: usize,
}
);
event_struct!(
Undo {
sf: Option<String>,
id: String,
}
);
event_struct!(
SendText {
sf: Option<String>,
id: String,
text: String,
}
);
event_struct!(
ErrorEvent {
sf: Option<String>,
s: Option<u64>,
op: Option<u64>,
msg: String,
sheet: Option<String>,
id: Option<String>,
}
);
event_struct!(
Gone {
sf: Option<String>,
ids: Vec<String>,
}
);
#[derive(Clone, Debug, PartialEq)]
pub enum Event {
Ack(Ack),
Resize(Resize),
Theme(Theme),
Motion(Motion),
Visible(Visible),
Toggle(Toggle),
Select(Select),
Activate(Select),
Action(Action),
Change(Change),
Focus(Focus),
Edit(Edit),
Undo(Undo),
Send(SendText),
Error(ErrorEvent),
Gone(Gone),
Unknown(Map<String, Value>),
}
impl Event {
pub fn decode(map: Map<String, Value>) -> Self {
let ev = map
.get("ev")
.and_then(Value::as_str)
.unwrap_or_default()
.to_owned();
match ev.as_str() {
"ack" => typed(map).map_or_else(Self::Unknown, Self::Ack),
"resize" => typed(map).map_or_else(Self::Unknown, Self::Resize),
"theme" => typed(map).map_or_else(Self::Unknown, Self::Theme),
"motion" => typed(map).map_or_else(Self::Unknown, Self::Motion),
"visible" => typed(map).map_or_else(Self::Unknown, Self::Visible),
"toggle" => typed(map).map_or_else(Self::Unknown, Self::Toggle),
"select" => typed(map).map_or_else(Self::Unknown, Self::Select),
"activate" => typed(map).map_or_else(Self::Unknown, Self::Activate),
"action" => typed(map).map_or_else(Self::Unknown, Self::Action),
"change" => typed(map).map_or_else(Self::Unknown, Self::Change),
"focus" => typed(map).map_or_else(Self::Unknown, Self::Focus),
"edit" => typed(map).map_or_else(Self::Unknown, Self::Edit),
"undo" => typed(map).map_or_else(Self::Unknown, Self::Undo),
"send" => typed(map).map_or_else(Self::Unknown, Self::Send),
"error" => typed(map).map_or_else(Self::Unknown, Self::Error),
"gone" => typed(map).map_or_else(Self::Unknown, Self::Gone),
_ => Self::Unknown(map),
}
}
pub const fn raw(&self) -> &Map<String, Value> {
match self {
Self::Ack(e) => &e.raw,
Self::Resize(e) => &e.raw,
Self::Theme(e) => &e.raw,
Self::Motion(e) => &e.raw,
Self::Visible(e) => &e.raw,
Self::Toggle(e) => &e.raw,
Self::Select(e) | Self::Activate(e) => &e.raw,
Self::Action(e) => &e.raw,
Self::Change(e) => &e.raw,
Self::Focus(e) => &e.raw,
Self::Edit(e) => &e.raw,
Self::Undo(e) => &e.raw,
Self::Send(e) => &e.raw,
Self::Error(e) => &e.raw,
Self::Gone(e) => &e.raw,
Self::Unknown(e) => e,
}
}
pub fn name(&self) -> &str {
self
.raw()
.get("ev")
.and_then(Value::as_str)
.unwrap_or_default()
}
pub fn sf(&self) -> Option<&str> {
self.raw().get("sf").and_then(Value::as_str)
}
pub fn id(&self) -> Option<&str> {
self.raw().get("id").and_then(Value::as_str)
}
}