1mod icon;
10mod state;
11
12use ksni::blocking::TrayMethods;
13use planarian_ipc::{ConsoleReport, Request, Response, SOCKET};
14use state::{Level, Summary, classify};
15use std::io::{BufRead, BufReader, Write};
16use std::os::unix::net::UnixStream;
17use std::path::PathBuf;
18use std::time::Duration;
19
20const POLL: Duration = Duration::from_secs(5);
21
22fn report() -> Result<ConsoleReport, String> {
24 let path = std::env::var("PLANARIAN_SOCKET").unwrap_or_else(|_| SOCKET.into());
25 let mut s = UnixStream::connect(&path).map_err(|e| e.to_string())?;
26 let _ = s.set_read_timeout(Some(Duration::from_secs(10)));
27 s.write_all(Request::Console.encode().as_bytes()).map_err(|e| e.to_string())?;
28 let mut line = String::new();
29 BufReader::new(s).read_line(&mut line).map_err(|e| e.to_string())?;
30 match serde_json::from_str(&line).map_err(|e| format!("bad answer: {e}"))? {
31 Response::Console(r) => Ok(*r),
32 other => Err(format!("unexpected answer: {other:?}")),
33 }
34}
35
36fn console_command() -> PathBuf {
38 if let Ok(p) = std::env::var("PLANARIAN_CONSOLE") {
39 return p.into();
40 }
41 std::env::current_exe()
42 .ok()
43 .and_then(|e| e.parent().map(|d| d.join("planarian-console")))
44 .filter(|p| p.exists())
45 .unwrap_or_else(|| "planarian-console".into())
46}
47
48fn open_console() {
49 if let Err(e) = std::process::Command::new(console_command()).arg("--show").spawn() {
51 eprintln!("planarian-tray: cannot start the console: {e}");
52 }
53}
54
55struct Tray {
56 summary: Summary,
57}
58
59impl ksni::Tray for Tray {
60 fn id(&self) -> String {
61 "planarian".into()
62 }
63
64 fn title(&self) -> String {
65 "planarian".into()
66 }
67
68 fn category(&self) -> ksni::Category {
69 ksni::Category::SystemServices
70 }
71
72 fn status(&self) -> ksni::Status {
73 match self.summary.level {
74 Level::Ok | Level::Working => ksni::Status::Active,
75 Level::Attention | Level::Fault => ksni::Status::NeedsAttention,
76 }
77 }
78
79 fn icon_pixmap(&self) -> Vec<ksni::Icon> {
80 icon::SIZES
81 .iter()
82 .filter_map(|s| icon::render(self.summary.level, *s))
83 .map(|(w, h, data)| ksni::Icon { width: w as i32, height: h as i32, data })
84 .collect()
85 }
86
87 fn attention_icon_pixmap(&self) -> Vec<ksni::Icon> {
88 self.icon_pixmap()
89 }
90
91 fn tool_tip(&self) -> ksni::ToolTip {
92 ksni::ToolTip {
93 title: format!("planarian: {}", self.summary.line),
94 description: String::new(),
95 icon_name: String::new(),
96 icon_pixmap: vec![],
97 }
98 }
99
100 fn activate(&mut self, _x: i32, _y: i32) {
101 open_console();
102 }
103
104 fn menu(&self) -> Vec<ksni::MenuItem<Self>> {
105 use ksni::menu::StandardItem;
106 vec![
107 StandardItem { label: self.summary.line.clone(), enabled: false, ..Default::default() }.into(),
108 ksni::MenuItem::Separator,
109 StandardItem { label: "Open console".into(), activate: Box::new(|_| open_console()), ..Default::default() }
110 .into(),
111 StandardItem { label: "Quit".into(), activate: Box::new(|_| std::process::exit(0)), ..Default::default() }
112 .into(),
113 ]
114 }
115}
116
117fn poll() -> Summary {
118 match report() {
119 Ok(r) => classify(Ok(&r)),
120 Err(e) => classify(Err(&e)),
121 }
122}
123
124fn main() {
125 let tray = Tray { summary: poll() };
126 let handle = match tray.assume_sni_available(true).spawn() {
128 Ok(h) => h,
129 Err(e) => {
130 eprintln!("planarian-tray: {e}");
131 std::process::exit(1);
132 }
133 };
134 let mut last = None;
135 loop {
136 let s = poll();
137 if last.as_ref() != Some(&s) {
138 eprintln!("planarian-tray: {:?}: {}", s.level, s.line);
139 last = Some(s.clone());
140 handle.update(|t: &mut Tray| t.summary = s);
141 }
142 if handle.is_closed() {
143 break;
144 }
145 std::thread::sleep(POLL);
146 }
147}