1use std::path::{Path, PathBuf};
7
8use serde::{Deserialize, Serialize};
9
10pub const PANEL_PROTO_VERSION: u32 = 1;
11
12pub fn murmur_run_dir(mur_home: &Path) -> PathBuf {
15 mur_home.join("runtime").join("murmur")
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct PanelSession {
21 pub pid: u32,
22 pub agent: String,
23 pub cwd: String,
24 pub sock: String,
25 #[serde(default)]
26 pub terminal_program: Option<String>,
27 pub proto_version: u32,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum PanelTab {
33 Information,
34 Activities,
35 Preview,
36 Notifications,
37 Schedule,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub enum PreviewKind {
43 File,
44 Url,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49#[serde(tag = "type", rename_all = "snake_case")]
50pub enum PanelFrame {
51 Hello {
53 session: PanelSession,
54 },
55 State {
57 cwd: String,
58 agent: String,
59 },
60 Panel {
62 focus: PanelTab,
63 },
64 Preview {
66 kind: PreviewKind,
67 target: String,
68 },
69 Stream {
71 delta: String,
72 },
73 Bye,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80#[serde(tag = "type", rename_all = "snake_case")]
81pub enum HubFrame {
82 Insert { text: String },
83}
84
85pub fn decode_line<T: serde::de::DeserializeOwned>(line: &str) -> Option<T> {
88 serde_json::from_str(line).ok()
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use std::path::Path;
95
96 #[test]
97 fn run_dir_under_runtime() {
98 assert_eq!(
99 murmur_run_dir(Path::new("/h")),
100 Path::new("/h/runtime/murmur")
101 );
102 }
103
104 #[test]
105 fn frames_round_trip() {
106 let s = PanelSession {
107 pid: 42,
108 agent: "mur".into(),
109 cwd: "/tmp".into(),
110 sock: "/h/runtime/murmur/42.sock".into(),
111 terminal_program: Some("iTerm.app".into()),
112 proto_version: PANEL_PROTO_VERSION,
113 };
114 let line = serde_json::to_string(&PanelFrame::Hello { session: s }).unwrap();
115 assert!(line.contains("\"type\":\"hello\""));
116 assert!(matches!(
117 decode_line::<PanelFrame>(&line),
118 Some(PanelFrame::Hello { .. })
119 ));
120
121 let line = serde_json::to_string(&PanelFrame::Panel {
122 focus: PanelTab::Preview,
123 })
124 .unwrap();
125 assert!(line.contains("\"focus\":\"preview\""));
126
127 let line = serde_json::to_string(&PanelFrame::Panel {
128 focus: PanelTab::Schedule,
129 })
130 .unwrap();
131 assert!(line.contains("\"focus\":\"schedule\""));
132
133 let line = serde_json::to_string(&PanelFrame::Stream {
134 delta: "tok".into(),
135 })
136 .unwrap();
137 assert!(line.contains("\"type\":\"stream\""));
138 assert!(matches!(
139 decode_line::<PanelFrame>(&line),
140 Some(PanelFrame::Stream { delta }) if delta == "tok"
141 ));
142
143 let line = serde_json::to_string(&HubFrame::Insert {
144 text: "/help".into(),
145 })
146 .unwrap();
147 assert!(matches!(
148 decode_line::<HubFrame>(&line),
149 Some(HubFrame::Insert { text }) if text == "/help"
150 ));
151 }
152
153 #[test]
154 fn unknown_frames_are_none() {
155 assert!(decode_line::<PanelFrame>(r#"{"type":"from_the_future"}"#).is_none());
156 assert!(decode_line::<HubFrame>(r#"{"type":"execute","cmd":"rm"}"#).is_none());
157 assert!(decode_line::<HubFrame>("not json").is_none());
158 }
159}