use std::sync::{Arc, Mutex};
use ksni::{Icon, ToolTip, TrayMethods};
use tokio::sync::watch;
use tracing::{info, warn};
use crate::State;
mod icons {
pub fn circle_icon(argb: u32) -> Vec<u8> {
let size = 16;
let center = size as f32 / 2.0;
let radius = 6.0;
let mut pixels = Vec::with_capacity(size * size * 4);
for y in 0..size {
for x in 0..size {
let dx = x as f32 + 0.5 - center;
let dy = y as f32 + 0.5 - center;
let dist = (dx * dx + dy * dy).sqrt();
if dist <= radius {
pixels.extend_from_slice(&argb.to_be_bytes());
} else if dist <= radius + 1.0 {
let alpha = ((radius + 1.0 - dist) * 255.0) as u8;
let [_, r, g, b] = argb.to_be_bytes();
pixels.extend_from_slice(&[alpha, r, g, b]);
} else {
pixels.extend_from_slice(&[0, 0, 0, 0]);
}
}
}
pixels
}
pub fn idle() -> Vec<u8> {
circle_icon(0xFF_88_88_88)
}
pub fn recording() -> Vec<u8> {
circle_icon(0xFF_E0_40_40)
}
pub fn transcribing() -> Vec<u8> {
circle_icon(0xFF_E0_A0_20)
}
}
struct TrayState {
current: State,
}
struct WhisrsTray {
state: Arc<Mutex<TrayState>>,
}
impl ksni::Tray for WhisrsTray {
fn id(&self) -> String {
"whisrs".to_string()
}
fn title(&self) -> String {
let state = self.state.lock().unwrap();
match state.current {
State::Idle => "whisrs — idle".to_string(),
State::Recording => "whisrs — recording".to_string(),
State::Transcribing => "whisrs — transcribing".to_string(),
}
}
fn icon_pixmap(&self) -> Vec<Icon> {
let state = self.state.lock().unwrap();
let data = match state.current {
State::Idle => icons::idle(),
State::Recording => icons::recording(),
State::Transcribing => icons::transcribing(),
};
vec![Icon {
width: 16,
height: 16,
data,
}]
}
fn tool_tip(&self) -> ToolTip {
let state = self.state.lock().unwrap();
let description = match state.current {
State::Idle => "Idle — ready to record",
State::Recording => "Recording...",
State::Transcribing => "Transcribing...",
};
ToolTip {
title: "whisrs".to_string(),
description: description.to_string(),
icon_name: String::new(),
icon_pixmap: Vec::new(),
}
}
}
pub async fn spawn_tray(mut state_rx: watch::Receiver<State>) {
let tray_state = Arc::new(Mutex::new(TrayState {
current: State::Idle,
}));
let tray = WhisrsTray {
state: Arc::clone(&tray_state),
};
let handle = match tray.spawn().await {
Ok(h) => {
info!("system tray started");
h
}
Err(e) => {
warn!("failed to start system tray: {e} — continuing without tray");
return;
}
};
let state_ref = Arc::clone(&tray_state);
tokio::spawn(async move {
while state_rx.changed().await.is_ok() {
let new_state = *state_rx.borrow();
{
let mut ts = state_ref.lock().unwrap();
ts.current = new_state;
}
handle.update(|_tray| {}).await;
}
});
}