Skip to main content

planarian_tray/
main.rs

1//! `planarian-tray`: the status indicator. A StatusNotifierItem (the
2//! freedesktop tray protocol COSMIC's panel shows) with the planarian mark:
3//! no dot when all is well, blue-grey while a change is being built or
4//! applied, amber when something needs a look, red when something needs a
5//! person. Its tooltip says what; clicking it opens the console. Read-only,
6//! like the console in Phase A: it asks the daemon for its console report
7//! every few seconds and changes nothing.
8
9mod 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
22/// One request to the daemon, one answer.
23fn 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
36/// The console next to this binary (both ship in one package), else on PATH.
37fn 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    // `--show`: the console stays up even when nothing needs attention.
50    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    // The panel may come up after this service; register once it does.
127    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}