use std::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 STATS: &str = "stats";
pub const TABLE: &str = "table";
pub const TREE: &str = "tree";
pub const JSON: &str = "json";
pub const BADGE: &str = "badge";
pub const KBD: &str = "kbd";
pub const ICON: &str = "icon";
pub const IMAGE: &str = "image";
pub const MEDIA: &str = "media";
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 BUTTON: &str = "button";
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 SELECT: &str = "select";
pub const SWITCH: &str = "switch";
pub const SLIDER: &str = "slider";
pub const FIELD: &str = "field";
pub const OTP: &str = "otp";
pub const BUTTONS: &str = "buttons";
pub const AVATAR: &str = "avatar";
pub const AVATARS: &str = "avatars";
pub const BREADCRUMB: &str = "breadcrumb";
pub const PAGER: &str = "pager";
pub const EMPTY: &str = "empty";
pub const SKELETON: &str = "skeleton";
pub const CALENDAR: &str = "calendar";
pub const DATE: &str = "date";
pub const MENU: &str = "menu";
pub const MENUBAR: &str = "menubar";
pub const NAV: &str = "nav";
pub const SPLIT: &str = "split";
pub const SCROLL: &str = "scroll";
pub const CAROUSEL: &str = "carousel";
pub const MESSAGE: &str = "message";
pub const ATTACHMENT: &str = "attachment";
pub const TRACE: &str = "trace";
pub const STACKTRACE: &str = "stacktrace";
pub const SUGGESTIONS: &str = "suggestions";
pub const QUESTIONS: &str = "questions";
pub const EL: &str = "el";
pub const ALL: [&str; 74] = [
COL,
ROW,
CARD,
SECTION,
RULE,
SPACER,
TEXT,
MD,
CODE,
DIFF,
ANSI,
ROWS,
MATH,
KV,
STATS,
TABLE,
TREE,
JSON,
BADGE,
KBD,
ICON,
IMAGE,
MEDIA,
LIST,
ITEM,
TABS,
PICKER,
SPINNER,
SHIMMER,
ELAPSED,
RATE,
PROGRESS,
METER,
CHART,
EFFORT,
EDITOR,
INPUT,
BUTTON,
STATUS,
SEG,
TOAST,
OVERLAY,
TOOL,
AGENT,
CHECKLIST,
BLOCK,
PREFS,
SELECT,
SWITCH,
SLIDER,
FIELD,
OTP,
BUTTONS,
AVATAR,
AVATARS,
BREADCRUMB,
PAGER,
EMPTY,
SKELETON,
CALENDAR,
DATE,
MENU,
MENUBAR,
NAV,
SPLIT,
SCROLL,
CAROUSEL,
MESSAGE,
ATTACHMENT,
TRACE,
STACKTRACE,
SUGGESTIONS,
QUESTIONS,
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 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";
pub const FONT: &str = "font";
}
#[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>,
#[serde(skip_serializing_if = "Option::is_none")]
pub closable: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub font: Option<Font>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Font {
#[default]
Mono,
Sans,
}
#[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 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),
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::Sheet(_) => "s",
Self::Close(_) => "x",
}
}
pub fn params(&self) -> Vec<(&'static str, &str)> {
match self {
Self::Blob(b) => b.mime.iter().map(|mime| ("mime", mime.as_str())).collect(),
_ => 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::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 string_or_number<'de, D: serde::Deserializer<'de>>(d: D) -> Result<String, D::Error> {
match Value::deserialize(d)? {
Value::String(s) => Ok(s),
Value::Number(n) => Ok(n.to_string()),
_ => Err(serde::de::Error::custom("expected a string or a number")),
}
}
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,
open: Option<bool>,
item: Option<String>,
key: Option<Value>,
}
);
event_struct!(
Select {
sf: Option<String>,
id: String,
#[serde(deserialize_with = "string_or_number")]
item: String,
values: Option<Map<String, Value>>,
}
);
event_struct!(
Action {
sf: Option<String>,
id: String,
act: String,
value: Option<Value>,
item: Option<String>,
checked: Option<bool>,
mods: Vec<String>,
values: Option<Map<String, Value>>,
}
);
event_struct!(
Change {
sf: Option<String>,
id: String,
value: Value,
checked: Option<bool>,
on: 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)
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
fn decode(body: Value) -> Event {
Event::decode(body.as_object().expect("an object").clone())
}
#[test]
fn toggles_carry_open_and_item_without_collapsed() {
let Event::Toggle(t) =
decode(json!({"ev": "toggle", "id": "main.mb", "item": "file", "open": true}))
else {
panic!("not a toggle");
};
assert_eq!((t.open, t.item.as_deref(), t.collapsed), (Some(true), Some("file"), false));
let Event::Toggle(t) = decode(json!({"ev": "toggle", "id": "main.date", "collapsed": true}))
else {
panic!("not a toggle");
};
assert_eq!((t.open, t.collapsed), (None, true));
}
#[test]
fn action_values_keep_their_json_shape() {
for value in [
json!({"col": "age", "dir": "asc"}),
json!(["billing", "web"]),
json!(2),
json!("session"),
json!({"db": "postgres", "tags": ["a"]}),
] {
let Event::Action(a) =
decode(json!({"ev": "action", "id": "main.t", "act": "sort", "value": value}))
else {
panic!("not an action");
};
assert_eq!(a.value.as_ref(), a.raw.get("value"));
assert!(a.value.is_some());
}
let Event::Action(a) =
decode(json!({"ev": "action", "id": "main.m", "act": "follow", "checked": true}))
else {
panic!("not an action");
};
assert_eq!((a.act.as_str(), a.checked), ("follow", Some(true)));
}
#[test]
fn change_values_keep_their_json_shape_with_item_and_on() {
for value in [json!(true), json!(0.6), json!([0.3, 0.7]), json!("haiku"), json!(["a", "b"])] {
let Event::Change(c) = decode(json!({"ev": "change", "id": "main.c", "value": value}))
else {
panic!("not a change");
};
assert_eq!(Some(&c.value), c.raw.get("value"));
}
let Event::Change(c) = decode(
json!({"ev": "change", "id": "main.q", "item": "db", "value": ["pg"], "on": false}),
) else {
panic!("not a change");
};
assert_eq!((c.item.as_deref(), c.on), (Some("db"), Some(false)));
}
#[test]
fn selects_take_an_integer_item() {
let Event::Select(s) = decode(json!({"ev": "select", "id": "main.st", "item": 3})) else {
panic!("not a select");
};
assert_eq!(s.item, "3");
assert!(matches!(
decode(json!({"ev": "activate", "id": "main.st", "item": "frame"})),
Event::Activate(s) if s.item == "frame"
));
}
}