Skip to main content

tern_sdk/
wire.rs

1//! Layer 1: the wire. Constants, the messages a program sends and receives,
2//! the encoder (framing, chunking, blobs) and the decoder of replies and
3//! events.
4//!
5//! Every message is one APC string, `ESC _ tsp;<verb>[;k=v]*;<body> ESC \`.
6//! A body over the negotiated `apc` limit is cut by [`chunks`] exactly as
7//! Tern's own `tsp_chunks` cuts it, and each piece is sent with `c=<id>`
8//! (and `m=1` on all but the last).
9
10use 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
19/// The protocol version this SDK speaks.
20pub const VERSION: u32 = 1;
21/// The largest body sent in one message unless the `hello` reply says
22/// otherwise.
23pub const APC_LIMIT: usize = 65_536;
24/// How many frames may be unacknowledged unless the `hello` reply says
25/// otherwise.
26pub const CREDITS: u32 = 2;
27/// The largest blob Tern takes, in decoded bytes.
28pub const MAX_BLOB: usize = 16 << 20;
29
30/// Node kinds, as the `k` of a node and the `kinds` of the `hello` reply.
31pub mod kind {
32	/// A vertical stack.
33	pub const COL: &str = "col";
34	/// A horizontal flex row.
35	pub const ROW: &str = "row";
36	/// A card with a head and a body.
37	pub const CARD: &str = "card";
38	/// A disclosure group with no ring.
39	pub const SECTION: &str = "section";
40	/// A divider hairline.
41	pub const RULE: &str = "rule";
42	/// Vertical space.
43	pub const SPACER: &str = "spacer";
44	/// Styled spans.
45	pub const TEXT: &str = "text";
46	/// Markdown.
47	pub const MD: &str = "md";
48	/// Highlighted code.
49	pub const CODE: &str = "code";
50	/// A unified or split diff.
51	pub const DIFF: &str = "diff";
52	/// Output with escape sequences.
53	pub const ANSI: &str = "ansi";
54	/// Pre-rendered ANSI rows.
55	pub const ROWS: &str = "rows";
56	/// TeX math.
57	pub const MATH: &str = "math";
58	/// An aligned key/value list.
59	pub const KV: &str = "kv";
60	/// A well of stat tiles.
61	pub const STATS: &str = "stats";
62	/// A table.
63	pub const TABLE: &str = "table";
64	/// A disclosure tree.
65	pub const TREE: &str = "tree";
66	/// A JSON value Tern draws as a foldable tree.
67	pub const JSON: &str = "json";
68	/// A pill chip.
69	pub const BADGE: &str = "badge";
70	/// Keycaps.
71	pub const KBD: &str = "kbd";
72	/// A named icon.
73	pub const ICON: &str = "icon";
74	/// An image from a blob.
75	pub const IMAGE: &str = "image";
76	/// A file Tern loads by path and shows in its media viewer.
77	pub const MEDIA: &str = "media";
78	/// A selectable list.
79	pub const LIST: &str = "list";
80	/// One list row.
81	pub const ITEM: &str = "item";
82	/// A tab strip.
83	pub const TABS: &str = "tabs";
84	/// A searchable picker sheet.
85	pub const PICKER: &str = "picker";
86	/// An activity indicator.
87	pub const SPINNER: &str = "spinner";
88	/// A shimmering label.
89	pub const SHIMMER: &str = "shimmer";
90	/// A live timer.
91	pub const ELAPSED: &str = "elapsed";
92	/// A number easing between updates.
93	pub const RATE: &str = "rate";
94	/// A progress bar.
95	pub const PROGRESS: &str = "progress";
96	/// A value as a bar, ring or grid.
97	pub const METER: &str = "meter";
98	/// Bars, a sparkline or a heatmap.
99	pub const CHART: &str = "chart";
100	/// A thinking-effort glyph.
101	pub const EFFORT: &str = "effort";
102	/// A multi-line text field.
103	pub const EDITOR: &str = "editor";
104	/// A single-line text field.
105	pub const INPUT: &str = "input";
106	/// A push button.
107	pub const BUTTON: &str = "button";
108	/// A status bar strip.
109	pub const STATUS: &str = "status";
110	/// A status bar segment.
111	pub const SEG: &str = "seg";
112	/// A transient notice.
113	pub const TOAST: &str = "toast";
114	/// A floating panel.
115	pub const OVERLAY: &str = "overlay";
116	/// A coding-agent tool call.
117	pub const TOOL: &str = "tool";
118	/// A subagent row.
119	pub const AGENT: &str = "agent";
120	/// A todo list.
121	pub const CHECKLIST: &str = "checklist";
122	/// A shell command's output (Tern's own).
123	pub const BLOCK: &str = "block";
124	/// A settings page.
125	pub const PREFS: &str = "prefs";
126	/// A value picked from a menu, optionally searchable.
127	pub const SELECT: &str = "select";
128	/// An on/off switch.
129	pub const SWITCH: &str = "switch";
130	/// A value, or a range, dragged along a track.
131	pub const SLIDER: &str = "slider";
132	/// A labelled form row: label, control, help or error.
133	pub const FIELD: &str = "field";
134	/// A one-time code, one cell per character.
135	pub const OTP: &str = "otp";
136	/// Buttons drawn as one group.
137	pub const BUTTONS: &str = "buttons";
138	/// A person, bot or org: initials, an icon or a picture.
139	pub const AVATAR: &str = "avatar";
140	/// Avatars stacked, with an overflow count.
141	pub const AVATARS: &str = "avatars";
142	/// A trail of places, the last one current.
143	pub const BREADCRUMB: &str = "breadcrumb";
144	/// Pagination.
145	pub const PAGER: &str = "pager";
146	/// An empty state.
147	pub const EMPTY: &str = "empty";
148	/// Placeholder shapes while content loads.
149	pub const SKELETON: &str = "skeleton";
150	/// A month grid or a week strip of days to pick from.
151	pub const CALENDAR: &str = "calendar";
152	/// A date field that opens a calendar.
153	pub const DATE: &str = "date";
154	/// A button that opens a menu of commands.
155	pub const MENU: &str = "menu";
156	/// A strip of menus.
157	pub const MENUBAR: &str = "menubar";
158	/// Navigation links, some opening a panel.
159	pub const NAV: &str = "nav";
160	/// Panes the user resizes by dragging between them.
161	pub const SPLIT: &str = "split";
162	/// A scrolling container, optionally pinned to its end.
163	pub const SCROLL: &str = "scroll";
164	/// Slides shown one at a time.
165	pub const CAROUSEL: &str = "carousel";
166	/// One chat message.
167	pub const MESSAGE: &str = "message";
168	/// A file attached to a message, with its upload state.
169	pub const ATTACHMENT: &str = "attachment";
170	/// An agent's steps, each opening in place.
171	pub const TRACE: &str = "trace";
172	/// An error and its stack frames.
173	pub const STACKTRACE: &str = "stacktrace";
174	/// Suggested prompts, sent with one click.
175	pub const SUGGESTIONS: &str = "suggestions";
176	/// A questionnaire answered in steps.
177	pub const QUESTIONS: &str = "questions";
178	/// A plain HTML element.
179	pub const EL: &str = "el";
180
181	/// Every kind of protocol version 1.
182	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	/// The kinds whose `text` prop is primary text, updated with `text` and
260	/// `splice` ops.
261	pub const TEXT_KINDS: [&str; 9] = [TEXT, MD, CODE, ANSI, MATH, EDITOR, INPUT, SHIMMER, EL];
262
263	/// Whether `kind` keeps primary text in its `text` prop.
264	pub fn has_text(kind: &str) -> bool {
265		TEXT_KINDS.contains(&kind)
266	}
267}
268
269/// Feature names of the `hello` query (program) and reply (terminal).
270pub mod feature {
271	/// Program: applies `edit` events (native editing).
272	pub const EDIT: &str = "edit";
273	/// Program: applies `undo` events.
274	pub const UNDO: &str = "undo";
275	/// Program: submits text from `send` events.
276	pub const SEND: &str = "send";
277
278	/// Terminal: the `b` verb and the `blobs` query.
279	pub const BLOBS: &str = "blobs";
280	/// Terminal: the `settle` op is a useful hint.
281	pub const SETTLE: &str = "settle";
282	/// Terminal: `o` with `adopt:true` reopens a closed surface.
283	pub const ADOPT: &str = "adopt";
284	/// Terminal: the `dock` region sticks to the pane's bottom.
285	pub const DOCK: &str = "dock";
286	/// Terminal: `reduceMotion` and `motion` events are reported.
287	pub const REDUCE_MOTION: &str = "reduce-motion";
288	/// Terminal: a `prefs` node in an inline surface's `layer` docks beside
289	/// the pane.
290	pub const ASIDE: &str = "aside";
291	/// Terminal: the `scroll` op.
292	pub const SCROLL: &str = "scroll";
293	/// Terminal: the `s` verb (stylesheets).
294	pub const STYLES: &str = "styles";
295	/// Terminal: `flow` surfaces and `listen:false`.
296	pub const FLOW: &str = "flow";
297	/// Terminal: `o` takes `font` (`sans` | `mono`).
298	pub const FONT: &str = "font";
299}
300
301// ---------------------------------------------------------------------------
302// Program → terminal
303// ---------------------------------------------------------------------------
304
305/// A surface mode.
306#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
307#[serde(rename_all = "lowercase")]
308pub enum Mode {
309	/// At the cursor row, owning the pane while live (a session program).
310	#[default]
311	Inline,
312	/// Over the whole pane, like the alternate screen.
313	Screen,
314	/// At the cursor row among the output around it, kept as output.
315	Flow,
316}
317
318/// A `q` query.
319#[derive(Clone, Debug, PartialEq, Eq)]
320pub enum Query {
321	/// `{"q":"hello","v":[1],"app"?,"ver"?,"features"?}`.
322	Hello {
323		/// The program's name.
324		app:      Option<String>,
325		/// The program's version.
326		ver:      Option<String>,
327		/// Program features (`edit`, `undo`, `send`).
328		features: Vec<String>,
329	},
330	/// `{"q":"blobs","ids":[…]}`: which blobs Tern still holds.
331	Blobs {
332		/// Blob ids (lowercase SHA-256 hex).
333		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/// An `o` message: open a surface.
367#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
368pub struct Open {
369	/// The surface id; frames and events carry it as `sf`.
370	pub id:       String,
371	/// The mode (`inline` when absent).
372	#[serde(skip_serializing_if = "Option::is_none")]
373	pub mode:     Option<Mode>,
374	/// Names the pane until the program sets its own title.
375	#[serde(skip_serializing_if = "Option::is_none")]
376	pub title:    Option<String>,
377	/// `data-surface` on the region elements, for stylesheets.
378	#[serde(skip_serializing_if = "Option::is_none")]
379	pub role:     Option<String>,
380	/// `false`: the program never reads its input.
381	#[serde(skip_serializing_if = "Option::is_none")]
382	pub listen:   Option<bool>,
383	/// `true` reopens a closed inline surface with this id.
384	#[serde(skip_serializing_if = "Option::is_none")]
385	pub adopt:    Option<bool>,
386	/// `true`: closing the pane loses nothing, so Tern closes it without
387	/// asking.
388	#[serde(skip_serializing_if = "Option::is_none")]
389	pub closable: Option<bool>,
390	/// The surface's base face (`mono` when absent).
391	#[serde(skip_serializing_if = "Option::is_none")]
392	pub font:     Option<Font>,
393}
394
395/// A surface's base face: the `font` of an `o` message.
396#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
397#[serde(rename_all = "lowercase")]
398pub enum Font {
399	/// The terminal face (the default).
400	#[default]
401	Mono,
402	/// The UI face for apps; machine strings stay mono.
403	Sans,
404}
405
406/// An `f` message: an atomic batch of ops on one surface.
407#[derive(Clone, Debug, PartialEq, Serialize)]
408pub struct Frame {
409	/// The surface id.
410	pub sf:  String,
411	/// The frame's sequence number, from 1 per surface.
412	pub s:   u64,
413	/// The ops, applied in order.
414	pub ops: Vec<Op>,
415}
416
417/// How a `text` op changes primary text.
418#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
419#[serde(rename_all = "lowercase")]
420pub enum TextMode {
421	/// Add to the end (the streaming path).
422	Append,
423	/// Set the whole text.
424	Replace,
425}
426
427/// Where a `reveal` op scrolls a node.
428#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
429#[serde(rename_all = "lowercase")]
430pub enum RevealAt {
431	/// The node's start at the scroller's start.
432	Start,
433	/// The node's end at the scroller's end.
434	End,
435	/// The least scrolling that shows the node.
436	#[default]
437	Nearest,
438}
439
440/// How far a `scroll` op moves.
441#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
442#[serde(rename_all = "kebab-case")]
443pub enum ScrollBy {
444	/// One line up.
445	LineUp,
446	/// One line down.
447	LineDown,
448	/// A viewport less a line up.
449	PageUp,
450	/// A viewport less a line down.
451	PageDown,
452	/// To the start.
453	Start,
454	/// To the end (resumes following).
455	End,
456}
457
458/// A node with its id, as an `add` op carries it.
459#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
460pub struct WireNode {
461	/// The node id.
462	pub id: String,
463	/// The kind.
464	pub k:  String,
465	/// Props (omitted when empty).
466	#[serde(default, skip_serializing_if = "Map::is_empty")]
467	pub p:  Map<String, Value>,
468	/// Children (omitted when empty).
469	#[serde(default, skip_serializing_if = "Vec::is_empty")]
470	pub c:  Vec<Self>,
471}
472
473/// One frame op.
474#[derive(Clone, Debug, PartialEq)]
475pub enum Op {
476	/// `["add", id, parent, before|null, node]`.
477	Add {
478		/// Parent id.
479		parent: String,
480		/// The sibling to insert before, or last.
481		before: Option<String>,
482		/// The subtree; its id is the op's.
483		node:   WireNode,
484	},
485	/// `["set", id, props]`: merge props shallowly; `null` deletes.
486	Set {
487		/// Node id.
488		id:    String,
489		/// Props to replace.
490		props: Map<String, Value>,
491	},
492	/// `["text", id, mode, string]`.
493	Text {
494		/// Node id.
495		id:   String,
496		/// Append or replace.
497		mode: TextMode,
498		/// The text.
499		text: String,
500	},
501	/// `["splice", id, at, del, string]` in UTF-16 units.
502	Splice {
503		/// Node id.
504		id:   String,
505		/// Offset in UTF-16 units.
506		at:   usize,
507		/// UTF-16 units removed.
508		del:  usize,
509		/// Text inserted.
510		text: String,
511	},
512	/// `["move", id, parent, before|null]`.
513	Move {
514		/// Node id.
515		id:     String,
516		/// New parent id.
517		parent: String,
518		/// The sibling to move before, or last.
519		before: Option<String>,
520	},
521	/// `["del", id]`.
522	Del {
523		/// Node id.
524		id: String,
525	},
526	/// `["settle", id]`: a hint that the subtree won't change soon.
527	Settle {
528		/// Node id.
529		id: String,
530	},
531	/// `["focus", id|null]`: the field owning the caret.
532	Focus {
533		/// Field id, or none.
534		id: Option<String>,
535	},
536	/// `["reveal", id, at]`.
537	Reveal {
538		/// Node id.
539		id: String,
540		/// Where to scroll it.
541		at: RevealAt,
542	},
543	/// `["scroll", id, by]`.
544	Scroll {
545		/// A node in the scroll container.
546		id: String,
547		/// How far.
548		by: ScrollBy,
549	},
550	/// `["suspend"]`: hand the pane back to the grid.
551	Suspend,
552	/// `["resume"]`: take the pane again.
553	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/// An `s` message: install, replace or (without `css`) remove a stylesheet.
621#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
622pub struct Sheet {
623	/// The surface (the live one when absent).
624	#[serde(skip_serializing_if = "Option::is_none")]
625	pub sf:   Option<String>,
626	/// The sheet's name (1–64 of `A-Z a-z 0-9 _ -`).
627	pub name: String,
628	/// The CSS; absent removes the sheet.
629	#[serde(skip_serializing_if = "Option::is_none")]
630	pub css:  Option<String>,
631}
632
633/// An `x` message: close a surface.
634#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
635pub struct Close {
636	/// The surface id.
637	pub id:   String,
638	/// Keep it on screen (`main` stays in the scrollback).
639	pub keep: bool,
640}
641
642/// A `b` message: binary data with an optional MIME type. Its id is the
643/// program's own reference to it and is not sent; Tern hashes the bytes.
644#[derive(Clone, Debug, PartialEq, Eq)]
645pub struct Blob {
646	/// The lowercase hex SHA-256 of `data`, for references (not sent).
647	pub id:   String,
648	/// The MIME type.
649	pub mime: Option<String>,
650	/// The bytes.
651	pub data: Vec<u8>,
652}
653
654impl Blob {
655	/// A blob of `data`, its id computed.
656	///
657	/// # Errors
658	/// [`Error::BlobTooLarge`] over [`MAX_BLOB`] bytes.
659	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
667/// The id of a blob: the lowercase hex SHA-256 of its bytes.
668pub 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/// Any program → terminal message.
679#[derive(Clone, Debug, PartialEq)]
680pub enum Message {
681	/// `q`.
682	Query(Query),
683	/// `o`.
684	Open(Open),
685	/// `f`.
686	Frame(Frame),
687	/// `b`.
688	Blob(Blob),
689	/// `s`.
690	Sheet(Sheet),
691	/// `x`.
692	Close(Close),
693}
694
695impl Message {
696	/// The one-letter verb.
697	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	/// The parameters: a blob's `mime` when it has one, else none.
709	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	/// The body: compact JSON, or base64 for a blob.
717	///
718	/// # Errors
719	/// When serialization fails (it doesn't for these types).
720	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
732/// A byte of a parameter key.
733const fn key_byte(b: u8) -> bool {
734	b.is_ascii_alphanumeric() || b == b'_' || b == b'-'
735}
736
737/// A byte of a parameter value: printable ASCII other than `;`.
738const fn value_byte(b: u8) -> bool {
739	b >= 0x21 && b <= 0x7e && b != b';'
740}
741
742/// Whether `rest`, placed after a message's parameters, would be read as one
743/// more parameter: a `key=value` segment with a `;` after it.
744fn 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
752/// Where the chunk of `body` starting at `start` ends.
753fn 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
766/// Splits a body into chunk bodies exactly as Tern's `tsp_chunks` does.
767///
768/// Each piece is at most `limit` bytes, cut where a cut is safe: on a UTF-8
769/// boundary, and never where the next piece would lead with a
770/// parameter-shaped segment. A body within `limit` is one chunk; an empty
771/// body has none.
772pub 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
784/// Appends one APC string `ESC _ tsp;<verb>;<params…>;<body> ESC \`.
785fn 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
799/// Encodes one message of `verb` with `params` and an already serialized
800/// `body`, chunked under `limit` with chunk id `chunk` when it is longer.
801///
802/// # Example
803/// ```
804/// let out = tern_sdk::wire::encode("o", &[], br#"{"id":"s1"}"#, 65536, "1");
805/// assert_eq!(out, b"\x1b_tsp;o;{\"id\":\"s1\"}\x1b\\");
806/// ```
807pub 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/// Encodes messages under the session's `apc` limit, numbering chunk
833/// sequences with a base-36 counter.
834#[derive(Clone, Debug)]
835pub struct Encoder {
836	/// The largest body per APC string.
837	limit:      usize,
838	/// The next chunk id.
839	next_chunk: u64,
840}
841
842impl Default for Encoder {
843	fn default() -> Self {
844		Self::new(APC_LIMIT)
845	}
846}
847
848impl Encoder {
849	/// An encoder chunking bodies over `limit` bytes.
850	pub const fn new(limit: usize) -> Self {
851		Self { limit, next_chunk: 1 }
852	}
853
854	/// The limit in use.
855	pub const fn limit(&self) -> usize {
856		self.limit
857	}
858
859	/// Changes the limit (the `hello` reply's `apc`).
860	pub const fn set_limit(&mut self, limit: usize) {
861		self.limit = limit;
862	}
863
864	/// The bytes of `message`, chunked when its body is over the limit.
865	///
866	/// # Errors
867	/// When the body can't be serialized.
868	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
881/// `n` in lowercase base 36.
882fn 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// ---------------------------------------------------------------------------
897// Terminal → program
898// ---------------------------------------------------------------------------
899
900/// A cell size in pixels.
901#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
902pub struct Cell {
903	/// Width.
904	#[serde(default)]
905	pub w: f64,
906	/// Height.
907	#[serde(default)]
908	pub h: f64,
909}
910
911/// The `hello` reply: what the terminal draws and supports.
912#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
913#[serde(rename_all = "camelCase")]
914pub struct Hello {
915	/// The version picked (`1`).
916	#[serde(default)]
917	pub v:             u32,
918	/// The terminal (`tern`).
919	#[serde(default)]
920	pub term:          String,
921	/// The terminal's version.
922	#[serde(default)]
923	pub ver:           String,
924	/// Every kind it draws.
925	#[serde(default)]
926	pub kinds:         Vec<String>,
927	/// Terminal features.
928	#[serde(default)]
929	pub features:      Vec<String>,
930	/// The largest body per message.
931	#[serde(default = "default_apc")]
932	pub apc:           usize,
933	/// Frames that may be unacknowledged.
934	#[serde(default = "default_credits")]
935	pub credits:       u32,
936	/// The pane's width in columns.
937	#[serde(default)]
938	pub cols:          u32,
939	/// The cell size.
940	#[serde(default)]
941	pub cell:          Option<Cell>,
942	/// Whether the terminal shows its dark appearance.
943	#[serde(default)]
944	pub dark:          bool,
945	/// Whether Reduce Motion is on.
946	#[serde(default)]
947	pub reduce_motion: bool,
948	/// Whether the user's system reads a 12-hour clock (`3:05 PM`); false
949	/// (24-hour) on terminals that don't say.
950	#[serde(default)]
951	pub hour12:        bool,
952	/// The reply as received.
953	#[serde(skip)]
954	pub raw:           Map<String, Value>,
955}
956
957/// The default of `apc`.
958const fn default_apc() -> usize {
959	APC_LIMIT
960}
961
962/// The default of `credits`.
963const fn default_credits() -> u32 {
964	CREDITS
965}
966
967/// The `blobs` reply: which of the asked blobs Tern holds.
968#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
969pub struct BlobsReply {
970	/// The ids held, in the order asked.
971	#[serde(default)]
972	pub have: Vec<String>,
973	/// The reply as received.
974	#[serde(skip)]
975	pub raw:  Map<String, Value>,
976}
977
978/// An `r` message: the reply to a query.
979#[derive(Clone, Debug, PartialEq)]
980pub enum Reply {
981	/// `hello`.
982	Hello(Hello),
983	/// `blobs`.
984	Blobs(BlobsReply),
985	/// Any other reply, kept raw.
986	Unknown(Map<String, Value>),
987}
988
989/// Types decoded from a raw object that keep it.
990trait Raw: DeserializeOwned {
991	/// Stores the raw object.
992	fn set_raw(&mut self, raw: Map<String, Value>);
993}
994
995/// Decodes a string, or a number as its decimal text.
996fn 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
1004/// Decodes `map` as `T`, or `None` when its fields don't fit.
1005fn 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	/// Decodes a reply body; fields that don't fit keep it raw.
1022	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	/// The reply as received.
1031	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
1040/// Defines an event payload struct that keeps its raw object.
1041macro_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			/// The event as received.
1049			#[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`: the highest frame applied and drawn.
1075	Ack {
1076		/// The surface.
1077		sf: String,
1078		/// The frame's sequence number.
1079		s:  u64,
1080	}
1081);
1082
1083event_struct!(
1084	/// `resize`: the pane's width or cell size changed.
1085	Resize {
1086		/// The surface.
1087		sf: Option<String>,
1088		/// The width in columns.
1089		cols: u32,
1090		/// The cell size.
1091		cell: Option<Cell>,
1092		/// Whether the pane is visible.
1093		visible: Option<bool>,
1094	}
1095);
1096
1097event_struct!(
1098	/// `theme`: the appearance switched.
1099	Theme {
1100		/// Whether it is dark now.
1101		dark: bool,
1102	}
1103);
1104
1105event_struct!(
1106	/// `motion`: Reduce Motion was toggled.
1107	Motion {
1108		/// Whether motion is reduced now.
1109		reduce: bool,
1110	}
1111);
1112
1113event_struct!(
1114	/// `visible`: the pane was hidden or shown.
1115	Visible {
1116		/// The surface.
1117		sf: Option<String>,
1118		/// Whether it shows.
1119		visible: bool,
1120	}
1121);
1122
1123event_struct!(
1124	/// `toggle`: a collapsible node was folded or unfolded, or a menu,
1125	/// select or trace line opened or closed.
1126	Toggle {
1127		/// The surface.
1128		sf: Option<String>,
1129		/// The node.
1130		id: String,
1131		/// Whether it is folded now (`false` in the events that say `open`
1132		/// instead).
1133		collapsed: bool,
1134		/// Whether it is open now (`select`, `menu`, `menubar`, `trace`).
1135		open: Option<bool>,
1136		/// The menu of a `menubar`, or the line of a `trace`.
1137		item: Option<String>,
1138		/// The node's `key` prop (or a `tree` item).
1139		key: Option<Value>,
1140	}
1141);
1142
1143event_struct!(
1144	/// `select` or `activate`: an item was picked or opened.
1145	Select {
1146		/// The surface.
1147		sf: Option<String>,
1148		/// The node (the list, for a list's item).
1149		id: String,
1150		/// The item (a `stacktrace` frame's index in decimal).
1151		#[serde(deserialize_with = "string_or_number")]
1152		item: String,
1153		/// Form values, inside an `el` form.
1154		values: Option<Map<String, Value>>,
1155	}
1156);
1157
1158event_struct!(
1159	/// `action`: a pointer action with a custom name.
1160	Action {
1161		/// The surface.
1162		sf: Option<String>,
1163		/// The node.
1164		id: String,
1165		/// The action's name (before any `=`).
1166		act: String,
1167		/// The part after `=` in the action's name, or the action's payload:
1168		/// a table's `sort` (`{col, dir}`) and selection bar acts (the picked
1169		/// ids), the scope of a tool's approval, the cell of an `otp`'s
1170		/// `focus`, a calendar's `month`, the answers of a `questions` submit.
1171		value: Option<Value>,
1172		/// The row, for a table's row menu or button cell; the menu of a
1173		/// `menubar`; the question a `questions` step left.
1174		item: Option<String>,
1175		/// The new state of a check or radio row of a menu.
1176		checked: Option<bool>,
1177		/// Modifiers held, in the order `shift`, `ctrl`, `alt`, `meta`.
1178		mods: Vec<String>,
1179		/// Form values, inside an `el` form.
1180		values: Option<Map<String, Value>>,
1181	}
1182);
1183
1184event_struct!(
1185	/// `change`: an `el` control flipped, a `prefs` row changed, or an input
1186	/// kind took a new value.
1187	///
1188	/// The new value is a switch's state, a slider's value or range, a
1189	/// select's option, a calendar's day, a table's picks, a split's shares,
1190	/// a pager's page, a carousel's slide or a question's answer.
1191	Change {
1192		/// The surface.
1193		sf: Option<String>,
1194		/// The node.
1195		id: String,
1196		/// The control's value, the `prefs` row's new value or the kind's
1197		/// new value.
1198		value: Value,
1199		/// The control's new state.
1200		checked: Option<bool>,
1201		/// Whether the user reacted with a `message`'s reaction chip now.
1202		on: Option<bool>,
1203		/// The control's name.
1204		name: Option<String>,
1205		/// The `prefs` row, or the question of a `questions` answer.
1206		item: Option<String>,
1207		/// Form values, inside an `el` form.
1208		values: Option<Map<String, Value>>,
1209	}
1210);
1211
1212event_struct!(
1213	/// `focus`: a click asked for the keys in a field.
1214	Focus {
1215		/// The surface.
1216		sf: Option<String>,
1217		/// The field.
1218		id: String,
1219	}
1220);
1221
1222event_struct!(
1223	/// `edit`: native editing replaced UTF-16 `[from, to)` with `text`.
1224	Edit {
1225		/// The surface.
1226		sf: Option<String>,
1227		/// The field.
1228		id: String,
1229		/// Start, UTF-16 units.
1230		from: usize,
1231		/// End, UTF-16 units.
1232		to: usize,
1233		/// The replacement.
1234		text: String,
1235		/// The caret afterwards.
1236		cursor: usize,
1237		/// The length of the text Tern saw.
1238		len: usize,
1239	}
1240);
1241
1242event_struct!(
1243	/// `undo`: undo the last change to a field.
1244	Undo {
1245		/// The surface.
1246		sf: Option<String>,
1247		/// The field.
1248		id: String,
1249	}
1250);
1251
1252event_struct!(
1253	/// `send`: submit text through a composer.
1254	SendText {
1255		/// The surface.
1256		sf: Option<String>,
1257		/// The field.
1258		id: String,
1259		/// The text.
1260		text: String,
1261	}
1262);
1263
1264event_struct!(
1265	/// `error`: a rejected op, message, stylesheet or element.
1266	ErrorEvent {
1267		/// The surface, when the message named one.
1268		sf: Option<String>,
1269		/// The frame.
1270		s: Option<u64>,
1271		/// The op's index in the frame.
1272		op: Option<u64>,
1273		/// What went wrong.
1274		msg: String,
1275		/// The stylesheet.
1276		sheet: Option<String>,
1277		/// The element.
1278		id: Option<String>,
1279	}
1280);
1281
1282event_struct!(
1283	/// `gone`: nodes or a surface Tern dropped.
1284	Gone {
1285		/// The surface.
1286		sf: Option<String>,
1287		/// The ids dropped.
1288		ids: Vec<String>,
1289	}
1290);
1291
1292/// An `e` message: an event.
1293#[derive(Clone, Debug, PartialEq)]
1294pub enum Event {
1295	/// `ack`.
1296	Ack(Ack),
1297	/// `resize`.
1298	Resize(Resize),
1299	/// `theme`.
1300	Theme(Theme),
1301	/// `motion`.
1302	Motion(Motion),
1303	/// `visible`.
1304	Visible(Visible),
1305	/// `toggle`.
1306	Toggle(Toggle),
1307	/// `select`.
1308	Select(Select),
1309	/// `activate`.
1310	Activate(Select),
1311	/// `action`.
1312	Action(Action),
1313	/// `change`.
1314	Change(Change),
1315	/// `focus`.
1316	Focus(Focus),
1317	/// `edit`.
1318	Edit(Edit),
1319	/// `undo`.
1320	Undo(Undo),
1321	/// `send`.
1322	Send(SendText),
1323	/// `error`.
1324	Error(ErrorEvent),
1325	/// `gone`.
1326	Gone(Gone),
1327	/// Any other event, or a known one whose fields don't fit, kept raw.
1328	Unknown(Map<String, Value>),
1329}
1330
1331impl Event {
1332	/// Decodes an event body; fields that don't fit keep it raw.
1333	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	/// The event as received.
1361	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	/// The event's name (`ev`).
1383	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	/// The surface the event is about, when it names one.
1392	pub fn sf(&self) -> Option<&str> {
1393		self.raw().get("sf").and_then(Value::as_str)
1394	}
1395
1396	/// The node the event is about, when it names one.
1397	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}