1use serde::{Deserialize, Serialize};
2
3use crate::types::*;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub enum Request {
7 NewSession { name: String, shell: Option<String>, #[serde(default)] layout: Option<String>, #[serde(default)] pane_alias: Option<String>, #[serde(default)] template: Option<String> },
9 KillSession { name: String },
10 ListSessions,
11 HasSession { name: String },
12
13 NewWindow { session: SessionId, shell: Option<String>, #[serde(default)] layout: Option<String>, #[serde(default)] pane_alias: Option<String> },
15 KillWindow { session: SessionId, window: WindowId },
16 ListWindows { session: SessionId },
17 SelectWindow { session: SessionId, window: WindowId },
18
19 SplitPane { target: String, horizontal: bool, shell: Option<String>, #[serde(default)] pane_alias: Option<String> },
21 KillPane { pane_id: PaneId },
22 ListPanes { session: SessionId },
23 SelectPane { pane_id: PaneId },
24 PaneInfo { pane_id: PaneId },
25
26 ResizePane { pane_id: PaneId, cols: u16, rows: u16 },
28
29 SendKeys { pane_id: PaneId, keys: String },
31 Capture { pane_id: PaneId, json: bool, scrollback: Option<ScrollbackRange>, #[serde(default)] filter: Option<CaptureFilter> },
32 Wait { pane_id: PaneId, pattern: String, timeout_secs: Option<u64>, #[serde(default)] return_match: bool, #[serde(default)] capture_on_match: bool, #[serde(default)] scrollback: bool },
33 Exec { pane_id: PaneId, command: String, wait: bool, capture: bool, scrollback: Option<ScrollbackRange>, #[serde(default)] filter: Option<CaptureFilter> },
34 Subscribe { pane_id: PaneId, events: Vec<String> },
35
36 SetOption { target: String, key: String, value: String },
38
39 MarkPane { pane_id: PaneId, mark: String },
41 UnmarkPane { mark: String },
42 ListMarks,
43
44 SwapPanes { pane_a: PaneId, pane_b: PaneId },
46
47 MovePane { pane_id: PaneId, target_session: SessionId, target_window: WindowId },
49
50 ApplyLayout { session: SessionId, window: WindowId, layout_name: String },
52 SelectLayout { session: SessionId, window: WindowId, layout: String },
53 SaveLayout { session: SessionId, window: WindowId, name: String },
54 LoadLayout { session: SessionId, window: WindowId, name: String },
55 ListLayouts,
56
57 Ping,
59 KillServer,
60 ServerStatus,
61
62 Attach { session: SessionId },
64
65 ReloadConfig,
67
68 Batch { requests: Vec<Request> },
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub enum Response {
74 SessionCreated { session_id: SessionId, pane_id: PaneId },
76 SessionKilled,
77 Sessions(Vec<SessionInfo>),
78 SessionExists(bool),
79
80 WindowCreated { window_id: WindowId, pane_id: PaneId },
82 WindowKilled,
83 Windows(Vec<WindowInfo>),
84 WindowSelected,
85
86 PaneSplit { pane_id: PaneId },
88 PaneKilled,
89 Panes(Vec<PaneInfo>),
90 PaneSelected,
91 PaneDetail(PaneInfo),
92
93 PaneResized,
95
96 KeysSent,
98 CaptureResult(CaptureResult),
99 WaitMatched {
100 #[serde(default, skip_serializing_if = "Option::is_none")]
101 matched: Option<String>,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
103 line: Option<String>,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 capture: Option<CaptureResult>,
106 },
107 ExecResult { output: Option<CaptureResult> },
108 Subscribed { stream_pipe: String },
109
110 OptionSet,
112
113 Pong,
115 ServerKilling,
116 ServerStatus(ServerStatusInfo),
117
118 MarkSet,
120 MarkRemoved,
121 Marks(Vec<MarkInfo>),
122
123 PanesSwapped,
125
126 PaneMoved,
128
129 LayoutApplied { pane_ids: Vec<PaneId> },
131 LayoutSelected,
132 LayoutSaved,
133 LayoutLoaded { pane_ids: Vec<PaneId> },
134 Layouts(Vec<LayoutListEntry>),
135
136 BatchResult { responses: Vec<Response> },
138
139 Ok,
141 Error { message: String },
142
143 AttachAccepted { layout: LayoutInfo },
145
146 ConfigReloaded,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152pub enum ServerPush {
153 ScreenSnapshot {
154 pane_id: PaneId,
155 cells: Vec<Vec<ScreenCell>>,
156 cursor: CursorPos,
157 size: (u16, u16),
158 cursor_visible: bool,
159 title: String,
160 },
161 LayoutChanged(LayoutInfo),
162 Error { message: String },
163 ServerShutdown,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
168pub enum ClientInput {
169 Keys { data: Vec<u8> },
170 Resize { cols: u16, rows: u16 },
171 Detach,
172 Command(Request),
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::types::{CaptureFilter, CaptureResult, CursorPos, PaneInfo, ProcessInfo, SessionInfo, WindowInfo};
179
180 fn roundtrip_request(req: &Request) -> Request {
182 let bytes = rmp_serde::to_vec(req).unwrap();
183 rmp_serde::from_slice(&bytes).unwrap()
184 }
185
186 fn roundtrip_response(resp: &Response) -> Response {
187 let bytes = rmp_serde::to_vec(resp).unwrap();
188 rmp_serde::from_slice(&bytes).unwrap()
189 }
190
191 #[test]
192 fn request_new_session_roundtrip() {
193 let req = Request::NewSession {
194 name: "test".into(),
195 shell: None,
196 layout: None,
197 pane_alias: None,
198 template: None,
199 };
200 let decoded = roundtrip_request(&req);
201 match decoded {
202 Request::NewSession { name, shell, layout, pane_alias, template } => {
203 assert_eq!(name, "test");
204 assert!(shell.is_none());
205 assert!(layout.is_none());
206 assert!(pane_alias.is_none());
207 assert!(template.is_none());
208 }
209 _ => panic!("wrong variant"),
210 }
211 }
212
213 #[test]
214 fn request_send_keys_roundtrip() {
215 let req = Request::SendKeys {
216 pane_id: "%0".into(),
217 keys: "ls\r".into(),
218 };
219 let decoded = roundtrip_request(&req);
220 match decoded {
221 Request::SendKeys { pane_id, keys } => {
222 assert_eq!(pane_id, "%0");
223 assert_eq!(keys, "ls\r");
224 }
225 _ => panic!("wrong variant"),
226 }
227 }
228
229 #[test]
230 fn request_split_pane_roundtrip() {
231 let req = Request::SplitPane {
232 target: "%0".into(),
233 horizontal: true,
234 shell: Some("cmd.exe".into()),
235 pane_alias: None,
236 };
237 let decoded = roundtrip_request(&req);
238 match decoded {
239 Request::SplitPane {
240 target,
241 horizontal,
242 shell,
243 pane_alias,
244 } => {
245 assert_eq!(target, "%0");
246 assert!(horizontal);
247 assert_eq!(shell.unwrap(), "cmd.exe");
248 assert!(pane_alias.is_none());
249 }
250 _ => panic!("wrong variant"),
251 }
252 }
253
254 #[test]
255 fn request_wait_roundtrip() {
256 let req = Request::Wait {
257 pane_id: "%1".into(),
258 pattern: ">".into(),
259 timeout_secs: Some(30),
260 return_match: false,
261 capture_on_match: false,
262 scrollback: false,
263 };
264 let decoded = roundtrip_request(&req);
265 match decoded {
266 Request::Wait {
267 pane_id,
268 pattern,
269 timeout_secs,
270 return_match,
271 capture_on_match,
272 scrollback,
273 } => {
274 assert_eq!(pane_id, "%1");
275 assert_eq!(pattern, ">");
276 assert_eq!(timeout_secs, Some(30));
277 assert!(!return_match);
278 assert!(!capture_on_match);
279 assert!(!scrollback);
280 }
281 _ => panic!("wrong variant"),
282 }
283 }
284
285 #[test]
286 fn request_exec_roundtrip() {
287 let req = Request::Exec {
288 pane_id: "%0".into(),
289 command: "dir".into(),
290 wait: true,
291 capture: true,
292 scrollback: None,
293 filter: None,
294 };
295 let decoded = roundtrip_request(&req);
296 match decoded {
297 Request::Exec {
298 pane_id,
299 command,
300 wait,
301 capture,
302 scrollback,
303 filter,
304 } => {
305 assert_eq!(pane_id, "%0");
306 assert_eq!(command, "dir");
307 assert!(wait);
308 assert!(capture);
309 assert!(scrollback.is_none());
310 assert!(filter.is_none());
311 }
312 _ => panic!("wrong variant"),
313 }
314 }
315
316 #[test]
317 fn request_new_window_roundtrip() {
318 let req = Request::NewWindow {
319 session: "work".into(),
320 shell: Some("cmd.exe".into()),
321 layout: None,
322 pane_alias: None,
323 };
324 let decoded = roundtrip_request(&req);
325 match decoded {
326 Request::NewWindow { session, shell, pane_alias, .. } => {
327 assert_eq!(session, "work");
328 assert_eq!(shell.unwrap(), "cmd.exe");
329 assert!(pane_alias.is_none());
330 }
331 _ => panic!("wrong variant"),
332 }
333
334 let req2 = Request::NewWindow {
336 session: "test".into(),
337 shell: None,
338 layout: None,
339 pane_alias: None,
340 };
341 let decoded2 = roundtrip_request(&req2);
342 match decoded2 {
343 Request::NewWindow { session, shell, .. } => {
344 assert_eq!(session, "test");
345 assert!(shell.is_none());
346 }
347 _ => panic!("wrong variant"),
348 }
349 }
350
351 #[test]
352 fn request_parameterless_variants_roundtrip() {
353 for req in [Request::ListSessions, Request::Ping, Request::KillServer, Request::ReloadConfig, Request::ListMarks, Request::ServerStatus] {
354 let decoded = roundtrip_request(&req);
355 let _ = format!("{:?}", decoded);
357 }
358 }
359
360 #[test]
361 fn response_session_created_roundtrip() {
362 let resp = Response::SessionCreated {
363 session_id: "work".into(),
364 pane_id: "%0".into(),
365 };
366 let decoded = roundtrip_response(&resp);
367 match decoded {
368 Response::SessionCreated {
369 session_id,
370 pane_id,
371 } => {
372 assert_eq!(session_id, "work");
373 assert_eq!(pane_id, "%0");
374 }
375 _ => panic!("wrong variant"),
376 }
377 }
378
379 #[test]
380 fn response_sessions_roundtrip() {
381 let resp = Response::Sessions(vec![SessionInfo {
382 name: "s1".into(),
383 windows: vec![WindowInfo {
384 id: 0,
385 panes: vec![PaneInfo {
386 id: "%0".into(),
387 pid: 1234,
388 running: true,
389 exit_code: None,
390 size: (80, 24),
391 title: "powershell.exe".into(),
392 marks: vec![],
393 }],
394 active_pane: "%0".into(),
395 }],
396 created_at: 1700000000,
397 }]);
398 let decoded = roundtrip_response(&resp);
399 match decoded {
400 Response::Sessions(sessions) => {
401 assert_eq!(sessions.len(), 1);
402 assert_eq!(sessions[0].name, "s1");
403 assert_eq!(sessions[0].windows[0].panes[0].pid, 1234);
404 }
405 _ => panic!("wrong variant"),
406 }
407 }
408
409 #[test]
410 fn response_capture_result_roundtrip() {
411 let resp = Response::CaptureResult(CaptureResult {
412 pane_id: "%0".into(),
413 lines: vec!["PS C:\\>".into(), "hello".into()],
414 scrollback_lines: vec![],
415 cursor: CursorPos { row: 2, col: 0 },
416 size: (80, 24),
417 process: ProcessInfo {
418 pid: 5678,
419 running: true,
420 exit_code: None,
421 },
422 title: "powershell.exe".into(),
423 });
424 let decoded = roundtrip_response(&resp);
425 match decoded {
426 Response::CaptureResult(cap) => {
427 assert_eq!(cap.pane_id, "%0");
428 assert_eq!(cap.lines.len(), 2);
429 assert_eq!(cap.cursor.row, 2);
430 assert_eq!(cap.process.pid, 5678);
431 }
432 _ => panic!("wrong variant"),
433 }
434 }
435
436 #[test]
437 fn response_error_roundtrip() {
438 let resp = Response::Error {
439 message: "something broke".into(),
440 };
441 let decoded = roundtrip_response(&resp);
442 match decoded {
443 Response::Error { message } => assert_eq!(message, "something broke"),
444 _ => panic!("wrong variant"),
445 }
446 }
447
448 #[test]
449 fn response_simple_variants_roundtrip() {
450 for resp in [
451 Response::SessionKilled,
452 Response::WindowKilled,
453 Response::WindowSelected,
454 Response::PaneKilled,
455 Response::PaneSelected,
456 Response::PaneResized,
457 Response::KeysSent,
458 Response::WaitMatched { matched: None, line: None, capture: None },
459 Response::OptionSet,
460 Response::Pong,
461 Response::ServerKilling,
462 Response::Ok,
463 Response::ConfigReloaded,
464 Response::MarkSet,
465 Response::MarkRemoved,
466 Response::PanesSwapped,
467 Response::PaneMoved,
468 Response::LayoutSelected,
469 ] {
470 let decoded = roundtrip_response(&resp);
471 let _ = format!("{:?}", decoded);
472 }
473 }
474
475 #[test]
476 fn request_capture_with_scrollback_roundtrip() {
477 let req = Request::Capture {
478 pane_id: "%0".into(),
479 json: false,
480 scrollback: Some(ScrollbackRange::Last(100)),
481 filter: None,
482 };
483 let decoded = roundtrip_request(&req);
484 match decoded {
485 Request::Capture {
486 pane_id,
487 json,
488 scrollback,
489 filter,
490 } => {
491 assert_eq!(pane_id, "%0");
492 assert!(!json);
493 match scrollback {
494 Some(ScrollbackRange::Last(n)) => assert_eq!(n, 100),
495 _ => panic!("expected ScrollbackRange::Last(100)"),
496 }
497 assert!(filter.is_none());
498 }
499 _ => panic!("wrong variant"),
500 }
501 }
502
503 #[test]
504 fn capture_result_with_scrollback_lines_roundtrip() {
505 let resp = Response::CaptureResult(CaptureResult {
506 pane_id: "%0".into(),
507 lines: vec!["visible line".into()],
508 scrollback_lines: vec!["old line 1".into(), "old line 2".into()],
509 cursor: CursorPos { row: 1, col: 0 },
510 size: (80, 24),
511 process: ProcessInfo {
512 pid: 1234,
513 running: true,
514 exit_code: None,
515 },
516 title: "cmd.exe".into(),
517 });
518 let decoded = roundtrip_response(&resp);
519 match decoded {
520 Response::CaptureResult(cap) => {
521 assert_eq!(cap.scrollback_lines.len(), 2);
522 assert_eq!(cap.scrollback_lines[0], "old line 1");
523 assert_eq!(cap.scrollback_lines[1], "old line 2");
524 assert_eq!(cap.lines.len(), 1);
525 }
526 _ => panic!("wrong variant"),
527 }
528 }
529
530 #[test]
531 fn request_resize_pane_roundtrip() {
532 let req = Request::ResizePane {
533 pane_id: "%0".into(),
534 cols: 120,
535 rows: 40,
536 };
537 let decoded = roundtrip_request(&req);
538 match decoded {
539 Request::ResizePane { pane_id, cols, rows } => {
540 assert_eq!(pane_id, "%0");
541 assert_eq!(cols, 120);
542 assert_eq!(rows, 40);
543 }
544 _ => panic!("wrong variant"),
545 }
546 }
547
548 #[test]
549 fn response_pane_resized_roundtrip() {
550 let resp = Response::PaneResized;
551 let decoded = roundtrip_response(&resp);
552 assert!(matches!(decoded, Response::PaneResized));
553 }
554
555 #[test]
556 fn request_exec_with_scrollback_roundtrip() {
557 let req = Request::Exec {
558 pane_id: "%0".into(),
559 command: "dir".into(),
560 wait: true,
561 capture: true,
562 scrollback: Some(ScrollbackRange::All),
563 filter: None,
564 };
565 let decoded = roundtrip_request(&req);
566 match decoded {
567 Request::Exec {
568 scrollback, ..
569 } => {
570 assert!(matches!(scrollback, Some(ScrollbackRange::All)));
571 }
572 _ => panic!("wrong variant"),
573 }
574 }
575
576 #[test]
577 fn request_attach_roundtrip() {
578 let req = Request::Attach {
579 session: "work".into(),
580 };
581 let decoded = roundtrip_request(&req);
582 match decoded {
583 Request::Attach { session } => assert_eq!(session, "work"),
584 _ => panic!("wrong variant"),
585 }
586 }
587
588 #[test]
589 fn response_attach_accepted_roundtrip() {
590 let layout = LayoutInfo {
591 session: "work".into(),
592 window_id: 0,
593 panes: vec![PaneLayout {
594 pane_id: "%0".into(),
595 x: 0,
596 y: 0,
597 width: 80,
598 height: 24,
599 is_active: true,
600 is_zoomed: false,
601 }],
602 terminal_cols: 80,
603 terminal_rows: 24,
604 windows: vec![WindowBarInfo {
605 id: 0,
606 title: "bash".into(),
607 is_active: true,
608 }],
609 };
610 let resp = Response::AttachAccepted {
611 layout: layout.clone(),
612 };
613 let decoded = roundtrip_response(&resp);
614 match decoded {
615 Response::AttachAccepted { layout: l } => assert_eq!(l, layout),
616 _ => panic!("wrong variant"),
617 }
618 }
619
620 fn roundtrip_server_push(push: &ServerPush) -> ServerPush {
621 let bytes = rmp_serde::to_vec(push).unwrap();
622 rmp_serde::from_slice(&bytes).unwrap()
623 }
624
625 fn roundtrip_client_input(input: &ClientInput) -> ClientInput {
626 let bytes = rmp_serde::to_vec(input).unwrap();
627 rmp_serde::from_slice(&bytes).unwrap()
628 }
629
630 #[test]
631 fn server_push_screen_snapshot_roundtrip() {
632 let push = ServerPush::ScreenSnapshot {
633 pane_id: "%0".into(),
634 cells: vec![vec![ScreenCell {
635 ch: 'A',
636 attrs: ScreenCellAttrs {
637 bold: true,
638 fg: ScreenColor::Rgb(255, 0, 0),
639 ..Default::default()
640 },
641 }]],
642 cursor: CursorPos { row: 0, col: 1 },
643 size: (80, 24),
644 cursor_visible: true,
645 title: "cmd.exe".into(),
646 };
647 let decoded = roundtrip_server_push(&push);
648 match decoded {
649 ServerPush::ScreenSnapshot {
650 pane_id,
651 cells,
652 cursor,
653 size,
654 cursor_visible,
655 title,
656 } => {
657 assert_eq!(pane_id, "%0");
658 assert_eq!(cells.len(), 1);
659 assert_eq!(cells[0][0].ch, 'A');
660 assert!(cells[0][0].attrs.bold);
661 assert_eq!(cells[0][0].attrs.fg, ScreenColor::Rgb(255, 0, 0));
662 assert_eq!(cursor.row, 0);
663 assert_eq!(cursor.col, 1);
664 assert_eq!(size, (80, 24));
665 assert!(cursor_visible);
666 assert_eq!(title, "cmd.exe");
667 }
668 _ => panic!("wrong variant"),
669 }
670 }
671
672 #[test]
673 fn server_push_layout_changed_roundtrip() {
674 let layout = LayoutInfo {
675 session: "s".into(),
676 window_id: 1,
677 panes: vec![],
678 terminal_cols: 120,
679 terminal_rows: 40,
680 windows: vec![
681 WindowBarInfo {
682 id: 0,
683 title: "win0".into(),
684 is_active: false,
685 },
686 WindowBarInfo {
687 id: 1,
688 title: "win1".into(),
689 is_active: true,
690 },
691 ],
692 };
693 let push = ServerPush::LayoutChanged(layout.clone());
694 let decoded = roundtrip_server_push(&push);
695 match decoded {
696 ServerPush::LayoutChanged(l) => assert_eq!(l, layout),
697 _ => panic!("wrong variant"),
698 }
699 }
700
701 #[test]
702 fn server_push_error_and_shutdown_roundtrip() {
703 let push = ServerPush::Error {
704 message: "oops".into(),
705 };
706 let decoded = roundtrip_server_push(&push);
707 match decoded {
708 ServerPush::Error { message } => assert_eq!(message, "oops"),
709 _ => panic!("wrong variant"),
710 }
711
712 let push2 = ServerPush::ServerShutdown;
713 let decoded2 = roundtrip_server_push(&push2);
714 assert!(matches!(decoded2, ServerPush::ServerShutdown));
715 }
716
717 #[test]
718 fn client_input_keys_roundtrip() {
719 let input = ClientInput::Keys {
720 data: vec![27, 91, 65], };
722 let decoded = roundtrip_client_input(&input);
723 match decoded {
724 ClientInput::Keys { data } => assert_eq!(data, vec![27, 91, 65]),
725 _ => panic!("wrong variant"),
726 }
727 }
728
729 #[test]
730 fn client_input_resize_roundtrip() {
731 let input = ClientInput::Resize {
732 cols: 120,
733 rows: 40,
734 };
735 let decoded = roundtrip_client_input(&input);
736 match decoded {
737 ClientInput::Resize { cols, rows } => {
738 assert_eq!(cols, 120);
739 assert_eq!(rows, 40);
740 }
741 _ => panic!("wrong variant"),
742 }
743 }
744
745 #[test]
746 fn client_input_detach_roundtrip() {
747 let input = ClientInput::Detach;
748 let decoded = roundtrip_client_input(&input);
749 assert!(matches!(decoded, ClientInput::Detach));
750 }
751
752 #[test]
753 fn client_input_command_roundtrip() {
754 let input = ClientInput::Command(Request::Ping);
755 let decoded = roundtrip_client_input(&input);
756 match decoded {
757 ClientInput::Command(req) => assert!(matches!(req, Request::Ping)),
758 _ => panic!("wrong variant"),
759 }
760 }
761
762 #[test]
763 fn screen_color_variants_roundtrip() {
764 let colors = vec![
765 ScreenColor::Default,
766 ScreenColor::Indexed(196),
767 ScreenColor::Rgb(0, 128, 255),
768 ];
769 for color in &colors {
770 let bytes = rmp_serde::to_vec(color).unwrap();
771 let decoded: ScreenColor = rmp_serde::from_slice(&bytes).unwrap();
772 assert_eq!(&decoded, color);
773 }
774 }
775
776 #[test]
777 fn window_bar_info_roundtrip() {
778 let info = WindowBarInfo {
779 id: 3,
780 title: "my window".into(),
781 is_active: false,
782 };
783 let bytes = rmp_serde::to_vec(&info).unwrap();
784 let decoded: WindowBarInfo = rmp_serde::from_slice(&bytes).unwrap();
785 assert_eq!(decoded, info);
786 }
787
788 #[test]
789 fn request_mark_pane_roundtrip() {
790 let req = Request::MarkPane {
791 pane_id: "%0".into(),
792 mark: "build".into(),
793 };
794 let decoded = roundtrip_request(&req);
795 match decoded {
796 Request::MarkPane { pane_id, mark } => {
797 assert_eq!(pane_id, "%0");
798 assert_eq!(mark, "build");
799 }
800 _ => panic!("wrong variant"),
801 }
802 }
803
804 #[test]
805 fn request_unmark_pane_roundtrip() {
806 let req = Request::UnmarkPane {
807 mark: "build".into(),
808 };
809 let decoded = roundtrip_request(&req);
810 match decoded {
811 Request::UnmarkPane { mark } => assert_eq!(mark, "build"),
812 _ => panic!("wrong variant"),
813 }
814 }
815
816 #[test]
817 fn response_marks_roundtrip() {
818 let resp = Response::Marks(vec![
819 MarkInfo {
820 mark: "build".into(),
821 pane_id: "%0".into(),
822 },
823 MarkInfo {
824 mark: "test".into(),
825 pane_id: "%1".into(),
826 },
827 ]);
828 let decoded = roundtrip_response(&resp);
829 match decoded {
830 Response::Marks(marks) => {
831 assert_eq!(marks.len(), 2);
832 assert_eq!(marks[0].mark, "build");
833 assert_eq!(marks[0].pane_id, "%0");
834 assert_eq!(marks[1].mark, "test");
835 assert_eq!(marks[1].pane_id, "%1");
836 }
837 _ => panic!("wrong variant"),
838 }
839 }
840
841 #[test]
842 fn request_swap_panes_roundtrip() {
843 let req = Request::SwapPanes {
844 pane_a: "%0".into(),
845 pane_b: "%1".into(),
846 };
847 let decoded = roundtrip_request(&req);
848 match decoded {
849 Request::SwapPanes { pane_a, pane_b } => {
850 assert_eq!(pane_a, "%0");
851 assert_eq!(pane_b, "%1");
852 }
853 _ => panic!("wrong variant"),
854 }
855 }
856
857 #[test]
858 fn response_panes_swapped_roundtrip() {
859 let resp = Response::PanesSwapped;
860 let decoded = roundtrip_response(&resp);
861 assert!(matches!(decoded, Response::PanesSwapped));
862 }
863
864 #[test]
865 fn request_move_pane_roundtrip() {
866 let req = Request::MovePane {
867 pane_id: "%0".into(),
868 target_session: "work".into(),
869 target_window: 1,
870 };
871 let decoded = roundtrip_request(&req);
872 match decoded {
873 Request::MovePane { pane_id, target_session, target_window } => {
874 assert_eq!(pane_id, "%0");
875 assert_eq!(target_session, "work");
876 assert_eq!(target_window, 1);
877 }
878 _ => panic!("wrong variant"),
879 }
880 }
881
882 #[test]
883 fn response_pane_moved_roundtrip() {
884 let resp = Response::PaneMoved;
885 let decoded = roundtrip_response(&resp);
886 assert!(matches!(decoded, Response::PaneMoved));
887 }
888
889 #[test]
890 fn pane_info_with_marks_roundtrip() {
891 let info = PaneInfo {
892 id: "%0".into(),
893 pid: 1234,
894 running: true,
895 exit_code: None,
896 size: (80, 24),
897 title: "powershell.exe".into(),
898 marks: vec!["build".into(), "main".into()],
899 };
900 let bytes = rmp_serde::to_vec(&info).unwrap();
901 let decoded: PaneInfo = rmp_serde::from_slice(&bytes).unwrap();
902 assert_eq!(decoded.id, "%0");
903 assert_eq!(decoded.marks, vec!["build", "main"]);
904 }
905
906 #[test]
907 fn request_apply_layout_roundtrip() {
908 let req = Request::ApplyLayout {
909 session: "work".into(),
910 window: 0,
911 layout_name: "dev".into(),
912 };
913 let decoded = roundtrip_request(&req);
914 match decoded {
915 Request::ApplyLayout { session, window, layout_name } => {
916 assert_eq!(session, "work");
917 assert_eq!(window, 0);
918 assert_eq!(layout_name, "dev");
919 }
920 _ => panic!("wrong variant"),
921 }
922 }
923
924 #[test]
925 fn request_select_layout_roundtrip() {
926 let req = Request::SelectLayout {
927 session: "work".into(),
928 window: 0,
929 layout: "tiled".into(),
930 };
931 let decoded = roundtrip_request(&req);
932 match decoded {
933 Request::SelectLayout { session, window, layout } => {
934 assert_eq!(session, "work");
935 assert_eq!(window, 0);
936 assert_eq!(layout, "tiled");
937 }
938 _ => panic!("wrong variant"),
939 }
940 }
941
942 #[test]
943 fn response_layout_applied_roundtrip() {
944 let resp = Response::LayoutApplied {
945 pane_ids: vec!["%0".into(), "%1".into(), "%2".into()],
946 };
947 let decoded = roundtrip_response(&resp);
948 match decoded {
949 Response::LayoutApplied { pane_ids } => {
950 assert_eq!(pane_ids, vec!["%0", "%1", "%2"]);
951 }
952 _ => panic!("wrong variant"),
953 }
954 }
955
956 #[test]
957 fn request_new_session_with_layout_roundtrip() {
958 let req = Request::NewSession {
959 name: "work".into(),
960 shell: None,
961 layout: Some("dev".into()),
962 pane_alias: None,
963 template: None,
964 };
965 let decoded = roundtrip_request(&req);
966 match decoded {
967 Request::NewSession { name, shell, layout, pane_alias, template } => {
968 assert_eq!(name, "work");
969 assert!(shell.is_none());
970 assert_eq!(layout, Some("dev".into()));
971 assert!(pane_alias.is_none());
972 assert!(template.is_none());
973 }
974 _ => panic!("wrong variant"),
975 }
976 }
977
978 #[test]
979 fn request_new_window_with_layout_roundtrip() {
980 let req = Request::NewWindow {
981 session: "work".into(),
982 shell: None,
983 layout: Some("dev".into()),
984 pane_alias: None,
985 };
986 let decoded = roundtrip_request(&req);
987 match decoded {
988 Request::NewWindow { session, shell, layout, pane_alias } => {
989 assert_eq!(session, "work");
990 assert!(shell.is_none());
991 assert_eq!(layout, Some("dev".into()));
992 assert!(pane_alias.is_none());
993 }
994 _ => panic!("wrong variant"),
995 }
996 }
997
998 #[test]
999 fn request_save_layout_roundtrip() {
1000 let req = Request::SaveLayout {
1001 session: "work".into(),
1002 window: 0,
1003 name: "dev-layout".into(),
1004 };
1005 let decoded = roundtrip_request(&req);
1006 match decoded {
1007 Request::SaveLayout { session, window, name } => {
1008 assert_eq!(session, "work");
1009 assert_eq!(window, 0);
1010 assert_eq!(name, "dev-layout");
1011 }
1012 _ => panic!("wrong variant"),
1013 }
1014 }
1015
1016 #[test]
1017 fn request_load_layout_roundtrip() {
1018 let req = Request::LoadLayout {
1019 session: "work".into(),
1020 window: 1,
1021 name: "dev-layout".into(),
1022 };
1023 let decoded = roundtrip_request(&req);
1024 match decoded {
1025 Request::LoadLayout { session, window, name } => {
1026 assert_eq!(session, "work");
1027 assert_eq!(window, 1);
1028 assert_eq!(name, "dev-layout");
1029 }
1030 _ => panic!("wrong variant"),
1031 }
1032 }
1033
1034 #[test]
1035 fn request_list_layouts_roundtrip() {
1036 let req = Request::ListLayouts;
1037 let decoded = roundtrip_request(&req);
1038 assert!(matches!(decoded, Request::ListLayouts));
1039 }
1040
1041 #[test]
1042 fn response_layout_saved_roundtrip() {
1043 let resp = Response::LayoutSaved;
1044 let decoded = roundtrip_response(&resp);
1045 assert!(matches!(decoded, Response::LayoutSaved));
1046 }
1047
1048 #[test]
1049 fn response_layout_loaded_roundtrip() {
1050 let resp = Response::LayoutLoaded {
1051 pane_ids: vec!["%0".into(), "%1".into()],
1052 };
1053 let decoded = roundtrip_response(&resp);
1054 match decoded {
1055 Response::LayoutLoaded { pane_ids } => {
1056 assert_eq!(pane_ids, vec!["%0", "%1"]);
1057 }
1058 _ => panic!("wrong variant"),
1059 }
1060 }
1061
1062 #[test]
1063 fn response_layouts_roundtrip() {
1064 let resp = Response::Layouts(vec![
1065 LayoutListEntry { name: "dev".into(), source: LayoutSource::Config },
1066 LayoutListEntry { name: "my-save".into(), source: LayoutSource::Saved },
1067 LayoutListEntry { name: "tiled".into(), source: LayoutSource::Preset },
1068 ]);
1069 let decoded = roundtrip_response(&resp);
1070 match decoded {
1071 Response::Layouts(entries) => {
1072 assert_eq!(entries.len(), 3);
1073 assert_eq!(entries[0].name, "dev");
1074 assert_eq!(entries[0].source, LayoutSource::Config);
1075 assert_eq!(entries[1].name, "my-save");
1076 assert_eq!(entries[1].source, LayoutSource::Saved);
1077 assert_eq!(entries[2].name, "tiled");
1078 assert_eq!(entries[2].source, LayoutSource::Preset);
1079 }
1080 _ => panic!("wrong variant"),
1081 }
1082 }
1083
1084 #[test]
1085 fn request_server_status_roundtrip() {
1086 let req = Request::ServerStatus;
1087 let decoded = roundtrip_request(&req);
1088 assert!(matches!(decoded, Request::ServerStatus));
1089 }
1090
1091 #[test]
1092 fn response_server_status_roundtrip() {
1093 let resp = Response::ServerStatus(ServerStatusInfo {
1094 version: "0.1.0".into(),
1095 uptime_secs: 3600,
1096 sessions: 2,
1097 windows: 3,
1098 panes: 5,
1099 pid: 12345,
1100 });
1101 let decoded = roundtrip_response(&resp);
1102 match decoded {
1103 Response::ServerStatus(info) => {
1104 assert_eq!(info.version, "0.1.0");
1105 assert_eq!(info.uptime_secs, 3600);
1106 assert_eq!(info.sessions, 2);
1107 assert_eq!(info.windows, 3);
1108 assert_eq!(info.panes, 5);
1109 assert_eq!(info.pid, 12345);
1110 }
1111 _ => panic!("wrong variant"),
1112 }
1113 }
1114
1115 #[test]
1116 fn request_new_session_with_pane_alias_roundtrip() {
1117 let req = Request::NewSession {
1118 name: "work".into(),
1119 shell: None,
1120 layout: None,
1121 pane_alias: Some("builder".into()),
1122 template: None,
1123 };
1124 let decoded = roundtrip_request(&req);
1125 match decoded {
1126 Request::NewSession { pane_alias, .. } => {
1127 assert_eq!(pane_alias, Some("builder".into()));
1128 }
1129 _ => panic!("wrong variant"),
1130 }
1131 }
1132
1133 #[test]
1134 fn request_new_session_with_template_roundtrip() {
1135 let req = Request::NewSession {
1136 name: "work".into(),
1137 shell: None,
1138 layout: None,
1139 pane_alias: None,
1140 template: Some("dev".into()),
1141 };
1142 let decoded = roundtrip_request(&req);
1143 match decoded {
1144 Request::NewSession { name, template, layout, .. } => {
1145 assert_eq!(name, "work");
1146 assert_eq!(template, Some("dev".into()));
1147 assert!(layout.is_none());
1148 }
1149 _ => panic!("wrong variant"),
1150 }
1151 }
1152
1153 #[test]
1154 fn request_batch_roundtrip() {
1155 let req = Request::Batch {
1156 requests: vec![
1157 Request::Ping,
1158 Request::ListSessions,
1159 Request::NewSession {
1160 name: "test".into(),
1161 shell: None,
1162 layout: None,
1163 pane_alias: None,
1164 template: None,
1165 },
1166 ],
1167 };
1168 let decoded = roundtrip_request(&req);
1169 match decoded {
1170 Request::Batch { requests } => {
1171 assert_eq!(requests.len(), 3);
1172 assert!(matches!(requests[0], Request::Ping));
1173 assert!(matches!(requests[1], Request::ListSessions));
1174 assert!(matches!(requests[2], Request::NewSession { .. }));
1175 }
1176 _ => panic!("wrong variant"),
1177 }
1178 }
1179
1180 #[test]
1181 fn response_batch_result_roundtrip() {
1182 let resp = Response::BatchResult {
1183 responses: vec![
1184 Response::Pong,
1185 Response::Sessions(vec![]),
1186 Response::Error { message: "test error".into() },
1187 ],
1188 };
1189 let decoded = roundtrip_response(&resp);
1190 match decoded {
1191 Response::BatchResult { responses } => {
1192 assert_eq!(responses.len(), 3);
1193 assert!(matches!(responses[0], Response::Pong));
1194 assert!(matches!(responses[1], Response::Sessions(_)));
1195 match &responses[2] {
1196 Response::Error { message } => assert_eq!(message, "test error"),
1197 _ => panic!("expected Error"),
1198 }
1199 }
1200 _ => panic!("wrong variant"),
1201 }
1202 }
1203
1204 #[test]
1205 fn request_batch_json_roundtrip() {
1206 let requests = vec![Request::Ping, Request::ListSessions];
1208 let json = serde_json::to_string(&requests).unwrap();
1209 let decoded: Vec<Request> = serde_json::from_str(&json).unwrap();
1210 assert_eq!(decoded.len(), 2);
1211 assert!(matches!(decoded[0], Request::Ping));
1212 assert!(matches!(decoded[1], Request::ListSessions));
1213 }
1214
1215 #[test]
1216 fn response_batch_result_json_roundtrip() {
1217 let responses = vec![Response::Pong, Response::Sessions(vec![])];
1218 let json = serde_json::to_string_pretty(&responses).unwrap();
1219 let decoded: Vec<Response> = serde_json::from_str(&json).unwrap();
1220 assert_eq!(decoded.len(), 2);
1221 assert!(matches!(decoded[0], Response::Pong));
1222 assert!(matches!(decoded[1], Response::Sessions(_)));
1223 }
1224
1225 #[test]
1226 fn request_split_pane_with_alias_roundtrip() {
1227 let req = Request::SplitPane {
1228 target: "%0".into(),
1229 horizontal: false,
1230 shell: None,
1231 pane_alias: Some("test-pane".into()),
1232 };
1233 let decoded = roundtrip_request(&req);
1234 match decoded {
1235 Request::SplitPane { pane_alias, .. } => {
1236 assert_eq!(pane_alias, Some("test-pane".into()));
1237 }
1238 _ => panic!("wrong variant"),
1239 }
1240 }
1241
1242 #[test]
1243 fn request_new_window_with_pane_alias_roundtrip() {
1244 let req = Request::NewWindow {
1245 session: "work".into(),
1246 shell: None,
1247 layout: None,
1248 pane_alias: Some("runner".into()),
1249 };
1250 let decoded = roundtrip_request(&req);
1251 match decoded {
1252 Request::NewWindow { pane_alias, .. } => {
1253 assert_eq!(pane_alias, Some("runner".into()));
1254 }
1255 _ => panic!("wrong variant"),
1256 }
1257 }
1258
1259 #[test]
1260 fn request_wait_with_new_flags_roundtrip() {
1261 let req = Request::Wait {
1262 pane_id: "%0".into(),
1263 pattern: "ready|done".into(),
1264 timeout_secs: Some(10),
1265 return_match: true,
1266 capture_on_match: true,
1267 scrollback: true,
1268 };
1269 let decoded = roundtrip_request(&req);
1270 match decoded {
1271 Request::Wait {
1272 pane_id,
1273 pattern,
1274 timeout_secs,
1275 return_match,
1276 capture_on_match,
1277 scrollback,
1278 } => {
1279 assert_eq!(pane_id, "%0");
1280 assert_eq!(pattern, "ready|done");
1281 assert_eq!(timeout_secs, Some(10));
1282 assert!(return_match);
1283 assert!(capture_on_match);
1284 assert!(scrollback);
1285 }
1286 _ => panic!("wrong variant"),
1287 }
1288 }
1289
1290 #[test]
1291 fn response_wait_matched_with_details_roundtrip() {
1292 let resp = Response::WaitMatched {
1293 matched: Some("done".into()),
1294 line: Some("build done successfully".into()),
1295 capture: Some(CaptureResult {
1296 pane_id: "%0".into(),
1297 lines: vec!["build done successfully".into()],
1298 scrollback_lines: vec![],
1299 cursor: CursorPos { row: 1, col: 0 },
1300 size: (80, 24),
1301 process: ProcessInfo { pid: 1234, running: true, exit_code: None },
1302 title: "cmd.exe".into(),
1303 }),
1304 };
1305 let decoded = roundtrip_response(&resp);
1306 match decoded {
1307 Response::WaitMatched { matched, line, capture } => {
1308 assert_eq!(matched, Some("done".into()));
1309 assert_eq!(line, Some("build done successfully".into()));
1310 assert!(capture.is_some());
1311 assert_eq!(capture.unwrap().pane_id, "%0");
1312 }
1313 _ => panic!("wrong variant"),
1314 }
1315 }
1316
1317 #[test]
1318 fn request_capture_with_filter_roundtrip() {
1319 let req = Request::Capture {
1320 pane_id: "%0".into(),
1321 json: true,
1322 scrollback: Some(ScrollbackRange::All),
1323 filter: Some(CaptureFilter::LastCommand { strip_prompt: true }),
1324 };
1325 let decoded = roundtrip_request(&req);
1326 match decoded {
1327 Request::Capture { filter, .. } => {
1328 match filter {
1329 Some(CaptureFilter::LastCommand { strip_prompt }) => assert!(strip_prompt),
1330 _ => panic!("expected LastCommand filter"),
1331 }
1332 }
1333 _ => panic!("wrong variant"),
1334 }
1335 }
1336
1337 #[test]
1338 fn request_exec_with_filter_roundtrip() {
1339 let req = Request::Exec {
1340 pane_id: "%0".into(),
1341 command: "dir".into(),
1342 wait: true,
1343 capture: true,
1344 scrollback: Some(ScrollbackRange::All),
1345 filter: Some(CaptureFilter::LastCommand { strip_prompt: false }),
1346 };
1347 let decoded = roundtrip_request(&req);
1348 match decoded {
1349 Request::Exec { filter, .. } => {
1350 match filter {
1351 Some(CaptureFilter::LastCommand { strip_prompt }) => assert!(!strip_prompt),
1352 _ => panic!("expected LastCommand filter"),
1353 }
1354 }
1355 _ => panic!("wrong variant"),
1356 }
1357 }
1358}