tray-wrapper 0.4.0

A simple wrapper library to make it easy to run servers with a GUI tray icon
Documentation
use thiserror::Error;
use tray_icon::{
    Icon, TrayIcon, TrayIconBuilder,
    menu::{Menu, MenuItem},
};

use crate::{event_loop::UserEvent, server_status::ServerStatus};

pub struct MenuState {
    tray_icon: TrayIcon,
    status_item: MenuItem,
    quit_item: MenuItem,
    tray_menu: Menu,
}

impl MenuState {
    pub fn new(icon: Icon, version: Option<String>) -> Result<Self, MenuStateError> {
        let tray_icon = TrayIconBuilder::new().with_icon(icon).build()?;

        let status_item = MenuItem::new("Starting Up", false, None);
        let quit_item = MenuItem::new("Quit", true, None);
        let tray_menu = Menu::new();
        tray_menu.append(&status_item)?;
        if let Some(v) = version {
            tray_menu.append(&MenuItem::new(v, false, None))?;
        }
        tray_menu.append(&quit_item)?;

        Ok(Self {
            tray_icon,
            status_item,
            quit_item,
            tray_menu,
        })
    }

    pub fn update_tray_icon(&mut self, status: ServerStatus) {
        let (title, text) = status_display(&status);
        self.tray_icon.set_title(Some(title));
        self.status_item.set_text(text);
        self.tray_icon
            .set_menu(Some(Box::new(self.tray_menu.clone())));
    }

    pub fn quit_matches(&self, event: &UserEvent) -> bool {
        matches!(event, UserEvent::Menu(me) if me.id == self.quit_item.id())
    }
}

fn status_display(status: &ServerStatus) -> (&'static str, &str) {
    match status {
        ServerStatus::StartUp => ("?", "In startup"),
        ServerStatus::Running => ("", "Running"),
        ServerStatus::Stopped(s) => ("X", s),
        ServerStatus::Error(e) => ("E", e),
    }
}

#[derive(Error, Debug)]
pub enum MenuStateError {
    #[error(transparent)]
    MenuError(#[from] tray_icon::menu::Error),
    #[error(transparent)]
    TrayError(#[from] tray_icon::Error),
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn startup_display() {
        assert_eq!(status_display(&ServerStatus::StartUp), ("?", "In startup"));
    }

    #[test]
    fn running_display() {
        assert_eq!(status_display(&ServerStatus::Running), ("", "Running"));
    }

    #[test]
    fn stopped_display_passes_through_message() {
        let status = ServerStatus::Stopped("custom stop message".to_string());
        assert_eq!(status_display(&status), ("X", "custom stop message"));
    }

    #[test]
    fn error_display_passes_through_message() {
        let status = ServerStatus::Error("something failed".to_string());
        assert_eq!(status_display(&status), ("E", "something failed"));
    }
}