1use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12use super::oref::ORef;
13
14fn serialize_meta_as_null_if_empty<S>(meta: &MetaMapType, serializer: S) -> Result<S::Ok, S::Error>
20where
21 S: serde::Serializer,
22{
23 if meta.is_empty() {
24 serializer.serialize_none()
25 } else {
26 meta.serialize(serializer)
27 }
28}
29
30fn deserialize_meta_or_null<'de, D>(deserializer: D) -> Result<MetaMapType, D::Error>
31where
32 D: serde::Deserializer<'de>,
33{
34 Option::<MetaMapType>::deserialize(deserializer).map(|opt| opt.unwrap_or_default())
35}
36
37pub const OTYPE_CLIENT: &str = "client";
40pub const OTYPE_WINDOW: &str = "window";
41pub const OTYPE_WORKSPACE: &str = "workspace";
42pub const OTYPE_TAB: &str = "tab";
43pub const OTYPE_LAYOUT: &str = "layout";
44pub const OTYPE_BLOCK: &str = "block";
45pub const OTYPE_TEMP: &str = "temp";
46
47pub const VALID_OTYPES: &[&str] = &[
48 OTYPE_CLIENT,
49 OTYPE_WINDOW,
50 OTYPE_WORKSPACE,
51 OTYPE_TAB,
52 OTYPE_LAYOUT,
53 OTYPE_BLOCK,
54 OTYPE_TEMP,
55];
56
57pub type MetaMapType = HashMap<String, serde_json::Value>;
61
62pub fn merge_meta(base: &MetaMapType, update: &MetaMapType, merge_special: bool) -> MetaMapType {
67 let mut result = base.clone();
68
69 for (k, v) in update {
71 if !k.ends_with(":*") {
72 continue;
73 }
74 let is_true = matches!(v, serde_json::Value::Bool(true));
76 if !is_true {
77 continue;
78 }
79 let prefix = k.trim_end_matches(":*");
80 if prefix.is_empty() {
81 continue;
82 }
83 let prefix_colon = format!("{prefix}:");
84 result.retain(|k2, _| k2 != prefix && !k2.starts_with(&prefix_colon));
85 }
86
87 for (k, v) in update {
89 if !merge_special && k.starts_with("display:") {
90 continue;
91 }
92 if k.ends_with(":*") {
93 continue;
94 }
95 if v.is_null() {
96 result.remove(k);
97 continue;
98 }
99 result.insert(k.clone(), v.clone());
100 }
101
102 result
103}
104
105pub fn meta_get_string(meta: &MetaMapType, key: &str, default: &str) -> String {
107 meta.get(key)
108 .and_then(|v| v.as_str())
109 .unwrap_or(default)
110 .to_string()
111}
112
113pub fn block_meta_agent_id(meta: &MetaMapType) -> Option<&str> {
116 meta.get("agentId")
117 .and_then(|v| v.as_str())
118 .or_else(|| meta.get("agent:id").and_then(|v| v.as_str()))
119}
120
121pub fn meta_get_bool(meta: &MetaMapType, key: &str, default: bool) -> bool {
123 meta.get(key).and_then(|v| v.as_bool()).unwrap_or(default)
124}
125
126pub trait StoreObj: Serialize + for<'de> Deserialize<'de> {
131 fn get_otype() -> &'static str;
132 fn get_oid(&self) -> &str;
133 #[allow(dead_code)]
134 fn set_oid(&mut self, oid: String);
135 #[allow(dead_code)]
136 fn get_version(&self) -> i64;
137 fn set_version(&mut self, version: i64);
138 #[allow(dead_code)]
139 fn get_meta(&self) -> &MetaMapType;
140 #[allow(dead_code)]
141 fn set_meta(&mut self, meta: MetaMapType);
142
143 #[allow(dead_code)]
144 fn oref(&self) -> ORef {
145 ORef::new(Self::get_otype(), self.get_oid())
146 }
147}
148
149macro_rules! impl_mux_obj {
152 ($ty:ty, $otype:expr) => {
153 impl StoreObj for $ty {
154 fn get_otype() -> &'static str {
155 $otype
156 }
157 fn get_oid(&self) -> &str {
158 &self.oid
159 }
160 fn set_oid(&mut self, oid: String) {
161 self.oid = oid;
162 }
163 fn get_version(&self) -> i64 {
164 self.version
165 }
166 fn set_version(&mut self, version: i64) {
167 self.version = version;
168 }
169 fn get_meta(&self) -> &MetaMapType {
170 &self.meta
171 }
172 fn set_meta(&mut self, meta: MetaMapType) {
173 self.meta = meta;
174 }
175 }
176 };
177}
178
179pub const UPDATE_TYPE_UPDATE: &str = "update";
182#[allow(dead_code)]
183pub const UPDATE_TYPE_DELETE: &str = "delete";
184
185#[allow(dead_code)]
188#[derive(Debug, Clone, Serialize, Deserialize, Default)]
189pub struct UIContext {
190 #[serde(rename = "windowid")]
191 pub window_id: String,
192 #[serde(rename = "activetabid")]
193 pub active_tab_id: String,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize, Default)]
199pub struct Point {
200 pub x: i64,
201 pub y: i64,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize, Default)]
205pub struct WinSize {
206 pub width: i64,
207 pub height: i64,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize, Default)]
211pub struct TermSize {
212 pub rows: i64,
213 pub cols: i64,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize, Default)]
219pub struct RuntimeOpts {
220 #[serde(default, skip_serializing_if = "is_default_term_size")]
221 pub termsize: TermSize,
222 #[serde(default, skip_serializing_if = "is_default_win_size")]
223 pub winsize: WinSize,
224}
225
226fn is_default_term_size(ts: &TermSize) -> bool {
227 ts.rows == 0 && ts.cols == 0
228}
229fn is_default_win_size(ws: &WinSize) -> bool {
230 ws.width == 0 && ws.height == 0
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize, Default)]
236pub struct FileDef {
237 #[serde(default, skip_serializing_if = "String::is_empty")]
238 pub content: String,
239 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub meta: Option<HashMap<String, serde_json::Value>>,
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize, Default)]
244pub struct BlockDef {
245 #[serde(default, skip_serializing_if = "Option::is_none")]
246 pub files: Option<HashMap<String, FileDef>>,
247 #[serde(default, skip_serializing_if = "MetaMapType::is_empty")]
248 pub meta: MetaMapType,
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct StickerClickOpts {
255 #[serde(default, skip_serializing_if = "String::is_empty")]
256 pub sendinput: String,
257 #[serde(default, skip_serializing_if = "Option::is_none")]
258 pub createblock: Option<BlockDef>,
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct StickerDisplayOpts {
263 #[serde(default)]
264 pub icon: String,
265 #[serde(default)]
266 pub imgsrc: String,
267 #[serde(default, skip_serializing_if = "String::is_empty")]
268 pub svgblob: String,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct StickerType {
273 pub stickertype: String,
274 pub style: HashMap<String, serde_json::Value>,
275 #[serde(default, skip_serializing_if = "Option::is_none")]
276 pub clickopts: Option<StickerClickOpts>,
277 pub display: StickerDisplayOpts,
278}
279
280#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
283pub struct LayoutActionData {
284 pub actiontype: String,
285 pub actionid: String,
286 pub blockid: String,
287 #[serde(default, skip_serializing_if = "Option::is_none")]
288 pub nodesize: Option<u32>,
289 #[serde(default, skip_serializing_if = "Option::is_none")]
296 pub nodesizefraction: Option<f64>,
297 #[serde(default, skip_serializing_if = "Option::is_none")]
298 pub indexarr: Option<Vec<i32>>,
299 #[serde(default)]
300 pub focused: bool,
301 #[serde(default)]
302 pub magnified: bool,
303 #[serde(default)]
304 pub ephemeral: bool,
305 #[serde(default, skip_serializing_if = "String::is_empty")]
306 pub targetblockid: String,
307 #[serde(default, skip_serializing_if = "String::is_empty")]
308 pub position: String,
309}
310
311#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
312pub struct LeafOrderEntry {
313 pub nodeid: String,
314 pub blockid: String,
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize, Default)]
323pub struct Client {
324 pub oid: String,
325 pub version: i64,
326 #[serde(default)]
327 pub windowids: Vec<String>,
328 #[serde(default, serialize_with = "serialize_meta_as_null_if_empty", deserialize_with = "deserialize_meta_or_null")]
329 pub meta: MetaMapType,
330 #[serde(default, skip_serializing_if = "is_zero_i64")]
331 pub tosagreed: i64,
332 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
333 pub hasoldhistory: bool,
334 #[serde(default, skip_serializing_if = "String::is_empty")]
335 pub tempoid: String,
336}
337
338impl_mux_obj!(Client, OTYPE_CLIENT);
339
340#[derive(Debug, Clone, Serialize, Deserialize, Default)]
342pub struct Window {
343 pub oid: String,
344 pub version: i64,
345 #[serde(default)]
346 pub workspaceid: String,
347 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
348 pub isnew: bool,
349 #[serde(default)]
350 pub pos: Point,
351 #[serde(default)]
352 pub winsize: WinSize,
353 #[serde(default)]
354 pub lastfocusts: i64,
355 #[serde(default, skip_serializing_if = "Option::is_none")]
364 pub opacity: Option<f32>,
365 #[serde(default, skip_serializing_if = "Option::is_none")]
374 pub kind: Option<String>,
375 #[serde(default, skip_serializing_if = "Option::is_none")]
380 pub parent_window_id: Option<String>,
381 #[serde(default, serialize_with = "serialize_meta_as_null_if_empty", deserialize_with = "deserialize_meta_or_null")]
382 pub meta: MetaMapType,
383}
384
385impl_mux_obj!(Window, OTYPE_WINDOW);
386
387#[derive(Debug, Clone, Serialize, Deserialize, Default)]
389pub struct Workspace {
390 pub oid: String,
391 pub version: i64,
392 #[serde(default, skip_serializing_if = "String::is_empty")]
393 pub name: String,
394 #[serde(default)]
395 pub tabids: Vec<String>,
396 #[serde(default)]
397 pub pinnedtabids: Vec<String>,
398 #[serde(default)]
399 pub activetabid: String,
400 #[serde(default, serialize_with = "serialize_meta_as_null_if_empty", deserialize_with = "deserialize_meta_or_null")]
401 pub meta: MetaMapType,
402}
403
404impl_mux_obj!(Workspace, OTYPE_WORKSPACE);
405
406#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct WorkspaceListEntry {
410 pub workspaceid: String,
411 #[serde(default)]
412 pub windowid: String,
413}
414
415#[derive(Debug, Clone, Serialize, Deserialize, Default)]
417pub struct Tab {
418 pub oid: String,
419 pub version: i64,
420 #[serde(default)]
421 pub name: String,
422 #[serde(default)]
423 pub layoutstate: String,
424 #[serde(default)]
425 pub blockids: Vec<String>,
426 #[serde(default, serialize_with = "serialize_meta_as_null_if_empty", deserialize_with = "deserialize_meta_or_null")]
427 pub meta: MetaMapType,
428}
429
430impl_mux_obj!(Tab, OTYPE_TAB);
431
432pub use agentmux_common::{FlexDirection, LayoutNode, LayoutNodeData};
439
440#[derive(Debug, Clone, Serialize, Deserialize, Default)]
442pub struct LayoutState {
443 pub oid: String,
444 pub version: i64,
445 #[serde(default, skip_serializing_if = "Option::is_none")]
448 pub rootnode: Option<LayoutNode>,
449 #[serde(default, skip_serializing_if = "String::is_empty")]
450 pub magnifiednodeid: String,
451 #[serde(default, skip_serializing_if = "String::is_empty")]
452 pub focusednodeid: String,
453 #[serde(default, skip_serializing_if = "Option::is_none")]
454 pub leaforder: Option<Vec<LeafOrderEntry>>,
455 #[serde(default, skip_serializing_if = "Option::is_none")]
456 pub pendingbackendactions: Option<Vec<LayoutActionData>>,
457 #[serde(default, skip_serializing_if = "Option::is_none")]
458 pub meta: Option<MetaMapType>,
459}
460
461impl StoreObj for LayoutState {
463 fn get_otype() -> &'static str {
464 OTYPE_LAYOUT
465 }
466 fn get_oid(&self) -> &str {
467 &self.oid
468 }
469 fn set_oid(&mut self, oid: String) {
470 self.oid = oid;
471 }
472 fn get_version(&self) -> i64 {
473 self.version
474 }
475 fn set_version(&mut self, version: i64) {
476 self.version = version;
477 }
478 fn get_meta(&self) -> &MetaMapType {
479 static EMPTY: std::sync::LazyLock<MetaMapType> = std::sync::LazyLock::new(MetaMapType::new);
480 self.meta.as_ref().unwrap_or(&EMPTY)
481 }
482 fn set_meta(&mut self, meta: MetaMapType) {
483 self.meta = Some(meta);
484 }
485}
486
487#[derive(Debug, Clone, Serialize, Deserialize, Default)]
489pub struct Block {
490 pub oid: String,
491 #[serde(default, skip_serializing_if = "String::is_empty")]
492 pub parentoref: String,
493 pub version: i64,
494 #[serde(default, skip_serializing_if = "Option::is_none")]
495 pub runtimeopts: Option<RuntimeOpts>,
496 #[serde(default, skip_serializing_if = "Option::is_none")]
497 pub stickers: Option<Vec<StickerType>>,
498 #[serde(default, serialize_with = "serialize_meta_as_null_if_empty", deserialize_with = "deserialize_meta_or_null")]
499 pub meta: MetaMapType,
500 #[serde(default, skip_serializing_if = "Option::is_none")]
501 pub subblockids: Option<Vec<String>>,
502}
503
504impl_mux_obj!(Block, OTYPE_BLOCK);
505
506#[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS)]
511#[ts(export, export_to = "../../../frontend/types/rpc/")]
512pub struct MuxObjUpdate {
513 pub updatetype: String,
514 pub otype: String,
515 pub oid: String,
516 #[serde(default, skip_serializing_if = "Option::is_none")]
518 #[ts(optional, type = "MuxObj")]
519 pub obj: Option<serde_json::Value>,
520}
521
522#[derive(Debug, Clone, Serialize, ts_rs::TS)]
524#[ts(export, export_to = "../../../frontend/types/rpc/")]
525pub struct MuxObjBatch(pub Vec<MuxObjUpdate>);
526
527use crate::backend::rpc_types::is_zero_i64;
530
531pub fn mux_obj_to_json<T: StoreObj>(obj: &T) -> Result<Vec<u8>, serde_json::Error> {
534 let mut map = serde_json::to_value(obj)?;
535 if let Some(m) = map.as_object_mut() {
536 m.insert("otype".to_string(), serde_json::Value::String(T::get_otype().to_string()));
537 }
538 serde_json::to_vec(&map)
539}
540
541pub fn mux_obj_to_value<T: StoreObj>(obj: &T) -> serde_json::Value {
544 let mut map = serde_json::to_value(obj).unwrap_or_default();
545 if let Some(m) = map.as_object_mut() {
546 m.insert("otype".to_string(), serde_json::Value::String(T::get_otype().to_string()));
547 }
548 map
549}
550
551pub fn mux_obj_from_json<T: StoreObj>(data: &[u8]) -> Result<T, serde_json::Error> {
554 serde_json::from_slice(data)
555}
556
557#[cfg(test)]
562mod tests {
563 #[test]
564 fn block_meta_agent_id_prefers_agent_id_over_the_legacy_key() {
565 use super::{block_meta_agent_id, MetaMapType};
566 let meta = |pairs: &[(&str, serde_json::Value)]| -> MetaMapType {
567 pairs.iter().map(|(k, v)| (k.to_string(), v.clone())).collect()
568 };
569 let both = meta(&[("agentId", "new".into()), ("agent:id", "old".into())]);
570 assert_eq!(block_meta_agent_id(&both), Some("new"));
571 let legacy = meta(&[("agent:id", "old".into())]);
572 assert_eq!(block_meta_agent_id(&legacy), Some("old"));
573 let non_string = meta(&[("agentId", 7.into()), ("agent:id", "old".into())]);
574 assert_eq!(block_meta_agent_id(&non_string), Some("old"));
575 let empty = meta(&[("agentId", "".into()), ("agent:id", "old".into())]);
576 assert_eq!(block_meta_agent_id(&empty), Some(""));
577 assert_eq!(block_meta_agent_id(&MetaMapType::new()), None);
578 }
579
580 #[test]
603 fn block_def_and_file_def_serialize_identically_in_both_modules() {
604 use crate::backend::wconfig::types as wc;
605
606 let a = serde_json::to_string(&super::BlockDef::default()).unwrap();
608 let b = serde_json::to_string(&wc::BlockDef::default()).unwrap();
609 assert_eq!(a, "{}", "obj::BlockDef default must serialize to {{}}");
610 assert_eq!(a, b, "the two BlockDef structs disagree on the empty case");
611
612 let a = serde_json::to_string(&super::FileDef::default()).unwrap();
613 let b = serde_json::to_string(&wc::FileDef::default()).unwrap();
614 assert_eq!(a, "{}", "obj::FileDef default must serialize to {{}}");
615 assert_eq!(a, b, "the two FileDef structs disagree on the empty case");
616
617 let mut oa = super::FileDef::default();
620 oa.content = "x".into();
621 let mut ob = wc::FileDef::default();
622 ob.content = "x".into();
623 assert_eq!(
624 serde_json::to_string(&oa).unwrap(),
625 serde_json::to_string(&ob).unwrap(),
626 "the two FileDef structs disagree once populated"
627 );
628 }
629
630 use super::*;
631
632 #[test]
633 fn test_client_roundtrip() {
634 let client = Client {
635 oid: "550e8400-e29b-41d4-a716-446655440000".to_string(),
636 version: 3,
637 windowids: vec!["w1".to_string(), "w2".to_string()],
638 meta: MetaMapType::new(),
639 tosagreed: 1700000000000,
640 ..Default::default()
641 };
642 let json = mux_obj_to_json(&client).unwrap();
643 let parsed: Client = mux_obj_from_json(&json).unwrap();
644 assert_eq!(parsed.oid, client.oid);
645 assert_eq!(parsed.version, client.version);
646 assert_eq!(parsed.windowids, client.windowids);
647 assert_eq!(parsed.tosagreed, client.tosagreed);
648 }
649
650 #[test]
651 fn test_window_roundtrip() {
652 let window = Window {
653 oid: "550e8400-e29b-41d4-a716-446655440001".to_string(),
654 version: 1,
655 workspaceid: "ws-123".to_string(),
656 pos: Point { x: 100, y: 200 },
657 winsize: WinSize {
658 width: 1920,
659 height: 1080,
660 },
661 lastfocusts: 1700000000000,
662 meta: MetaMapType::new(),
663 ..Default::default()
664 };
665 let json = mux_obj_to_json(&window).unwrap();
666 let parsed: Window = mux_obj_from_json(&json).unwrap();
667 assert_eq!(parsed.workspaceid, "ws-123");
668 assert_eq!(parsed.pos.x, 100);
669 assert_eq!(parsed.winsize.width, 1920);
670 }
671
672 #[test]
673 fn test_workspace_roundtrip() {
674 let ws = Workspace {
675 oid: "ws-oid".to_string(),
676 version: 2,
677 name: "My Workspace".to_string(),
678 tabids: vec!["t1".to_string(), "t2".to_string()],
679 pinnedtabids: vec!["t0".to_string()],
680 activetabid: "t1".to_string(),
681 meta: MetaMapType::new(),
682 ..Default::default()
683 };
684 let json = mux_obj_to_json(&ws).unwrap();
685 let parsed: Workspace = mux_obj_from_json(&json).unwrap();
686 assert_eq!(parsed.name, "My Workspace");
687 assert_eq!(parsed.tabids.len(), 2);
688 assert_eq!(parsed.pinnedtabids, vec!["t0"]);
689 }
690
691 #[test]
692 fn test_tab_roundtrip() {
693 let tab = Tab {
694 oid: "tab-oid".to_string(),
695 version: 1,
696 name: "Tab 1".to_string(),
697 layoutstate: "ls-123".to_string(),
698 blockids: vec!["b1".to_string(), "b2".to_string()],
699 meta: MetaMapType::new(),
700 };
701 let json = mux_obj_to_json(&tab).unwrap();
702 let parsed: Tab = mux_obj_from_json(&json).unwrap();
703 assert_eq!(parsed.name, "Tab 1");
704 assert_eq!(parsed.blockids.len(), 2);
705 }
706
707 #[test]
708 fn test_layout_state_roundtrip() {
709 let ls = LayoutState {
711 oid: "ls-oid".to_string(),
712 version: 1,
713 rootnode: Some(LayoutNode {
714 id: "node-1".into(),
715 flex_direction: FlexDirection::Row,
716 size: 1.0,
717 children: Vec::new(),
718 data: None,
719 ..Default::default()
720 }),
721 magnifiednodeid: "node-1".to_string(),
722 ..Default::default()
723 };
724 let json = mux_obj_to_json(&ls).unwrap();
725 let parsed: LayoutState = mux_obj_from_json(&json).unwrap();
726 assert_eq!(parsed.magnifiednodeid, "node-1");
727 assert!(parsed.rootnode.is_some());
728 assert_eq!(parsed.rootnode.as_ref().unwrap().id, "node-1");
729 }
730
731 #[test]
739 fn test_layout_node_serde_compat_with_frontend_shapes() {
740 let leaf_json = serde_json::json!({
742 "id": "node-uuid-1",
743 "data": { "blockId": "block-uuid-1" },
744 "flexDirection": "row",
745 "size": 1
746 });
747 let leaf: LayoutNode = serde_json::from_value(leaf_json).unwrap();
748 assert_eq!(leaf.id, "node-uuid-1");
749 assert_eq!(leaf.flex_direction, FlexDirection::Row);
750 assert_eq!(leaf.size, 1.0);
751 assert!(leaf.children.is_empty());
752 assert_eq!(leaf.data.as_ref().unwrap().block_id, "block-uuid-1");
753 let reserialized = serde_json::to_value(&leaf).unwrap();
755 let reparsed: LayoutNode = serde_json::from_value(reserialized).unwrap();
756 assert_eq!(leaf, reparsed);
757
758 let four_pane_json = serde_json::json!({
764 "id": "root-id",
765 "flexDirection": "row",
766 "size": 10,
767 "children": [
768 {
769 "id": "agent-id",
770 "flexDirection": "column",
771 "size": 5,
772 "data": { "blockId": "agent-block" }
773 },
774 {
775 "id": "right-col-id",
776 "flexDirection": "column",
777 "size": 5,
778 "children": [
779 {
780 "id": "swarm-id",
781 "flexDirection": "row",
782 "size": 4,
783 "data": { "blockId": "swarm-block" }
784 },
785 {
786 "id": "memory-id",
787 "flexDirection": "row",
788 "size": 4,
789 "data": { "blockId": "memory-block" }
790 },
791 {
792 "id": "sysinfo-id",
793 "flexDirection": "row",
794 "size": 2,
795 "data": { "blockId": "sysinfo-block" }
796 }
797 ]
798 }
799 ]
800 });
801 let root: LayoutNode = serde_json::from_value(four_pane_json).unwrap();
802 assert_eq!(root.id, "root-id");
803 assert_eq!(root.children.len(), 2);
804 assert_eq!(root.children[0].id, "agent-id");
805 assert_eq!(root.children[0].flex_direction, FlexDirection::Column);
806 assert_eq!(root.children[0].data.as_ref().unwrap().block_id, "agent-block");
807 let right_col = &root.children[1];
808 assert_eq!(right_col.children.len(), 3);
809 assert_eq!(right_col.data, None); assert_eq!(right_col.children[0].data.as_ref().unwrap().block_id, "swarm-block");
811 assert_eq!(right_col.children[1].data.as_ref().unwrap().block_id, "memory-block");
812 assert_eq!(right_col.children[2].data.as_ref().unwrap().block_id, "sysinfo-block");
813 let reserialized = serde_json::to_value(&root).unwrap();
814 let reparsed: LayoutNode = serde_json::from_value(reserialized).unwrap();
815 assert_eq!(root, reparsed);
816
817 let leaf_no_children = serde_json::json!({
820 "id": "loner",
821 "flexDirection": "column",
822 "size": 5,
823 "data": { "blockId": "b" }
824 });
825 let parsed: LayoutNode = serde_json::from_value(leaf_no_children).unwrap();
826 assert!(parsed.children.is_empty());
827 }
828
829 #[test]
832 fn test_layout_node_defaults_flex_direction_to_row() {
833 let no_dir = serde_json::json!({"id": "x", "size": 1});
834 let parsed: LayoutNode = serde_json::from_value(no_dir).unwrap();
835 assert_eq!(parsed.flex_direction, FlexDirection::Row);
836 }
837
838 #[test]
843 fn test_layout_node_size_serializes_as_integer_when_whole() {
844 let whole = LayoutNode {
845 id: "x".into(),
846 flex_direction: FlexDirection::Row,
847 size: 10.0,
848 children: Vec::new(),
849 data: None,
850 ..Default::default()
851 };
852 let json = serde_json::to_string(&whole).unwrap();
853 assert!(json.contains("\"size\":10"), "whole sizes should serialize as integer; got {}", json);
854 assert!(!json.contains("\"size\":10.0"), "whole sizes should NOT include the decimal; got {}", json);
855
856 let frac = LayoutNode {
858 id: "y".into(),
859 flex_direction: FlexDirection::Row,
860 size: 5.5,
861 children: Vec::new(),
862 data: None,
863 ..Default::default()
864 };
865 let json = serde_json::to_string(&frac).unwrap();
866 assert!(json.contains("\"size\":5.5"), "fractional sizes preserve the decimal; got {}", json);
867
868 let reparsed: LayoutNode = serde_json::from_str(&serde_json::to_string(&whole).unwrap()).unwrap();
870 assert_eq!(reparsed.size, 10.0);
871 }
872
873 #[test]
876 fn test_layout_node_data_skipped_when_none() {
877 let group = LayoutNode {
878 id: "g".into(),
879 flex_direction: FlexDirection::Row,
880 size: 1.0,
881 children: Vec::new(),
882 data: None,
883 ..Default::default()
884 };
885 let json = serde_json::to_value(&group).unwrap();
886 assert!(json.get("data").is_none(), "data: None must skip-serialize");
887 }
888
889 #[test]
895 fn test_layout_node_preserves_unknown_fields() {
896 let json_with_extras = serde_json::json!({
897 "id": "n",
898 "flexDirection": "row",
899 "size": 1,
900 "backendLayoutVersion": 7,
901 "data": {
902 "blockId": "b",
903 "renderHint": "compact"
904 }
905 });
906 let node: LayoutNode = serde_json::from_value(json_with_extras).unwrap();
907 assert_eq!(node.extra.get("backendLayoutVersion"), Some(&serde_json::json!(7)));
908 assert_eq!(
909 node.data.as_ref().unwrap().extra.get("renderHint"),
910 Some(&serde_json::json!("compact"))
911 );
912 let reserialized = serde_json::to_value(&node).unwrap();
914 assert_eq!(reserialized.get("backendLayoutVersion"), Some(&serde_json::json!(7)));
915 assert_eq!(
916 reserialized.get("data").and_then(|d| d.get("renderHint")),
917 Some(&serde_json::json!("compact"))
918 );
919 let no_extras = LayoutNode {
921 id: "x".into(),
922 flex_direction: FlexDirection::Row,
923 size: 1.0,
924 ..Default::default()
925 };
926 let json = serde_json::to_value(&no_extras).unwrap();
927 assert!(json.get("extra").is_none());
928 }
929
930 #[test]
936 fn test_layout_node_data_missing_block_id_fails_deserialization() {
937 let missing_id = serde_json::json!({ "renderHint": "compact" });
938 let result = serde_json::from_value::<LayoutNodeData>(missing_id);
939 assert!(result.is_err(), "missing blockId must fail deserialization");
940 }
941
942 #[test]
943 fn test_block_roundtrip() {
944 let block = Block {
945 oid: "blk-oid".to_string(),
946 version: 5,
947 parentoref: "tab:parent-id".to_string(),
948 meta: {
949 let mut m = MetaMapType::new();
950 m.insert("view".to_string(), serde_json::json!("term"));
951 m
952 },
953 ..Default::default()
954 };
955 let json = mux_obj_to_json(&block).unwrap();
956 let parsed: Block = mux_obj_from_json(&json).unwrap();
957 assert_eq!(parsed.parentoref, "tab:parent-id");
958 assert_eq!(
959 parsed.meta.get("view").and_then(|v| v.as_str()),
960 Some("term")
961 );
962 }
963
964 #[test]
965 fn test_wire_compat_go_json_client() {
966 let go_json = r#"{"oid":"abc-123","otype":"client","version":2,"windowids":["w1"],"meta":{"view":"term"},"tosagreed":1700000000000}"#;
968 let client: Client = serde_json::from_str(go_json).unwrap();
969 assert_eq!(client.oid, "abc-123");
970 assert_eq!(client.version, 2);
971 assert_eq!(client.windowids, vec!["w1"]);
972 assert_eq!(client.tosagreed, 1700000000000);
973 }
974
975 #[test]
976 fn test_wire_compat_go_json_block() {
977 let go_json = r#"{"oid":"blk-1","otype":"block","version":3,"parentoref":"tab:t1","meta":{"view":"term","cmd":"ls"}}"#;
978 let block: Block = serde_json::from_str(go_json).unwrap();
979 assert_eq!(block.oid, "blk-1");
980 assert_eq!(block.parentoref, "tab:t1");
981 assert_eq!(block.version, 3);
982 }
983
984 #[test]
985 fn test_wire_compat_go_json_tab() {
986 let go_json = r#"{"oid":"tab-1","otype":"tab","version":1,"name":"Shell","layoutstate":"ls-1","blockids":["b1","b2"],"meta":{}}"#;
987 let tab: Tab = serde_json::from_str(go_json).unwrap();
988 assert_eq!(tab.name, "Shell");
989 assert_eq!(tab.blockids, vec!["b1", "b2"]);
990 }
991
992 #[test]
993 fn test_mux_obj_to_json_includes_otype() {
994 let client = Client {
995 oid: "test".to_string(),
996 version: 1,
997 ..Default::default()
998 };
999 let json_bytes = mux_obj_to_json(&client).unwrap();
1000 let v: serde_json::Value = serde_json::from_slice(&json_bytes).unwrap();
1001 assert_eq!(v["otype"], "client");
1002 }
1003
1004 #[test]
1005 fn test_merge_meta_basic() {
1006 let mut base = MetaMapType::new();
1007 base.insert("view".into(), serde_json::json!("term"));
1008 base.insert("cmd".into(), serde_json::json!("ls"));
1009
1010 let mut update = MetaMapType::new();
1011 update.insert("cmd".into(), serde_json::json!("pwd"));
1012 update.insert("icon".into(), serde_json::json!("star"));
1013
1014 let result = merge_meta(&base, &update, true);
1015 assert_eq!(result["view"], "term");
1016 assert_eq!(result["cmd"], "pwd");
1017 assert_eq!(result["icon"], "star");
1018 }
1019
1020 #[test]
1021 fn test_merge_meta_null_deletes() {
1022 let mut base = MetaMapType::new();
1023 base.insert("view".into(), serde_json::json!("term"));
1024 base.insert("cmd".into(), serde_json::json!("ls"));
1025
1026 let mut update = MetaMapType::new();
1027 update.insert("cmd".into(), serde_json::Value::Null);
1028
1029 let result = merge_meta(&base, &update, true);
1030 assert_eq!(result.get("view").unwrap(), "term");
1031 assert!(!result.contains_key("cmd"));
1032 }
1033
1034 #[test]
1035 fn test_merge_meta_section_clear() {
1036 let mut base = MetaMapType::new();
1037 base.insert("frame".into(), serde_json::json!(true));
1038 base.insert("frame:title".into(), serde_json::json!("hello"));
1039 base.insert("frame:icon".into(), serde_json::json!("star"));
1040 base.insert("cmd".into(), serde_json::json!("ls"));
1041
1042 let mut update = MetaMapType::new();
1043 update.insert("frame:*".into(), serde_json::json!(true));
1044
1045 let result = merge_meta(&base, &update, true);
1046 assert!(!result.contains_key("frame"));
1047 assert!(!result.contains_key("frame:title"));
1048 assert!(!result.contains_key("frame:icon"));
1049 assert_eq!(result["cmd"], "ls"); }
1051
1052 #[test]
1053 fn test_merge_meta_skip_display_when_not_special() {
1054 let base = MetaMapType::new();
1055 let mut update = MetaMapType::new();
1056 update.insert("display:name".into(), serde_json::json!("test"));
1057 update.insert("view".into(), serde_json::json!("term"));
1058
1059 let result = merge_meta(&base, &update, false);
1060 assert!(!result.contains_key("display:name"));
1061 assert_eq!(result["view"], "term");
1062 }
1063
1064 #[test]
1065 fn test_meta_get_string() {
1066 let mut meta = MetaMapType::new();
1067 meta.insert("view".into(), serde_json::json!("term"));
1068 assert_eq!(meta_get_string(&meta, "view", "default"), "term");
1069 assert_eq!(meta_get_string(&meta, "missing", "default"), "default");
1070 }
1071
1072 #[test]
1073 fn test_meta_get_bool() {
1074 let mut meta = MetaMapType::new();
1075 meta.insert("edit".into(), serde_json::json!(true));
1076 assert!(meta_get_bool(&meta, "edit", false));
1077 assert!(!meta_get_bool(&meta, "missing", false));
1078 }
1079}