1use std::fmt;
11
12use base64::Engine as _;
13use serde::{Deserialize, Serialize, Serializer, de::DeserializeOwned, ser::SerializeSeq};
14use serde_json::{Map, Value};
15use sha2::{Digest as _, Sha256};
16
17use crate::Error;
18
19pub const VERSION: u32 = 1;
21pub const APC_LIMIT: usize = 65_536;
24pub const CREDITS: u32 = 2;
27pub const MAX_BLOB: usize = 16 << 20;
29
30pub mod kind {
32 pub const COL: &str = "col";
34 pub const ROW: &str = "row";
36 pub const CARD: &str = "card";
38 pub const SECTION: &str = "section";
40 pub const RULE: &str = "rule";
42 pub const SPACER: &str = "spacer";
44 pub const TEXT: &str = "text";
46 pub const MD: &str = "md";
48 pub const CODE: &str = "code";
50 pub const DIFF: &str = "diff";
52 pub const ANSI: &str = "ansi";
54 pub const ROWS: &str = "rows";
56 pub const MATH: &str = "math";
58 pub const KV: &str = "kv";
60 pub const STATS: &str = "stats";
62 pub const TABLE: &str = "table";
64 pub const TREE: &str = "tree";
66 pub const JSON: &str = "json";
68 pub const BADGE: &str = "badge";
70 pub const KBD: &str = "kbd";
72 pub const ICON: &str = "icon";
74 pub const IMAGE: &str = "image";
76 pub const MEDIA: &str = "media";
78 pub const LIST: &str = "list";
80 pub const ITEM: &str = "item";
82 pub const TABS: &str = "tabs";
84 pub const PICKER: &str = "picker";
86 pub const SPINNER: &str = "spinner";
88 pub const SHIMMER: &str = "shimmer";
90 pub const ELAPSED: &str = "elapsed";
92 pub const RATE: &str = "rate";
94 pub const PROGRESS: &str = "progress";
96 pub const METER: &str = "meter";
98 pub const CHART: &str = "chart";
100 pub const EFFORT: &str = "effort";
102 pub const EDITOR: &str = "editor";
104 pub const INPUT: &str = "input";
106 pub const BUTTON: &str = "button";
108 pub const STATUS: &str = "status";
110 pub const SEG: &str = "seg";
112 pub const TOAST: &str = "toast";
114 pub const OVERLAY: &str = "overlay";
116 pub const TOOL: &str = "tool";
118 pub const AGENT: &str = "agent";
120 pub const CHECKLIST: &str = "checklist";
122 pub const BLOCK: &str = "block";
124 pub const PREFS: &str = "prefs";
126 pub const SELECT: &str = "select";
128 pub const SWITCH: &str = "switch";
130 pub const SLIDER: &str = "slider";
132 pub const FIELD: &str = "field";
134 pub const OTP: &str = "otp";
136 pub const BUTTONS: &str = "buttons";
138 pub const AVATAR: &str = "avatar";
140 pub const AVATARS: &str = "avatars";
142 pub const BREADCRUMB: &str = "breadcrumb";
144 pub const PAGER: &str = "pager";
146 pub const EMPTY: &str = "empty";
148 pub const SKELETON: &str = "skeleton";
150 pub const CALENDAR: &str = "calendar";
152 pub const DATE: &str = "date";
154 pub const MENU: &str = "menu";
156 pub const MENUBAR: &str = "menubar";
158 pub const NAV: &str = "nav";
160 pub const SPLIT: &str = "split";
162 pub const SCROLL: &str = "scroll";
164 pub const CAROUSEL: &str = "carousel";
166 pub const MESSAGE: &str = "message";
168 pub const ATTACHMENT: &str = "attachment";
170 pub const TRACE: &str = "trace";
172 pub const STACKTRACE: &str = "stacktrace";
174 pub const SUGGESTIONS: &str = "suggestions";
176 pub const QUESTIONS: &str = "questions";
178 pub const EL: &str = "el";
180
181 pub const ALL: [&str; 74] = [
183 COL,
184 ROW,
185 CARD,
186 SECTION,
187 RULE,
188 SPACER,
189 TEXT,
190 MD,
191 CODE,
192 DIFF,
193 ANSI,
194 ROWS,
195 MATH,
196 KV,
197 STATS,
198 TABLE,
199 TREE,
200 JSON,
201 BADGE,
202 KBD,
203 ICON,
204 IMAGE,
205 MEDIA,
206 LIST,
207 ITEM,
208 TABS,
209 PICKER,
210 SPINNER,
211 SHIMMER,
212 ELAPSED,
213 RATE,
214 PROGRESS,
215 METER,
216 CHART,
217 EFFORT,
218 EDITOR,
219 INPUT,
220 BUTTON,
221 STATUS,
222 SEG,
223 TOAST,
224 OVERLAY,
225 TOOL,
226 AGENT,
227 CHECKLIST,
228 BLOCK,
229 PREFS,
230 SELECT,
231 SWITCH,
232 SLIDER,
233 FIELD,
234 OTP,
235 BUTTONS,
236 AVATAR,
237 AVATARS,
238 BREADCRUMB,
239 PAGER,
240 EMPTY,
241 SKELETON,
242 CALENDAR,
243 DATE,
244 MENU,
245 MENUBAR,
246 NAV,
247 SPLIT,
248 SCROLL,
249 CAROUSEL,
250 MESSAGE,
251 ATTACHMENT,
252 TRACE,
253 STACKTRACE,
254 SUGGESTIONS,
255 QUESTIONS,
256 EL,
257 ];
258
259 pub const TEXT_KINDS: [&str; 9] = [TEXT, MD, CODE, ANSI, MATH, EDITOR, INPUT, SHIMMER, EL];
262
263 pub fn has_text(kind: &str) -> bool {
265 TEXT_KINDS.contains(&kind)
266 }
267}
268
269pub mod feature {
271 pub const EDIT: &str = "edit";
273 pub const UNDO: &str = "undo";
275 pub const SEND: &str = "send";
277
278 pub const BLOBS: &str = "blobs";
280 pub const SETTLE: &str = "settle";
282 pub const ADOPT: &str = "adopt";
284 pub const DOCK: &str = "dock";
286 pub const REDUCE_MOTION: &str = "reduce-motion";
288 pub const ASIDE: &str = "aside";
291 pub const SCROLL: &str = "scroll";
293 pub const STYLES: &str = "styles";
295 pub const FLOW: &str = "flow";
297 pub const FONT: &str = "font";
299}
300
301#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
307#[serde(rename_all = "lowercase")]
308pub enum Mode {
309 #[default]
311 Inline,
312 Screen,
314 Flow,
316}
317
318#[derive(Clone, Debug, PartialEq, Eq)]
320pub enum Query {
321 Hello {
323 app: Option<String>,
325 ver: Option<String>,
327 features: Vec<String>,
329 },
330 Blobs {
332 ids: Vec<String>,
334 },
335}
336
337impl Serialize for Query {
338 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
339 use serde::ser::SerializeMap as _;
340 match self {
341 Self::Hello { app, ver, features } => {
342 let mut m = s.serialize_map(None)?;
343 m.serialize_entry("q", "hello")?;
344 m.serialize_entry("v", &[VERSION])?;
345 if let Some(app) = app {
346 m.serialize_entry("app", app)?;
347 }
348 if let Some(ver) = ver {
349 m.serialize_entry("ver", ver)?;
350 }
351 if !features.is_empty() {
352 m.serialize_entry("features", features)?;
353 }
354 m.end()
355 },
356 Self::Blobs { ids } => {
357 let mut m = s.serialize_map(Some(2))?;
358 m.serialize_entry("q", "blobs")?;
359 m.serialize_entry("ids", ids)?;
360 m.end()
361 },
362 }
363 }
364}
365
366#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
368pub struct Open {
369 pub id: String,
371 #[serde(skip_serializing_if = "Option::is_none")]
373 pub mode: Option<Mode>,
374 #[serde(skip_serializing_if = "Option::is_none")]
376 pub title: Option<String>,
377 #[serde(skip_serializing_if = "Option::is_none")]
379 pub role: Option<String>,
380 #[serde(skip_serializing_if = "Option::is_none")]
382 pub listen: Option<bool>,
383 #[serde(skip_serializing_if = "Option::is_none")]
385 pub adopt: Option<bool>,
386 #[serde(skip_serializing_if = "Option::is_none")]
389 pub closable: Option<bool>,
390 #[serde(skip_serializing_if = "Option::is_none")]
392 pub font: Option<Font>,
393}
394
395#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
397#[serde(rename_all = "lowercase")]
398pub enum Font {
399 #[default]
401 Mono,
402 Sans,
404}
405
406#[derive(Clone, Debug, PartialEq, Serialize)]
408pub struct Frame {
409 pub sf: String,
411 pub s: u64,
413 pub ops: Vec<Op>,
415}
416
417#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
419#[serde(rename_all = "lowercase")]
420pub enum TextMode {
421 Append,
423 Replace,
425}
426
427#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
429#[serde(rename_all = "lowercase")]
430pub enum RevealAt {
431 Start,
433 End,
435 #[default]
437 Nearest,
438}
439
440#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
442#[serde(rename_all = "kebab-case")]
443pub enum ScrollBy {
444 LineUp,
446 LineDown,
448 PageUp,
450 PageDown,
452 Start,
454 End,
456}
457
458#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
460pub struct WireNode {
461 pub id: String,
463 pub k: String,
465 #[serde(default, skip_serializing_if = "Map::is_empty")]
467 pub p: Map<String, Value>,
468 #[serde(default, skip_serializing_if = "Vec::is_empty")]
470 pub c: Vec<Self>,
471}
472
473#[derive(Clone, Debug, PartialEq)]
475pub enum Op {
476 Add {
478 parent: String,
480 before: Option<String>,
482 node: WireNode,
484 },
485 Set {
487 id: String,
489 props: Map<String, Value>,
491 },
492 Text {
494 id: String,
496 mode: TextMode,
498 text: String,
500 },
501 Splice {
503 id: String,
505 at: usize,
507 del: usize,
509 text: String,
511 },
512 Move {
514 id: String,
516 parent: String,
518 before: Option<String>,
520 },
521 Del {
523 id: String,
525 },
526 Settle {
528 id: String,
530 },
531 Focus {
533 id: Option<String>,
535 },
536 Reveal {
538 id: String,
540 at: RevealAt,
542 },
543 Scroll {
545 id: String,
547 by: ScrollBy,
549 },
550 Suspend,
552 Resume,
554}
555
556impl Serialize for Op {
557 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
558 let mut seq = s.serialize_seq(None)?;
559 match self {
560 Self::Add { parent, before, node } => {
561 seq.serialize_element("add")?;
562 seq.serialize_element(&node.id)?;
563 seq.serialize_element(parent)?;
564 seq.serialize_element(before)?;
565 seq.serialize_element(node)?;
566 },
567 Self::Set { id, props } => {
568 seq.serialize_element("set")?;
569 seq.serialize_element(id)?;
570 seq.serialize_element(props)?;
571 },
572 Self::Text { id, mode, text } => {
573 seq.serialize_element("text")?;
574 seq.serialize_element(id)?;
575 seq.serialize_element(mode)?;
576 seq.serialize_element(text)?;
577 },
578 Self::Splice { id, at, del, text } => {
579 seq.serialize_element("splice")?;
580 seq.serialize_element(id)?;
581 seq.serialize_element(at)?;
582 seq.serialize_element(del)?;
583 seq.serialize_element(text)?;
584 },
585 Self::Move { id, parent, before } => {
586 seq.serialize_element("move")?;
587 seq.serialize_element(id)?;
588 seq.serialize_element(parent)?;
589 seq.serialize_element(before)?;
590 },
591 Self::Del { id } => {
592 seq.serialize_element("del")?;
593 seq.serialize_element(id)?;
594 },
595 Self::Settle { id } => {
596 seq.serialize_element("settle")?;
597 seq.serialize_element(id)?;
598 },
599 Self::Focus { id } => {
600 seq.serialize_element("focus")?;
601 seq.serialize_element(id)?;
602 },
603 Self::Reveal { id, at } => {
604 seq.serialize_element("reveal")?;
605 seq.serialize_element(id)?;
606 seq.serialize_element(at)?;
607 },
608 Self::Scroll { id, by } => {
609 seq.serialize_element("scroll")?;
610 seq.serialize_element(id)?;
611 seq.serialize_element(by)?;
612 },
613 Self::Suspend => seq.serialize_element("suspend")?,
614 Self::Resume => seq.serialize_element("resume")?,
615 }
616 seq.end()
617 }
618}
619
620#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
622pub struct Sheet {
623 #[serde(skip_serializing_if = "Option::is_none")]
625 pub sf: Option<String>,
626 pub name: String,
628 #[serde(skip_serializing_if = "Option::is_none")]
630 pub css: Option<String>,
631}
632
633#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
635pub struct Close {
636 pub id: String,
638 pub keep: bool,
640}
641
642#[derive(Clone, Debug, PartialEq, Eq)]
645pub struct Blob {
646 pub id: String,
648 pub mime: Option<String>,
650 pub data: Vec<u8>,
652}
653
654impl Blob {
655 pub fn new(data: Vec<u8>, mime: Option<String>) -> Result<Self, Error> {
660 if data.len() > MAX_BLOB {
661 return Err(Error::BlobTooLarge(data.len()));
662 }
663 Ok(Self { id: blob_id(&data), mime, data })
664 }
665}
666
667pub fn blob_id(data: &[u8]) -> String {
669 use fmt::Write as _;
670 let digest = Sha256::digest(data);
671 let mut out = String::with_capacity(64);
672 for b in digest {
673 let _ = write!(out, "{b:02x}");
674 }
675 out
676}
677
678#[derive(Clone, Debug, PartialEq)]
680pub enum Message {
681 Query(Query),
683 Open(Open),
685 Frame(Frame),
687 Blob(Blob),
689 Sheet(Sheet),
691 Close(Close),
693}
694
695impl Message {
696 pub const fn verb(&self) -> &'static str {
698 match self {
699 Self::Query(_) => "q",
700 Self::Open(_) => "o",
701 Self::Frame(_) => "f",
702 Self::Blob(_) => "b",
703 Self::Sheet(_) => "s",
704 Self::Close(_) => "x",
705 }
706 }
707
708 pub fn params(&self) -> Vec<(&'static str, &str)> {
710 match self {
711 Self::Blob(b) => b.mime.iter().map(|mime| ("mime", mime.as_str())).collect(),
712 _ => Vec::new(),
713 }
714 }
715
716 pub fn body(&self) -> Result<String, Error> {
721 Ok(match self {
722 Self::Query(m) => serde_json::to_string(m)?,
723 Self::Open(m) => serde_json::to_string(m)?,
724 Self::Frame(m) => serde_json::to_string(m)?,
725 Self::Blob(b) => base64::engine::general_purpose::STANDARD.encode(&b.data),
726 Self::Sheet(m) => serde_json::to_string(m)?,
727 Self::Close(m) => serde_json::to_string(m)?,
728 })
729 }
730}
731
732const fn key_byte(b: u8) -> bool {
734 b.is_ascii_alphanumeric() || b == b'_' || b == b'-'
735}
736
737const fn value_byte(b: u8) -> bool {
739 b >= 0x21 && b <= 0x7e && b != b';'
740}
741
742fn leads_with_parameter(rest: &[u8]) -> bool {
745 let key = rest.iter().take_while(|&&b| key_byte(b)).count();
746 if key == 0 || rest.get(key) != Some(&b'=') {
747 return false;
748 }
749 rest[key + 1..].iter().find(|&&b| !value_byte(b)) == Some(&b';')
750}
751
752fn chunk_end(body: &[u8], start: usize, limit: usize) -> usize {
754 let target = start.saturating_add(limit.max(1));
755 if target >= body.len() {
756 return body.len();
757 }
758 let safe = |at: usize| body[at] & 0xc0 != 0x80 && !leads_with_parameter(&body[at..]);
759 (start + 1..=target)
760 .rev()
761 .find(|&at| safe(at))
762 .or_else(|| (target + 1..body.len()).find(|&at| safe(at)))
763 .unwrap_or(body.len())
764}
765
766pub fn chunks(body: &[u8], limit: usize) -> impl Iterator<Item = &[u8]> {
773 let mut start = 0;
774 std::iter::from_fn(move || {
775 (start < body.len()).then(|| {
776 let end = chunk_end(body, start, limit);
777 let piece = &body[start..end];
778 start = end;
779 piece
780 })
781 })
782}
783
784fn push_apc(out: &mut Vec<u8>, verb: &str, params: &[(&str, &str)], body: &[u8]) {
786 out.extend_from_slice(b"\x1b_tsp;");
787 out.extend_from_slice(verb.as_bytes());
788 out.push(b';');
789 for (k, v) in params {
790 out.extend_from_slice(k.as_bytes());
791 out.push(b'=');
792 out.extend_from_slice(v.as_bytes());
793 out.push(b';');
794 }
795 out.extend_from_slice(body);
796 out.extend_from_slice(b"\x1b\\");
797}
798
799pub fn encode(
808 verb: &str,
809 params: &[(&str, &str)],
810 body: &[u8],
811 limit: usize,
812 chunk: &str,
813) -> Vec<u8> {
814 let mut out = Vec::with_capacity(body.len() + 16);
815 if body.len() <= limit {
816 push_apc(&mut out, verb, params, body);
817 return out;
818 }
819 let pieces: Vec<&[u8]> = chunks(body, limit).collect();
820 let last = pieces.len() - 1;
821 for (i, piece) in pieces.into_iter().enumerate() {
822 let mut ps: Vec<(&str, &str)> = if i == 0 { params.to_vec() } else { Vec::new() };
823 ps.push(("c", chunk));
824 if i < last {
825 ps.push(("m", "1"));
826 }
827 push_apc(&mut out, verb, &ps, piece);
828 }
829 out
830}
831
832#[derive(Clone, Debug)]
835pub struct Encoder {
836 limit: usize,
838 next_chunk: u64,
840}
841
842impl Default for Encoder {
843 fn default() -> Self {
844 Self::new(APC_LIMIT)
845 }
846}
847
848impl Encoder {
849 pub const fn new(limit: usize) -> Self {
851 Self { limit, next_chunk: 1 }
852 }
853
854 pub const fn limit(&self) -> usize {
856 self.limit
857 }
858
859 pub const fn set_limit(&mut self, limit: usize) {
861 self.limit = limit;
862 }
863
864 pub fn encode(&mut self, message: &Message) -> Result<Vec<u8>, Error> {
869 let body = message.body()?;
870 let chunk = if body.len() > self.limit {
871 let id = base36(self.next_chunk);
872 self.next_chunk += 1;
873 id
874 } else {
875 String::new()
876 };
877 Ok(encode(message.verb(), &message.params(), body.as_bytes(), self.limit, &chunk))
878 }
879}
880
881fn base36(mut n: u64) -> String {
883 const DIGITS: &[u8; 36] = b"0123456789abcdefghijklmnopqrstuvwxyz";
884 let mut out = Vec::new();
885 loop {
886 out.push(DIGITS[(n % 36) as usize]);
887 n /= 36;
888 if n == 0 {
889 break;
890 }
891 }
892 out.reverse();
893 String::from_utf8(out).unwrap_or_default()
894}
895
896#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
902pub struct Cell {
903 #[serde(default)]
905 pub w: f64,
906 #[serde(default)]
908 pub h: f64,
909}
910
911#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
913#[serde(rename_all = "camelCase")]
914pub struct Hello {
915 #[serde(default)]
917 pub v: u32,
918 #[serde(default)]
920 pub term: String,
921 #[serde(default)]
923 pub ver: String,
924 #[serde(default)]
926 pub kinds: Vec<String>,
927 #[serde(default)]
929 pub features: Vec<String>,
930 #[serde(default = "default_apc")]
932 pub apc: usize,
933 #[serde(default = "default_credits")]
935 pub credits: u32,
936 #[serde(default)]
938 pub cols: u32,
939 #[serde(default)]
941 pub cell: Option<Cell>,
942 #[serde(default)]
944 pub dark: bool,
945 #[serde(default)]
947 pub reduce_motion: bool,
948 #[serde(default)]
951 pub hour12: bool,
952 #[serde(skip)]
954 pub raw: Map<String, Value>,
955}
956
957const fn default_apc() -> usize {
959 APC_LIMIT
960}
961
962const fn default_credits() -> u32 {
964 CREDITS
965}
966
967#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
969pub struct BlobsReply {
970 #[serde(default)]
972 pub have: Vec<String>,
973 #[serde(skip)]
975 pub raw: Map<String, Value>,
976}
977
978#[derive(Clone, Debug, PartialEq)]
980pub enum Reply {
981 Hello(Hello),
983 Blobs(BlobsReply),
985 Unknown(Map<String, Value>),
987}
988
989trait Raw: DeserializeOwned {
991 fn set_raw(&mut self, raw: Map<String, Value>);
993}
994
995fn string_or_number<'de, D: serde::Deserializer<'de>>(d: D) -> Result<String, D::Error> {
997 match Value::deserialize(d)? {
998 Value::String(s) => Ok(s),
999 Value::Number(n) => Ok(n.to_string()),
1000 _ => Err(serde::de::Error::custom("expected a string or a number")),
1001 }
1002}
1003
1004fn typed<T: Raw>(map: Map<String, Value>) -> Result<T, Map<String, Value>> {
1006 let value = Value::Object(map);
1007 let decoded = T::deserialize(&value);
1008 let Value::Object(map) = value else {
1009 unreachable!("built as an object")
1010 };
1011 match decoded {
1012 Ok(mut t) => {
1013 t.set_raw(map);
1014 Ok(t)
1015 },
1016 Err(_) => Err(map),
1017 }
1018}
1019
1020impl Reply {
1021 pub fn decode(map: Map<String, Value>) -> Self {
1023 match map.get("r").and_then(Value::as_str) {
1024 Some("hello") => typed(map).map_or_else(Self::Unknown, Self::Hello),
1025 Some("blobs") => typed(map).map_or_else(Self::Unknown, Self::Blobs),
1026 _ => Self::Unknown(map),
1027 }
1028 }
1029
1030 pub const fn raw(&self) -> &Map<String, Value> {
1032 match self {
1033 Self::Hello(r) => &r.raw,
1034 Self::Blobs(r) => &r.raw,
1035 Self::Unknown(r) => r,
1036 }
1037 }
1038}
1039
1040macro_rules! event_struct {
1042 ($(#[$m:meta])* $name:ident { $($(#[$fm:meta])* $field:ident : $ty:ty),* $(,)? }) => {
1043 $(#[$m])*
1044 #[derive(Clone, Debug, Default, PartialEq, Deserialize)]
1045 #[allow(clippy::derive_partial_eq_without_eq, reason = "some payloads hold floats")]
1046 pub struct $name {
1047 $($(#[$fm])* #[serde(default)] pub $field: $ty,)*
1048 #[serde(skip)]
1050 pub raw: Map<String, Value>,
1051 }
1052
1053 impl Raw for $name {
1054 fn set_raw(&mut self, raw: Map<String, Value>) {
1055 self.raw = raw;
1056 }
1057 }
1058 };
1059}
1060
1061impl Raw for Hello {
1062 fn set_raw(&mut self, raw: Map<String, Value>) {
1063 self.raw = raw;
1064 }
1065}
1066
1067impl Raw for BlobsReply {
1068 fn set_raw(&mut self, raw: Map<String, Value>) {
1069 self.raw = raw;
1070 }
1071}
1072
1073event_struct!(
1074 Ack {
1076 sf: String,
1078 s: u64,
1080 }
1081);
1082
1083event_struct!(
1084 Resize {
1086 sf: Option<String>,
1088 cols: u32,
1090 cell: Option<Cell>,
1092 visible: Option<bool>,
1094 }
1095);
1096
1097event_struct!(
1098 Theme {
1100 dark: bool,
1102 }
1103);
1104
1105event_struct!(
1106 Motion {
1108 reduce: bool,
1110 }
1111);
1112
1113event_struct!(
1114 Visible {
1116 sf: Option<String>,
1118 visible: bool,
1120 }
1121);
1122
1123event_struct!(
1124 Toggle {
1127 sf: Option<String>,
1129 id: String,
1131 collapsed: bool,
1134 open: Option<bool>,
1136 item: Option<String>,
1138 key: Option<Value>,
1140 }
1141);
1142
1143event_struct!(
1144 Select {
1146 sf: Option<String>,
1148 id: String,
1150 #[serde(deserialize_with = "string_or_number")]
1152 item: String,
1153 values: Option<Map<String, Value>>,
1155 }
1156);
1157
1158event_struct!(
1159 Action {
1161 sf: Option<String>,
1163 id: String,
1165 act: String,
1167 value: Option<Value>,
1172 item: Option<String>,
1175 checked: Option<bool>,
1177 mods: Vec<String>,
1179 values: Option<Map<String, Value>>,
1181 }
1182);
1183
1184event_struct!(
1185 Change {
1192 sf: Option<String>,
1194 id: String,
1196 value: Value,
1199 checked: Option<bool>,
1201 on: Option<bool>,
1203 name: Option<String>,
1205 item: Option<String>,
1207 values: Option<Map<String, Value>>,
1209 }
1210);
1211
1212event_struct!(
1213 Focus {
1215 sf: Option<String>,
1217 id: String,
1219 }
1220);
1221
1222event_struct!(
1223 Edit {
1225 sf: Option<String>,
1227 id: String,
1229 from: usize,
1231 to: usize,
1233 text: String,
1235 cursor: usize,
1237 len: usize,
1239 }
1240);
1241
1242event_struct!(
1243 Undo {
1245 sf: Option<String>,
1247 id: String,
1249 }
1250);
1251
1252event_struct!(
1253 SendText {
1255 sf: Option<String>,
1257 id: String,
1259 text: String,
1261 }
1262);
1263
1264event_struct!(
1265 ErrorEvent {
1267 sf: Option<String>,
1269 s: Option<u64>,
1271 op: Option<u64>,
1273 msg: String,
1275 sheet: Option<String>,
1277 id: Option<String>,
1279 }
1280);
1281
1282event_struct!(
1283 Gone {
1285 sf: Option<String>,
1287 ids: Vec<String>,
1289 }
1290);
1291
1292#[derive(Clone, Debug, PartialEq)]
1294pub enum Event {
1295 Ack(Ack),
1297 Resize(Resize),
1299 Theme(Theme),
1301 Motion(Motion),
1303 Visible(Visible),
1305 Toggle(Toggle),
1307 Select(Select),
1309 Activate(Select),
1311 Action(Action),
1313 Change(Change),
1315 Focus(Focus),
1317 Edit(Edit),
1319 Undo(Undo),
1321 Send(SendText),
1323 Error(ErrorEvent),
1325 Gone(Gone),
1327 Unknown(Map<String, Value>),
1329}
1330
1331impl Event {
1332 pub fn decode(map: Map<String, Value>) -> Self {
1334 let ev = map
1335 .get("ev")
1336 .and_then(Value::as_str)
1337 .unwrap_or_default()
1338 .to_owned();
1339 match ev.as_str() {
1340 "ack" => typed(map).map_or_else(Self::Unknown, Self::Ack),
1341 "resize" => typed(map).map_or_else(Self::Unknown, Self::Resize),
1342 "theme" => typed(map).map_or_else(Self::Unknown, Self::Theme),
1343 "motion" => typed(map).map_or_else(Self::Unknown, Self::Motion),
1344 "visible" => typed(map).map_or_else(Self::Unknown, Self::Visible),
1345 "toggle" => typed(map).map_or_else(Self::Unknown, Self::Toggle),
1346 "select" => typed(map).map_or_else(Self::Unknown, Self::Select),
1347 "activate" => typed(map).map_or_else(Self::Unknown, Self::Activate),
1348 "action" => typed(map).map_or_else(Self::Unknown, Self::Action),
1349 "change" => typed(map).map_or_else(Self::Unknown, Self::Change),
1350 "focus" => typed(map).map_or_else(Self::Unknown, Self::Focus),
1351 "edit" => typed(map).map_or_else(Self::Unknown, Self::Edit),
1352 "undo" => typed(map).map_or_else(Self::Unknown, Self::Undo),
1353 "send" => typed(map).map_or_else(Self::Unknown, Self::Send),
1354 "error" => typed(map).map_or_else(Self::Unknown, Self::Error),
1355 "gone" => typed(map).map_or_else(Self::Unknown, Self::Gone),
1356 _ => Self::Unknown(map),
1357 }
1358 }
1359
1360 pub const fn raw(&self) -> &Map<String, Value> {
1362 match self {
1363 Self::Ack(e) => &e.raw,
1364 Self::Resize(e) => &e.raw,
1365 Self::Theme(e) => &e.raw,
1366 Self::Motion(e) => &e.raw,
1367 Self::Visible(e) => &e.raw,
1368 Self::Toggle(e) => &e.raw,
1369 Self::Select(e) | Self::Activate(e) => &e.raw,
1370 Self::Action(e) => &e.raw,
1371 Self::Change(e) => &e.raw,
1372 Self::Focus(e) => &e.raw,
1373 Self::Edit(e) => &e.raw,
1374 Self::Undo(e) => &e.raw,
1375 Self::Send(e) => &e.raw,
1376 Self::Error(e) => &e.raw,
1377 Self::Gone(e) => &e.raw,
1378 Self::Unknown(e) => e,
1379 }
1380 }
1381
1382 pub fn name(&self) -> &str {
1384 self
1385 .raw()
1386 .get("ev")
1387 .and_then(Value::as_str)
1388 .unwrap_or_default()
1389 }
1390
1391 pub fn sf(&self) -> Option<&str> {
1393 self.raw().get("sf").and_then(Value::as_str)
1394 }
1395
1396 pub fn id(&self) -> Option<&str> {
1398 self.raw().get("id").and_then(Value::as_str)
1399 }
1400}
1401
1402#[cfg(test)]
1403mod tests {
1404 use serde_json::json;
1405
1406 use super::*;
1407
1408 fn decode(body: Value) -> Event {
1409 Event::decode(body.as_object().expect("an object").clone())
1410 }
1411
1412 #[test]
1413 fn toggles_carry_open_and_item_without_collapsed() {
1414 let Event::Toggle(t) =
1415 decode(json!({"ev": "toggle", "id": "main.mb", "item": "file", "open": true}))
1416 else {
1417 panic!("not a toggle");
1418 };
1419 assert_eq!((t.open, t.item.as_deref(), t.collapsed), (Some(true), Some("file"), false));
1420 let Event::Toggle(t) = decode(json!({"ev": "toggle", "id": "main.date", "collapsed": true}))
1421 else {
1422 panic!("not a toggle");
1423 };
1424 assert_eq!((t.open, t.collapsed), (None, true));
1425 }
1426
1427 #[test]
1428 fn action_values_keep_their_json_shape() {
1429 for value in [
1430 json!({"col": "age", "dir": "asc"}),
1431 json!(["billing", "web"]),
1432 json!(2),
1433 json!("session"),
1434 json!({"db": "postgres", "tags": ["a"]}),
1435 ] {
1436 let Event::Action(a) =
1437 decode(json!({"ev": "action", "id": "main.t", "act": "sort", "value": value}))
1438 else {
1439 panic!("not an action");
1440 };
1441 assert_eq!(a.value.as_ref(), a.raw.get("value"));
1442 assert!(a.value.is_some());
1443 }
1444 let Event::Action(a) =
1445 decode(json!({"ev": "action", "id": "main.m", "act": "follow", "checked": true}))
1446 else {
1447 panic!("not an action");
1448 };
1449 assert_eq!((a.act.as_str(), a.checked), ("follow", Some(true)));
1450 }
1451
1452 #[test]
1453 fn change_values_keep_their_json_shape_with_item_and_on() {
1454 for value in [json!(true), json!(0.6), json!([0.3, 0.7]), json!("haiku"), json!(["a", "b"])] {
1455 let Event::Change(c) = decode(json!({"ev": "change", "id": "main.c", "value": value}))
1456 else {
1457 panic!("not a change");
1458 };
1459 assert_eq!(Some(&c.value), c.raw.get("value"));
1460 }
1461 let Event::Change(c) = decode(
1462 json!({"ev": "change", "id": "main.q", "item": "db", "value": ["pg"], "on": false}),
1463 ) else {
1464 panic!("not a change");
1465 };
1466 assert_eq!((c.item.as_deref(), c.on), (Some("db"), Some(false)));
1467 }
1468
1469 #[test]
1470 fn selects_take_an_integer_item() {
1471 let Event::Select(s) = decode(json!({"ev": "select", "id": "main.st", "item": 3})) else {
1472 panic!("not a select");
1473 };
1474 assert_eq!(s.item, "3");
1475 assert!(matches!(
1476 decode(json!({"ev": "activate", "id": "main.st", "item": "frame"})),
1477 Event::Activate(s) if s.item == "frame"
1478 ));
1479 }
1480}