1#![allow(dead_code)]
2use std::collections::HashMap;
15
16use serde::{Deserialize, Serialize};
17
18use super::obj;
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct WebCallType {
26 pub service: String,
27 pub method: String,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub uicontext: Option<UIContext>,
30 #[serde(default)]
31 pub args: Vec<serde_json::Value>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct UIContext {
38 #[serde(default, rename = "activetabid")]
39 pub active_tab_id: String,
40 #[serde(flatten)]
41 pub extra: HashMap<String, serde_json::Value>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct WebReturnType {
48 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
49 pub success: bool,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub error: Option<String>,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub data: Option<serde_json::Value>,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub updates: Option<Vec<obj::MuxObjUpdate>>,
56}
57
58impl WebReturnType {
59 pub fn success(data: serde_json::Value) -> Self {
61 Self {
62 success: true,
63 error: None,
64 data: Some(data),
65 updates: None,
66 }
67 }
68
69 pub fn success_empty() -> Self {
71 Self {
72 success: true,
73 error: None,
74 data: None,
75 updates: None,
76 }
77 }
78
79 pub fn success_with_updates(updates: Vec<obj::MuxObjUpdate>) -> Self {
81 Self {
82 success: true,
83 error: None,
84 data: None,
85 updates: if updates.is_empty() {
86 None
87 } else {
88 Some(updates)
89 },
90 }
91 }
92
93 pub fn success_data_updates(
95 data: serde_json::Value,
96 updates: Vec<obj::MuxObjUpdate>,
97 ) -> Self {
98 Self {
99 success: true,
100 error: None,
101 data: Some(data),
102 updates: if updates.is_empty() {
103 None
104 } else {
105 Some(updates)
106 },
107 }
108 }
109
110 pub fn error(msg: impl Into<String>) -> Self {
112 Self {
113 success: false,
114 error: Some(msg.into()),
115 data: None,
116 updates: None,
117 }
118 }
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct MethodMeta {
130 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub desc: Option<String>,
132 #[serde(default, skip_serializing_if = "Vec::is_empty")]
133 pub arg_names: Vec<String>,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub return_desc: Option<String>,
136}
137
138pub fn get_method_meta(service: &str, method: &str) -> Option<MethodMeta> {
140 match (service, method) {
141 ("block", "SendCommand") => Some(MethodMeta {
143 desc: Some("send command to block".into()),
144 arg_names: vec!["blockid".into(), "cmd".into()],
145 return_desc: None,
146 }),
147 ("block", "GetControllerStatus") => Some(MethodMeta {
148 desc: Some("get block controller status".into()),
149 arg_names: vec!["blockid".into()],
150 return_desc: None,
151 }),
152 ("block", "SaveTerminalState") => Some(MethodMeta {
153 desc: Some("save the terminal state to a blockfile".into()),
154 arg_names: vec![
155 "ctx".into(),
156 "blockId".into(),
157 "state".into(),
158 "stateType".into(),
159 "ptyOffset".into(),
160 "termSize".into(),
161 ],
162 return_desc: None,
163 }),
164
165 ("object", "GetObject") => Some(MethodMeta {
167 desc: Some("get wave object by oref".into()),
168 arg_names: vec!["oref".into()],
169 return_desc: None,
170 }),
171 ("object", "GetObjects") => Some(MethodMeta {
172 desc: Some("get multiple wave objects".into()),
173 arg_names: vec!["orefs".into()],
174 return_desc: Some("objects".into()),
175 }),
176 ("object", "UpdateTabName") => Some(MethodMeta {
177 desc: Some("update tab name".into()),
178 arg_names: vec!["uiContext".into(), "tabId".into(), "name".into()],
179 return_desc: None,
180 }),
181 ("object", "CreateBlock") => Some(MethodMeta {
182 desc: Some("create a new block".into()),
183 arg_names: vec![
193 "uiContext".into(),
194 "blockDef".into(),
195 "rtOpts".into(),
196 "tabId".into(),
197 ],
198 return_desc: Some("blockId".into()),
199 }),
200 ("object", "DeleteBlock") => Some(MethodMeta {
201 desc: Some("delete a block".into()),
202 arg_names: vec!["uiContext".into(), "blockId".into()],
203 return_desc: None,
204 }),
205 ("object", "QuitAgent") => Some(MethodMeta {
206 desc: Some("end an agent gracefully and close its own tab (/quit)".into()),
207 arg_names: vec!["uiContext".into(), "blockId".into()],
208 return_desc: Some("{status, agent, released_claims, stopped_shells, crons_targeting, survivors}".into()),
209 }),
210 ("object", "ClosePane") => Some(MethodMeta {
211 desc: Some("close a pane: stop and delete every block in its stack".into()),
212 arg_names: vec!["uiContext".into(), "blockIds".into()],
213 return_desc: None,
214 }),
215 ("object", "UpdateObjectMeta") => Some(MethodMeta {
216 desc: Some("update object meta".into()),
217 arg_names: vec!["uiContext".into(), "oref".into(), "meta".into()],
218 return_desc: None,
219 }),
220 ("object", "UpdateObject") => Some(MethodMeta {
221 desc: Some("update a wave object".into()),
222 arg_names: vec!["uiContext".into(), "muxObj".into(), "returnUpdates".into()],
223 return_desc: None,
224 }),
225
226 ("client", "GetClientData") => Some(MethodMeta {
228 desc: Some("get client data".into()),
229 arg_names: vec![],
230 return_desc: None,
231 }),
232 ("client", "GetTab") => Some(MethodMeta {
233 desc: Some("get tab by ID".into()),
234 arg_names: vec!["tabId".into()],
235 return_desc: None,
236 }),
237 ("client", "GetAllConnStatus") => Some(MethodMeta {
238 desc: Some("get all connection statuses".into()),
239 arg_names: vec![],
240 return_desc: None,
241 }),
242 ("client", "FocusWindow") => Some(MethodMeta {
243 desc: Some("focus a window".into()),
244 arg_names: vec!["windowId".into()],
245 return_desc: None,
246 }),
247 ("client", "AgreeTos") => Some(MethodMeta {
248 desc: Some("agree to terms of service".into()),
249 arg_names: vec![],
250 return_desc: None,
251 }),
252 ("client", "TelemetryUpdate") => Some(MethodMeta {
253 desc: Some("update telemetry setting".into()),
254 arg_names: vec!["telemetryEnabled".into()],
255 return_desc: None,
256 }),
257
258 ("window", "GetWindow") => Some(MethodMeta {
260 desc: Some("get window by ID".into()),
261 arg_names: vec!["windowId".into()],
262 return_desc: None,
263 }),
264 ("window", "CreateWindow") => Some(MethodMeta {
265 desc: Some("create a new window".into()),
266 arg_names: vec!["ctx".into(), "winSize".into(), "workspaceId".into()],
267 return_desc: None,
268 }),
269 ("window", "SetWindowPosAndSize") => Some(MethodMeta {
270 desc: Some("set window position and size".into()),
271 arg_names: vec!["ctx".into(), "windowId".into(), "pos".into(), "size".into()],
272 return_desc: None,
273 }),
274 ("window", "SetWindowOpacity") => Some(MethodMeta {
275 desc: Some("set (or clear, via null) a window's persisted opacity".into()),
276 arg_names: vec!["ctx".into(), "windowId".into(), "opacity".into()],
277 return_desc: None,
278 }),
279 ("window", "MarkWindowOpen") => Some(MethodMeta {
280 desc: Some("a mark to take before opening a window that should inherit appearance".into()),
281 arg_names: vec![],
282 return_desc: Some("mark".into()),
283 }),
284 ("window", "InheritWindowAppearance") => Some(MethodMeta {
285 desc: Some("copy a window's theme and opacity onto the window opened after the mark with this host label".into()),
286 arg_names: vec!["sourceWindowId".into(), "hostLabel".into(), "mark".into()],
287 return_desc: None,
288 }),
289 ("window", "SetWindowTopology") => Some(MethodMeta {
290 desc: Some("set a window's persisted kind + parent_window_id".into()),
291 arg_names: vec!["ctx".into(), "windowId".into(), "kind".into(), "parentWindowId".into()],
292 return_desc: None,
293 }),
294 ("window", "MoveBlockToNewWindow") => Some(MethodMeta {
295 desc: Some("move block to new window".into()),
296 arg_names: vec!["ctx".into(), "currentTabId".into(), "blockId".into()],
297 return_desc: None,
298 }),
299 ("window", "SwitchWorkspace") => Some(MethodMeta {
300 desc: Some("switch workspace".into()),
301 arg_names: vec!["ctx".into(), "windowId".into(), "workspaceId".into()],
302 return_desc: None,
303 }),
304 ("window", "CloseWindow") => Some(MethodMeta {
305 desc: Some("close a window".into()),
306 arg_names: vec!["ctx".into(), "windowId".into(), "fromElectron".into()],
307 return_desc: None,
308 }),
309 ("window", "SaveSessionSnapshot") => Some(MethodMeta {
310 desc: Some("flush the session snapshot without tearing down (OS shutdown)".into()),
311 arg_names: vec![],
312 return_desc: None,
313 }),
314
315 ("workspace", "CreateWorkspace") => Some(MethodMeta {
317 desc: Some("create a new workspace".into()),
318 arg_names: vec![
319 "ctx".into(),
320 "name".into(),
321 ],
322 return_desc: Some("workspaceId".into()),
323 }),
324 ("workspace", "UpdateWorkspace") => Some(MethodMeta {
325 desc: Some("update workspace properties".into()),
326 arg_names: vec![
327 "ctx".into(),
328 "workspaceId".into(),
329 "name".into(),
330 ],
331 return_desc: None,
332 }),
333 ("workspace", "GetWorkspace") => Some(MethodMeta {
334 desc: Some("get workspace by ID".into()),
335 arg_names: vec!["workspaceId".into()],
336 return_desc: Some("workspace".into()),
337 }),
338 ("workspace", "DeleteWorkspace") => Some(MethodMeta {
339 desc: Some("delete a workspace".into()),
340 arg_names: vec!["workspaceId".into()],
341 return_desc: None,
342 }),
343 ("workspace", "ListWorkspaces") => Some(MethodMeta {
344 desc: Some("list all workspaces".into()),
345 arg_names: vec![],
346 return_desc: None,
347 }),
348 ("workspace", "CreateTab") => Some(MethodMeta {
349 desc: Some("create a new tab".into()),
350 arg_names: vec![
351 "workspaceId".into(),
352 "tabName".into(),
353 "activateTab".into(),
354 "pinned".into(),
355 ],
356 return_desc: Some("tabId".into()),
357 }),
358 ("workspace", "UpdateTabIds") => Some(MethodMeta {
359 desc: Some("update tab ordering".into()),
360 arg_names: vec![
361 "uiContext".into(),
362 "workspaceId".into(),
363 "tabIds".into(),
364 "pinnedTabIds".into(),
365 ],
366 return_desc: None,
367 }),
368 ("workspace", "SetActiveTab") => Some(MethodMeta {
369 desc: Some("set active tab".into()),
370 arg_names: vec!["workspaceId".into(), "tabId".into()],
371 return_desc: None,
372 }),
373 ("workspace", "CloseTab") => Some(MethodMeta {
374 desc: Some("close a tab".into()),
375 arg_names: vec![
376 "ctx".into(),
377 "workspaceId".into(),
378 "tabId".into(),
379 "fromElectron".into(),
380 ],
381 return_desc: Some("CloseTabRtn".into()),
382 }),
383
384 ("userinput", "SendUserInputResponse") => Some(MethodMeta {
386 desc: Some("send user input response".into()),
387 arg_names: vec!["response".into()],
388 return_desc: None,
389 }),
390
391 _ => None,
392 }
393}
394
395#[derive(Debug, Clone, Serialize, Deserialize)]
400pub struct CloseTabRtnType {
401 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
402 pub closewindow: bool,
403 #[serde(default, skip_serializing_if = "String::is_empty")]
404 pub newactivetabid: String,
405}
406
407pub const SERVICES: &[&str] = &[
409 "block",
410 "object",
411 "client",
412 "window",
413 "workspace",
414 "userinput",
415];
416
417pub fn is_valid_service(name: &str) -> bool {
419 SERVICES.contains(&name)
420}
421
422pub fn get_arg<T: serde::de::DeserializeOwned>(
424 args: &[serde_json::Value],
425 idx: usize,
426) -> Result<T, String> {
427 let val = args
428 .get(idx)
429 .ok_or_else(|| format!("missing argument at index {}", idx))?;
430 serde_json::from_value(val.clone()).map_err(|e| format!("invalid argument at index {}: {}", idx, e))
431}
432
433pub fn get_optional_arg<T: serde::de::DeserializeOwned>(
436 args: &[serde_json::Value],
437 idx: usize,
438) -> Result<Option<T>, String> {
439 match args.get(idx) {
440 None | Some(serde_json::Value::Null) => Ok(None),
441 Some(val) => serde_json::from_value(val.clone())
442 .map(Some)
443 .map_err(|e| format!("invalid argument at index {}: {}", idx, e)),
444 }
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450
451 #[test]
452 fn test_web_call_type_deserialize() {
453 let json = r#"{
454 "service": "object",
455 "method": "GetObject",
456 "args": ["block:abc123"]
457 }"#;
458 let call: WebCallType = serde_json::from_str(json).unwrap();
459 assert_eq!(call.service, "object");
460 assert_eq!(call.method, "GetObject");
461 assert_eq!(call.args.len(), 1);
462 assert!(call.uicontext.is_none());
463 }
464
465 #[test]
466 fn test_web_call_type_with_uicontext() {
467 let json = r#"{
468 "service": "object",
469 "method": "CreateBlock",
470 "uicontext": {"activetabid": "tab-123"},
471 "args": [{"view": "term"}, {}]
472 }"#;
473 let call: WebCallType = serde_json::from_str(json).unwrap();
474 assert_eq!(call.service, "object");
475 assert_eq!(call.method, "CreateBlock");
476 assert_eq!(call.uicontext.as_ref().unwrap().active_tab_id, "tab-123");
477 assert_eq!(call.args.len(), 2);
478 }
479
480 #[test]
481 fn test_web_return_type_success() {
482 let rtn = WebReturnType::success(serde_json::json!("hello"));
483 assert!(rtn.success);
484 assert!(rtn.error.is_none());
485 assert_eq!(rtn.data, Some(serde_json::json!("hello")));
486 }
487
488 #[test]
489 fn test_web_return_type_error() {
490 let rtn = WebReturnType::error("something went wrong");
491 assert!(!rtn.success);
492 assert_eq!(rtn.error.as_deref(), Some("something went wrong"));
493 assert!(rtn.data.is_none());
494 }
495
496 #[test]
497 fn test_web_return_type_success_empty() {
498 let rtn = WebReturnType::success_empty();
499 assert!(rtn.success);
500 assert!(rtn.data.is_none());
501 assert!(rtn.updates.is_none());
502 }
503
504 #[test]
505 fn test_web_return_type_with_updates() {
506 let updates = vec![obj::MuxObjUpdate {
507 updatetype: "update".into(),
508 otype: "tab".into(),
509 oid: "123".into(),
510 obj: None,
511 }];
512 let rtn = WebReturnType::success_with_updates(updates);
513 assert!(rtn.success);
514 assert_eq!(rtn.updates.as_ref().unwrap().len(), 1);
515 }
516
517 #[test]
518 fn test_web_return_type_empty_updates_omitted() {
519 let rtn = WebReturnType::success_with_updates(vec![]);
520 let json = serde_json::to_string(&rtn).unwrap();
521 assert!(!json.contains("updates"));
522 }
523
524 #[test]
525 fn test_web_return_type_serde_roundtrip() {
526 let rtn = WebReturnType::success_data_updates(
527 serde_json::json!({"id": "block-1"}),
528 vec![obj::MuxObjUpdate {
529 updatetype: "update".into(),
530 otype: "block".into(),
531 oid: "abc".into(),
532 obj: Some(serde_json::json!({"otype": "block"})),
533 }],
534 );
535 let json = serde_json::to_string(&rtn).unwrap();
536 let parsed: WebReturnType = serde_json::from_str(&json).unwrap();
537 assert!(parsed.success);
538 assert!(parsed.data.is_some());
539 assert_eq!(parsed.updates.as_ref().unwrap().len(), 1);
540 }
541
542 #[test]
543 fn test_close_tab_rtn_type() {
544 let rtn = CloseTabRtnType {
545 closewindow: true,
546 newactivetabid: String::new(),
547 };
548 let json = serde_json::to_string(&rtn).unwrap();
549 assert!(json.contains("closewindow"));
550 assert!(!json.contains("newactivetabid")); }
552
553 #[test]
554 fn test_method_meta_exists() {
555 assert!(get_method_meta("object", "GetObject").is_some());
556 assert!(get_method_meta("workspace", "CreateTab").is_some());
557 assert!(get_method_meta("block", "SaveTerminalState").is_some());
558 assert!(get_method_meta("nonexistent", "Foo").is_none());
559 }
560
561 #[test]
562 fn test_is_valid_service() {
563 assert!(is_valid_service("block"));
564 assert!(is_valid_service("object"));
565 assert!(is_valid_service("workspace"));
566 assert!(!is_valid_service("invalid"));
567 assert!(!is_valid_service(""));
568 }
569
570 #[test]
571 fn test_get_arg() {
572 let args = vec![
573 serde_json::json!("hello"),
574 serde_json::json!(42),
575 serde_json::json!(true),
576 ];
577 assert_eq!(get_arg::<String>(&args, 0).unwrap(), "hello");
578 assert_eq!(get_arg::<i64>(&args, 1).unwrap(), 42);
579 assert_eq!(get_arg::<bool>(&args, 2).unwrap(), true);
580 assert!(get_arg::<String>(&args, 5).is_err());
581 }
582
583 #[test]
584 fn test_get_optional_arg() {
585 let args = vec![
586 serde_json::json!("hello"),
587 serde_json::Value::Null,
588 ];
589 assert_eq!(
590 get_optional_arg::<String>(&args, 0).unwrap(),
591 Some("hello".to_string())
592 );
593 assert_eq!(get_optional_arg::<String>(&args, 1).unwrap(), None);
594 assert_eq!(get_optional_arg::<String>(&args, 5).unwrap(), None);
595 }
596
597 #[test]
598 fn test_ui_context_deserialize() {
599 let json = r#"{"activetabid": "tab-1", "extra_field": true}"#;
600 let ctx: UIContext = serde_json::from_str(json).unwrap();
601 assert_eq!(ctx.active_tab_id, "tab-1");
602 assert_eq!(ctx.extra.get("extra_field"), Some(&serde_json::json!(true)));
603 }
604
605 #[test]
606 fn test_web_call_no_args() {
607 let json = r#"{"service": "client", "method": "GetClientData"}"#;
608 let call: WebCallType = serde_json::from_str(json).unwrap();
609 assert_eq!(call.service, "client");
610 assert!(call.args.is_empty());
611 }
612}