use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
const TRAY_BIN: &str = if cfg!(windows) {
"llmtrim-tray.exe"
} else {
"llmtrim-tray"
};
pub fn tray_binary() -> Option<PathBuf> {
let exe = std::env::current_exe().ok()?;
resolve_tray_binary(exe.parent()?)
}
fn resolve_tray_binary(dir: &Path) -> Option<PathBuf> {
let candidate = dir.join(TRAY_BIN);
candidate.is_file().then_some(candidate)
}
pub fn run() -> Result<()> {
let Some(bin) = tray_binary() else {
anyhow::bail!(
"the llmtrim tray app isn't installed next to this binary.\n\
It ships with the desktop bundles — install with `npm i -g @llmtrim/cli` \
or `brew install fkiene/tap/llmtrim` and try again."
);
};
std::process::Command::new(&bin)
.stderr(std::process::Stdio::inherit())
.spawn()
.with_context(|| format!("failed to launch the tray app at {}", bin.display()))?;
Ok(())
}
pub fn launch_detached() -> Result<()> {
launch_detached_from(tray_binary())
}
fn launch_detached_from(bin: Option<PathBuf>) -> Result<()> {
let Some(bin) = bin else {
return Ok(());
};
std::process::Command::new(&bin)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.with_context(|| format!("failed to launch the tray app at {}", bin.display()))?;
Ok(())
}
const NUDGE_MARKER: &str = "tray-nudged";
pub fn nudge_once() -> Option<String> {
let dir = crate::daemon::home_dir().ok()?;
nudge_in(&dir, tray_binary().is_some())
}
fn nudge_in(state_dir: &Path, tray_present: bool) -> Option<String> {
if !tray_present {
return None;
}
let marker = state_dir.join(NUDGE_MARKER);
if marker.exists() {
return None;
}
let _ = std::fs::create_dir_all(state_dir);
let _ = std::fs::write(&marker, b"1\n");
Some(
"Desktop tray available: run `llmtrim tray` to watch savings in your menu bar.".to_string(),
)
}
#[cfg(test)]
mod tests {
use super::*;
struct TempDir(PathBuf);
impl TempDir {
fn new(suffix: &str) -> Self {
let dir = std::env::temp_dir()
.join(format!("llmtrim-tray-test-{}-{suffix}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create temp dir");
Self(dir)
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[test]
fn resolve_returns_none_when_absent() {
let dir = TempDir::new("absent");
assert!(resolve_tray_binary(dir.path()).is_none());
}
#[test]
fn resolve_finds_sibling_binary() {
let dir = TempDir::new("present");
let bin = dir.path().join(TRAY_BIN);
std::fs::write(&bin, b"#!/bin/sh\n").expect("write fake binary");
assert_eq!(resolve_tray_binary(dir.path()), Some(bin));
}
#[test]
fn resolve_ignores_a_directory_named_like_the_binary() {
let dir = TempDir::new("dir-named-bin");
std::fs::create_dir_all(dir.path().join(TRAY_BIN)).expect("create dir");
assert!(resolve_tray_binary(dir.path()).is_none());
}
#[test]
fn launch_detached_is_a_no_op_when_not_installed() {
assert!(launch_detached_from(None).is_ok());
}
#[test]
fn nudge_stays_quiet_when_tray_not_installed() {
let dir = TempDir::new("nudge-absent");
assert!(nudge_in(dir.path(), false).is_none());
assert!(!dir.path().join(NUDGE_MARKER).exists());
}
#[test]
fn nudge_shows_once_then_goes_silent() {
let dir = TempDir::new("nudge-once");
let first = nudge_in(dir.path(), true);
assert!(first.is_some_and(|m| m.contains("llmtrim tray")));
assert!(dir.path().join(NUDGE_MARKER).exists());
assert!(nudge_in(dir.path(), true).is_none());
}
}