use ksni::{menu::*, Tray, TrayService};
use std::sync::{Arc, Mutex};
use tokio::sync::mpsc;
use tracing::{debug, warn};
const LED_THEMES: &[(&str, u8)] = &[
("Rainbow", 1),
("Breathing", 2),
("Colors", 3),
("Off", 4),
("Auto", 5),
];
const ORIENTATIONS: &[(&str, &str)] = &[
("Landscape", "landscape"),
("Portrait", "portrait"),
("Landscape (Upside Down)", "landscape-upside-down"),
("Portrait (Upside Down)", "portrait-upside-down"),
];
const FACES: &[(&str, &str)] = &[("ASCII", "ascii"), ("Professional", "professional")];
#[derive(Debug, Clone)]
pub enum TrayCommand {
SetLedTheme(u8),
SetOrientation(String),
SetFace(String),
SetNetworkInterface(String),
QuitDaemon,
}
pub struct TrayState {
pub connected: bool,
pub web_enabled: bool,
pub led_theme: u8,
pub led_intensity: u8,
pub led_speed: u8,
pub orientation: String,
pub face: String,
pub network_interface: String,
pub network_interfaces: Vec<String>,
}
impl Default for TrayState {
fn default() -> Self {
Self {
connected: false,
web_enabled: false,
led_theme: 2, led_intensity: 3,
led_speed: 3,
orientation: "landscape".to_string(),
face: "professional".to_string(),
network_interface: String::new(),
network_interfaces: Vec::new(),
}
}
}
pub struct HT32PanelTray {
state: Arc<Mutex<TrayState>>,
command_tx: mpsc::Sender<TrayCommand>,
}
impl HT32PanelTray {
pub fn new(state: Arc<Mutex<TrayState>>, command_tx: mpsc::Sender<TrayCommand>) -> Self {
Self { state, command_tx }
}
fn set_led_theme(&mut self, index: usize) {
if let Some((_, theme)) = LED_THEMES.get(index) {
if let Err(e) = self.command_tx.try_send(TrayCommand::SetLedTheme(*theme)) {
debug!("Failed to send LED theme command: {}", e);
}
if let Ok(mut s) = self.state.lock() {
s.led_theme = *theme;
}
}
}
fn set_orientation(&mut self, index: usize) {
if let Some((_, orientation)) = ORIENTATIONS.get(index) {
if let Err(e) = self
.command_tx
.try_send(TrayCommand::SetOrientation(orientation.to_string()))
{
debug!("Failed to send orientation command: {}", e);
}
if let Ok(mut s) = self.state.lock() {
s.orientation = orientation.to_string();
}
}
}
fn set_face(&mut self, index: usize) {
if let Some((_, face)) = FACES.get(index) {
if let Err(e) = self
.command_tx
.try_send(TrayCommand::SetFace(face.to_string()))
{
debug!("Failed to send face command: {}", e);
}
if let Ok(mut s) = self.state.lock() {
s.face = face.to_string();
}
}
}
fn set_network_interface(&mut self, index: usize) {
let interface = {
let state = self.state.lock().unwrap();
if index == 0 {
"auto".to_string()
} else {
state
.network_interfaces
.get(index - 1)
.cloned()
.unwrap_or_default()
}
};
if !interface.is_empty() {
if let Err(e) = self
.command_tx
.try_send(TrayCommand::SetNetworkInterface(interface.clone()))
{
debug!("Failed to send network interface command: {}", e);
}
if let Ok(mut s) = self.state.lock() {
s.network_interface = if interface == "auto" {
String::new()
} else {
interface
};
}
}
}
fn quit_daemon(&self) {
if let Err(e) = self.command_tx.try_send(TrayCommand::QuitDaemon) {
debug!("Failed to send quit command: {}", e);
}
}
fn open_web_ui(&self) {
if let Err(e) = open::that("http://localhost:8686") {
warn!("Failed to open web UI: {}", e);
}
}
}
impl Tray for HT32PanelTray {
fn id(&self) -> String {
"ht32-panel-applet".to_string()
}
fn title(&self) -> String {
"HT32 Panel".to_string()
}
fn icon_name(&self) -> String {
let state = self.state.lock().unwrap();
if state.connected {
"display-brightness-symbolic".to_string()
} else {
"display-brightness-off-symbolic".to_string()
}
}
fn menu(&self) -> Vec<MenuItem<Self>> {
let state = self.state.lock().unwrap();
let current_theme = state.led_theme;
let current_orientation = state.orientation.clone();
let current_face = state.face.clone();
let current_network = state.network_interface.clone();
let network_interfaces = state.network_interfaces.clone();
let web_enabled = state.web_enabled;
drop(state);
let led_selected = LED_THEMES
.iter()
.position(|(_, t)| *t == current_theme)
.unwrap_or(0);
let orientation_selected = ORIENTATIONS
.iter()
.position(|(_, o)| *o == current_orientation)
.unwrap_or(0);
let face_selected = FACES
.iter()
.position(|(_, f)| *f == current_face)
.unwrap_or(0);
let network_selected = if current_network.is_empty() {
0 } else {
network_interfaces
.iter()
.position(|i| i == ¤t_network)
.map(|i| i + 1) .unwrap_or(0)
};
let led_options: Vec<RadioItem> = LED_THEMES
.iter()
.map(|(name, _)| RadioItem {
label: name.to_string(),
..Default::default()
})
.collect();
let orientation_options: Vec<RadioItem> = ORIENTATIONS
.iter()
.map(|(name, _)| RadioItem {
label: name.to_string(),
..Default::default()
})
.collect();
let face_options: Vec<RadioItem> = FACES
.iter()
.map(|(name, _)| RadioItem {
label: name.to_string(),
..Default::default()
})
.collect();
let mut network_options: Vec<RadioItem> = vec![RadioItem {
label: "Auto".to_string(),
..Default::default()
}];
for iface in &network_interfaces {
network_options.push(RadioItem {
label: iface.clone(),
..Default::default()
});
}
let mut items = vec![
SubMenu {
label: "Display Face".to_string(),
submenu: vec![RadioGroup {
selected: face_selected,
select: Box::new(|tray: &mut Self, index| {
tray.set_face(index);
}),
options: face_options,
}
.into()],
..Default::default()
}
.into(),
SubMenu {
label: "Orientation".to_string(),
submenu: vec![RadioGroup {
selected: orientation_selected,
select: Box::new(|tray: &mut Self, index| {
tray.set_orientation(index);
}),
options: orientation_options,
}
.into()],
..Default::default()
}
.into(),
SubMenu {
label: "Network Interface".to_string(),
submenu: vec![RadioGroup {
selected: network_selected,
select: Box::new(|tray: &mut Self, index| {
tray.set_network_interface(index);
}),
options: network_options,
}
.into()],
..Default::default()
}
.into(),
SubMenu {
label: "LED Theme".to_string(),
submenu: vec![RadioGroup {
selected: led_selected,
select: Box::new(|tray: &mut Self, index| {
tray.set_led_theme(index);
}),
options: led_options,
}
.into()],
..Default::default()
}
.into(),
MenuItem::Separator,
];
if web_enabled {
items.push(
StandardItem {
label: "Open Web UI".to_string(),
activate: Box::new(|tray: &mut Self| {
tray.open_web_ui();
}),
..Default::default()
}
.into(),
);
items.push(MenuItem::Separator);
}
items.push(
StandardItem {
label: "Quit Daemon".to_string(),
activate: Box::new(|tray: &mut Self| {
tray.quit_daemon();
}),
..Default::default()
}
.into(),
);
items
}
}
pub fn create_tray(
state: Arc<Mutex<TrayState>>,
) -> anyhow::Result<(TrayService<HT32PanelTray>, mpsc::Receiver<TrayCommand>)> {
let (command_tx, command_rx) = mpsc::channel(16);
let tray = HT32PanelTray::new(state, command_tx);
let service = TrayService::new(tray);
Ok((service, command_rx))
}