#[cfg(not(target_os = "macos"))]
compile_error!("vaken only builds on macOS — it wraps the macOS-only `caffeinate` command");
use std::process::{Child, Command, Stdio};
use std::sync::{Arc, Mutex};
use muda::{Menu, MenuId, MenuItem};
use tray_icon::{Icon, TrayIcon, TrayIconBuilder, menu::MenuEvent};
use winit::application::ApplicationHandler;
use winit::event::{StartCause, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::window::WindowId;
const CAFFEINATE_FLAGS: &[&str] = &["-d", "-i", "-m", "-s", "-u"];
const INACTIVE_ICON_BYTES: &[u8] = include_bytes!("../icons/sovbar.png");
const ACTIVE_ICON_BYTES: &[u8] = include_bytes!("../icons/vaken.png");
#[derive(Debug)]
enum UserEvent {
Menu(MenuEvent),
}
fn decode_icon(bytes: &'static [u8]) -> Icon {
let image = image::load_from_memory(bytes)
.expect("decoding embedded icon")
.into_rgba8();
let (width, height) = image.dimensions();
let rgba = image.into_raw();
Icon::from_rgba(rgba, width, height).expect("constructing icon from embedded bytes")
}
fn stop_caffeinate(slot: &Mutex<Option<Child>>) {
if let Some(mut child) = slot.lock().unwrap().take() {
let _ = child.kill();
let _ = child.wait();
}
}
fn sweep_orphan_caffeinates() {
let Ok(output) = Command::new("pgrep").args(["-x", "caffeinate"]).output() else {
return;
};
let pids = String::from_utf8_lossy(&output.stdout);
for pid in pids.lines().filter(|l| !l.trim().is_empty()) {
let Ok(ps_out) = Command::new("ps").args(["-o", "args=", "-p", pid]).output() else {
continue;
};
let args = String::from_utf8_lossy(&ps_out.stdout);
let argv: Vec<&str> = args.split_whitespace().collect();
let has_all_flags = CAFFEINATE_FLAGS.iter().all(|f| argv.contains(f));
let has_w = argv.contains(&"-w");
if has_all_flags && !has_w {
let _ = Command::new("kill").arg(pid).output();
eprintln!("vaken: killed orphan caffeinate pid={pid}");
}
}
}
struct App {
caffeinate: Arc<Mutex<Option<Child>>>,
toggle_item: MenuItem,
toggle_id: MenuId,
quit_id: MenuId,
vaken_pid: String,
inactive_icon: Icon,
active_icon: Icon,
menu: Option<Menu>,
tray: Option<TrayIcon>,
}
impl App {
fn new() -> Result<Self, Box<dyn std::error::Error>> {
sweep_orphan_caffeinates();
let toggle_item = MenuItem::new("Enable Awake Mode", true, None);
let quit_item = MenuItem::new("Quit Vaken", true, None);
let toggle_id = toggle_item.id().clone();
let quit_id = quit_item.id().clone();
let menu = Menu::new();
menu.append(&toggle_item)?;
menu.append(&quit_item)?;
Ok(Self {
caffeinate: Arc::new(Mutex::new(None)),
toggle_item,
toggle_id,
quit_id,
vaken_pid: std::process::id().to_string(),
inactive_icon: decode_icon(INACTIVE_ICON_BYTES),
active_icon: decode_icon(ACTIVE_ICON_BYTES),
menu: Some(menu),
tray: None,
})
}
fn ensure_tray(&mut self) {
if self.tray.is_some() {
return;
}
let menu = self
.menu
.take()
.expect("menu must still be present when ensure_tray runs");
let tray = TrayIconBuilder::new()
.with_menu(Box::new(menu))
.with_tooltip("Vaken")
.with_icon(self.inactive_icon.clone())
.build()
.expect("building tray icon");
self.tray = Some(tray);
}
fn handle_menu(&mut self, event: MenuEvent, event_loop: &ActiveEventLoop) {
if event.id == self.quit_id {
stop_caffeinate(&self.caffeinate);
event_loop.exit();
return;
}
if event.id != self.toggle_id {
return;
}
let mut proc = self.caffeinate.lock().unwrap();
match proc.take() {
None => match Command::new("caffeinate")
.args(CAFFEINATE_FLAGS)
.arg("-w")
.arg(&self.vaken_pid)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
{
Ok(child) => {
*proc = Some(child);
self.toggle_item.set_text("Disable Awake Mode");
if let Some(tray) = &self.tray {
let _ = tray.set_icon(Some(self.active_icon.clone()));
let _ = tray.set_tooltip(Some("Vaken — Awake Mode"));
}
}
Err(err) => {
eprintln!("Failed to launch caffeinate: {err}");
self.toggle_item.set_text("Enable Awake Mode (failed)");
}
},
Some(mut child) => {
let _ = child.kill();
let _ = child.wait();
self.toggle_item.set_text("Enable Awake Mode");
if let Some(tray) = &self.tray {
let _ = tray.set_icon(Some(self.inactive_icon.clone()));
let _ = tray.set_tooltip(Some("Vaken — Sleepy Mode"));
}
}
}
}
}
impl ApplicationHandler<UserEvent> for App {
fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
if matches!(cause, StartCause::Init) {
self.ensure_tray();
}
}
fn resumed(&mut self, _event_loop: &ActiveEventLoop) {
}
fn window_event(&mut self, _: &ActiveEventLoop, _: WindowId, _: WindowEvent) {
}
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
match event {
UserEvent::Menu(e) => self.handle_menu(e, event_loop),
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let event_loop = EventLoop::<UserEvent>::with_user_event().build()?;
event_loop.set_control_flow(ControlFlow::Wait);
let proxy = event_loop.create_proxy();
MenuEvent::set_event_handler(Some(move |event| {
let _ = proxy.send_event(UserEvent::Menu(event));
}));
let mut app = App::new()?;
event_loop.run_app(&mut app)?;
Ok(())
}