How I Built a macOS Menu Bar HUD with Rust + Tauri 2.0

rust dev.to

I recently built HiyokoBar — a push-type HUD for engineers that lives in the macOS menu bar and runs shell scripts on a schedule, displaying results as beautiful cards.
Here's a technical breakdown of the interesting parts.

Tech Stack

Backend: Rust / Tauri 2.0 / Tokio / Reqwest
Frontend: React + TypeScript + Tailwind CSS + Framer Motion
AI: Google Gemini API
Auth: Gumroad License API + SHA256 + macOS Keychain

Menu Bar App with Tauri 2.0
Tauri 2.0 makes menu bar apps surprisingly straightforward. The key is configuring the window to behave like a native HUD.
rust// tauri.conf.json
{
"app": {
"windows": [{
"visible": false,
"decorations": false,
"alwaysOnTop": true,
"skipTaskbar": true
}]
}
}
For positioning the window directly below the tray icon, I used tauri-plugin-positioner:
rustuse tauri_plugin_positioner::{Position, WindowExt};

fn show_window(app: &AppHandle) {
let window = app.get_webview_window("main").unwrap();
let _ = window.move_window(Position::TrayBottomCenter);
window.show().unwrap();
window.set_focus().unwrap();
}

Shell Script Execution with Jitter
Running multiple scripts simultaneously causes CPU spikes. I solved this by adding random jitter to each script's execution offset:
rustuse tokio::time::{sleep, Duration};
use rand::Rng;

async fn start_monitor(monitor: Monitor, tx: Sender) {
let offset = rand::thread_rng().gen_range(0..10);
sleep(Duration::from_secs(offset)).await;

loop {
    let result = execute_command(&monitor).await;
    tx.send(result).unwrap();
    sleep(Duration::from_secs(monitor.interval_secs)).await;
}
Enter fullscreen mode Exit fullscreen mode

}

Command Execution — Why It's Safe
I use std::process::Command with explicit argument separation, which calls execve directly without a shell:
rustasync fn execute_command(monitor: &Monitor) -> MonitorResult {
let output = Command::new(&monitor.command)
.args(&monitor.args)
.output()
.await?;

// ...
Enter fullscreen mode Exit fullscreen mode

}
This means even if a user types ; rm -rf / in the args field, it's treated as a literal string argument — not shell syntax. No injection possible at the OS level.

Gemini AI Error Analysis
When a script exits with a non-zero code and analyze_errors is enabled, I send the stdout/stderr to Gemini:
rustasync fn analyze_error(output: &str, api_key: &str) -> Result {
let prompt = format!(
"Analyze this error output and provide a concise diagnosis:\n\n{}",
output
);

let response = reqwest::Client::new()
    .post("https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent")
    .header("X-goog-api-key", api_key)
    .json(&serde_json::json!({
        "contents": [{"parts": [{"text": prompt}]}]
    }))
    .send()
    .await?;

// parse response...
Enter fullscreen mode Exit fullscreen mode

}

Secure API Key Storage with macOS Keychain
Never store API keys in plain JSON files. I use the keyring crate to store secrets in macOS Keychain:
rustuse keyring::Entry;

fn save_api_key(key: &str) -> Result<()> {
let entry = Entry::new("hiyoko-bar", "gemini-api-key")?;
entry.set_password(key)?;
Ok(())
}

fn load_api_key() -> Result {
let entry = Entry::new("hiyoko-bar", "gemini-api-key")?;
entry.get_password()
}

License Verification with Machine Binding
To prevent license sharing, I bind each license to a machine fingerprint:
rustuse sha2::{Sha256, Digest};

fn get_machine_id() -> String {
let uuid = get_io_platform_uuid(); // IOPlatformUUID via IOKit
let hostname = hostname::get().unwrap_or_default();

let mut hasher = Sha256::new();
hasher.update(format!("{}{}", uuid, hostname.to_string_lossy()));

format!("{:.16x}", hasher.finalize())
Enter fullscreen mode Exit fullscreen mode

}
The license is verified against Gumroad's API on first activation, then cached locally with a 7-day offline grace period.

Window Hide on Focus Loss
A subtle but important UX detail — the HUD should disappear when you click elsewhere:
rustwindow.on_window_event(|event| {
if let WindowEvent::Focused(false) = event {
window.hide().unwrap();
}
});

Result
The app went from idea to Gumroad listing in a single day, reusing architecture from my previous Tauri apps. Rust + Tauri 2.0 is an incredibly productive stack for macOS utilities.
HiyokoBar: https://hiyokoko.gumroad.com/l/hiyokobar

🇯🇵 日本語版: https://hiyokoko.gumroad.com/l/hiyokobar_jp

🐤 Launched on Product Hunt today — would love your support!
https://www.producthunt.com/products/hiyokobar

X: @hiyoyoko

Source: dev.to

arrow_back Back to Tutorials