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}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(rename_all = "snake_case")]
41pub enum PreviewKind {
42 File,
43 Url,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48#[serde(tag = "type", rename_all = "snake_case")]
49pub enum PanelFrame {
50 Hello {
52 session: PanelSession,
53 },
54 State {
56 cwd: String,
57 agent: String,
58 },
59 Panel {
61 focus: PanelTab,
62 },
63 Preview {
65 kind: PreviewKind,
66 target: String,
67 },
68 Bye,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(tag = "type", rename_all = "snake_case")]
76pub enum HubFrame {
77 Insert { text: String },
78}
79
80pub fn decode_line<T: serde::de::DeserializeOwned>(line: &str) -> Option<T> {
83 serde_json::from_str(line).ok()
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use std::path::Path;
90
91 #[test]
92 fn run_dir_under_runtime() {
93 assert_eq!(
94 murmur_run_dir(Path::new("/h")),
95 Path::new("/h/runtime/murmur")
96 );
97 }
98
99 #[test]
100 fn frames_round_trip() {
101 let s = PanelSession {
102 pid: 42,
103 agent: "mur".into(),
104 cwd: "/tmp".into(),
105 sock: "/h/runtime/murmur/42.sock".into(),
106 terminal_program: Some("iTerm.app".into()),
107 proto_version: PANEL_PROTO_VERSION,
108 };
109 let line = serde_json::to_string(&PanelFrame::Hello { session: s }).unwrap();
110 assert!(line.contains("\"type\":\"hello\""));
111 assert!(matches!(
112 decode_line::<PanelFrame>(&line),
113 Some(PanelFrame::Hello { .. })
114 ));
115
116 let line = serde_json::to_string(&PanelFrame::Panel {
117 focus: PanelTab::Preview,
118 })
119 .unwrap();
120 assert!(line.contains("\"focus\":\"preview\""));
121
122 let line = serde_json::to_string(&HubFrame::Insert {
123 text: "/help".into(),
124 })
125 .unwrap();
126 assert!(matches!(
127 decode_line::<HubFrame>(&line),
128 Some(HubFrame::Insert { text }) if text == "/help"
129 ));
130 }
131
132 #[test]
133 fn unknown_frames_are_none() {
134 assert!(decode_line::<PanelFrame>(r#"{"type":"from_the_future"}"#).is_none());
135 assert!(decode_line::<HubFrame>(r#"{"type":"execute","cmd":"rm"}"#).is_none());
136 assert!(decode_line::<HubFrame>("not json").is_none());
137 }
138}