use serde::{Deserialize, Serialize};
use tauri::{
AppHandle, Emitter, Manager, Runtime, Window, command,
menu::{AboutMetadata, Menu, MenuBuilder, MenuItem, SubmenuBuilder},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
};
pub const MENU_ID_PREFERENCES: &str = "menu.preferences";
pub const MENU_ID_SHOW_WINDOW: &str = "menu.show_window";
pub const MENU_ID_HIDE_WINDOW: &str = "menu.hide_window";
pub const MENU_ID_CHECK_UPDATES: &str = "menu.check_updates";
pub const MENU_ID_REPORT_ISSUE: &str = "menu.report_issue";
pub const MENU_ID_DOCUMENTATION: &str = "menu.documentation";
pub const MENU_ID_KEYBOARD_SHORTCUTS: &str = "menu.shortcuts";
pub const MENU_ID_NEW_INFERENCE: &str = "menu.new_inference";
pub const MENU_ID_OPEN_MODEL: &str = "menu.open_model";
pub const MENU_ID_IMPORT_MODEL: &str = "menu.import_model";
pub const MENU_ID_EXPORT_RESULTS: &str = "menu.export_results";
pub const MENU_ID_MODEL_INFO: &str = "menu.model_info";
pub const MENU_ID_VALIDATE_MODELS: &str = "menu.validate_models";
pub const MENU_ID_QUICK_INFERENCE: &str = "menu.quick_inference";
pub const MENU_ID_BATCH_INFERENCE: &str = "menu.batch_inference";
pub const MENU_ID_STOP_INFERENCE: &str = "menu.stop_inference";
pub const MENU_ID_VIEW_DASHBOARD: &str = "menu.view_dashboard";
pub const MENU_ID_VIEW_MODELS: &str = "menu.view_models";
pub const MENU_ID_VIEW_INFERENCE: &str = "menu.view_inference";
pub const MENU_ID_VIEW_METRICS: &str = "menu.view_metrics";
pub const TRAY_ID_DASHBOARD: &str = "tray.dashboard";
pub const TRAY_ID_MODELS: &str = "tray.models";
pub const TRAY_ID_INFERENCE: &str = "tray.quick_inference";
pub const TRAY_ID_SHOW: &str = "tray.show";
pub const TRAY_ID_HIDE: &str = "tray.hide";
pub const TRAY_ID_QUIT: &str = "tray.quit";
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MacOSNotification {
pub title: String,
pub body: String,
pub icon: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum VibrancyEffect {
AppearanceBased,
Light,
Dark,
Titlebar,
Selection,
Menu,
Popover,
Sidebar,
HeaderView,
Sheet,
WindowBackground,
HudWindow,
FullScreenUI,
Tooltip,
ContentBackground,
UnderWindowBackground,
UnderPageBackground,
}
pub fn create_app_menu<R: Runtime>(app: &AppHandle<R>) -> Result<Menu<R>, String> {
let about_metadata = AboutMetadata {
name: Some("Inferno AI Desktop".to_string()),
version: Some(env!("CARGO_PKG_VERSION").to_string()),
..Default::default()
};
let preferences = MenuItem::with_id(
app,
MENU_ID_PREFERENCES,
"Preferences…",
true,
Some("Cmd+,"),
)
.map_err(|e| e.to_string())?;
let new_inference = MenuItem::with_id(
app,
MENU_ID_NEW_INFERENCE,
"New Inference",
true,
Some("Cmd+N"),
)
.map_err(|e| e.to_string())?;
let open_model = MenuItem::with_id(app, MENU_ID_OPEN_MODEL, "Open Model…", true, Some("Cmd+O"))
.map_err(|e| e.to_string())?;
let import_model = MenuItem::with_id(
app,
MENU_ID_IMPORT_MODEL,
"Import Model…",
true,
Some("Cmd+Shift+I"),
)
.map_err(|e| e.to_string())?;
let export_results = MenuItem::with_id(
app,
MENU_ID_EXPORT_RESULTS,
"Export Results…",
true,
Some("Cmd+E"),
)
.map_err(|e| e.to_string())?;
let model_info = MenuItem::with_id(
app,
MENU_ID_MODEL_INFO,
"Model Information",
true,
Some("Cmd+I"),
)
.map_err(|e| e.to_string())?;
let validate_models = MenuItem::with_id(
app,
MENU_ID_VALIDATE_MODELS,
"Validate Models",
true,
None::<&str>,
)
.map_err(|e| e.to_string())?;
let quick_inference = MenuItem::with_id(
app,
MENU_ID_QUICK_INFERENCE,
"Quick Inference",
true,
Some("Cmd+R"),
)
.map_err(|e| e.to_string())?;
let batch_inference = MenuItem::with_id(
app,
MENU_ID_BATCH_INFERENCE,
"Batch Inference",
true,
Some("Cmd+Shift+R"),
)
.map_err(|e| e.to_string())?;
let stop_inference = MenuItem::with_id(
app,
MENU_ID_STOP_INFERENCE,
"Stop All Inference",
true,
Some("Cmd+."),
)
.map_err(|e| e.to_string())?;
let view_dashboard = MenuItem::with_id(
app,
MENU_ID_VIEW_DASHBOARD,
"Dashboard",
true,
Some("Cmd+1"),
)
.map_err(|e| e.to_string())?;
let view_models = MenuItem::with_id(app, MENU_ID_VIEW_MODELS, "Models", true, Some("Cmd+2"))
.map_err(|e| e.to_string())?;
let view_inference = MenuItem::with_id(
app,
MENU_ID_VIEW_INFERENCE,
"Inference",
true,
Some("Cmd+3"),
)
.map_err(|e| e.to_string())?;
let view_metrics = MenuItem::with_id(app, MENU_ID_VIEW_METRICS, "Metrics", true, Some("Cmd+4"))
.map_err(|e| e.to_string())?;
let documentation = MenuItem::with_id(
app,
MENU_ID_DOCUMENTATION,
"Documentation",
true,
None::<&str>,
)
.map_err(|e| e.to_string())?;
let shortcuts = MenuItem::with_id(
app,
MENU_ID_KEYBOARD_SHORTCUTS,
"Keyboard Shortcuts",
true,
None::<&str>,
)
.map_err(|e| e.to_string())?;
let check_updates = MenuItem::with_id(
app,
MENU_ID_CHECK_UPDATES,
"Check for Updates",
true,
None::<&str>,
)
.map_err(|e| e.to_string())?;
let report_issue = MenuItem::with_id(
app,
MENU_ID_REPORT_ISSUE,
"Report Issue…",
true,
None::<&str>,
)
.map_err(|e| e.to_string())?;
let show_window = MenuItem::with_id(
app,
MENU_ID_SHOW_WINDOW,
"Show Window",
true,
Some("Cmd+Shift+H"),
)
.map_err(|e| e.to_string())?;
let hide_window =
MenuItem::with_id(app, MENU_ID_HIDE_WINDOW, "Hide Window", true, None::<&str>)
.map_err(|e| e.to_string())?;
let inferno_submenu = SubmenuBuilder::with_id(app, "menu.inferno", "Inferno")
.about(Some(about_metadata))
.separator()
.item(&preferences)
.separator()
.hide()
.hide_others()
.show_all()
.separator()
.quit_with_text("Quit Inferno")
.build()
.map_err(|e| e.to_string())?;
let file_submenu = SubmenuBuilder::with_id(app, "menu.file", "File")
.item(&new_inference)
.item(&open_model)
.separator()
.item(&import_model)
.item(&export_results)
.separator()
.close_window()
.build()
.map_err(|e| e.to_string())?;
let edit_submenu = SubmenuBuilder::with_id(app, "menu.edit", "Edit")
.undo()
.redo()
.separator()
.cut()
.copy()
.paste()
.select_all()
.build()
.map_err(|e| e.to_string())?;
let models_submenu = SubmenuBuilder::with_id(app, "menu.models", "Models")
.item(&model_info)
.item(&validate_models)
.build()
.map_err(|e| e.to_string())?;
let inference_submenu = SubmenuBuilder::with_id(app, "menu.inference", "Inference")
.item(&quick_inference)
.item(&batch_inference)
.separator()
.item(&stop_inference)
.build()
.map_err(|e| e.to_string())?;
let view_submenu = SubmenuBuilder::with_id(app, "menu.view", "View")
.item(&view_dashboard)
.item(&view_models)
.item(&view_inference)
.item(&view_metrics)
.separator()
.fullscreen()
.build()
.map_err(|e| e.to_string())?;
let window_submenu = SubmenuBuilder::with_id(app, "menu.window", "Window")
.item(&show_window)
.item(&hide_window)
.separator()
.minimize()
.close_window()
.build()
.map_err(|e| e.to_string())?;
let help_submenu = SubmenuBuilder::with_id(app, "menu.help", "Help")
.item(&documentation)
.item(&shortcuts)
.separator()
.item(&report_issue)
.item(&check_updates)
.build()
.map_err(|e| e.to_string())?;
let menu = MenuBuilder::new(app)
.items(&[
&inferno_submenu,
&file_submenu,
&edit_submenu,
&models_submenu,
&inference_submenu,
&view_submenu,
&window_submenu,
&help_submenu,
])
.build()
.map_err(|e| e.to_string())?;
tracing::info!("✅ macOS application menu created successfully");
Ok(menu)
}
pub fn create_system_tray<R: Runtime>(app: &AppHandle<R>) -> Result<(), String> {
let menu = create_tray_menu(app)?;
let _tray = TrayIconBuilder::with_id("main")
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.show_menu_on_left_click(false)
.tooltip("Inferno AI Runner")
.on_tray_icon_event(|tray, event| {
handle_tray_event(tray.app_handle(), event);
})
.build(app)
.map_err(|e| format!("Failed to create system tray: {}", e))?;
tracing::info!("✅ System tray created successfully");
Ok(())
}
fn create_tray_menu<R: Runtime>(app: &AppHandle<R>) -> Result<Menu<R>, String> {
let dashboard = MenuItem::with_id(app, TRAY_ID_DASHBOARD, "Open Dashboard", true, None::<&str>)
.map_err(|e| e.to_string())?;
let models = MenuItem::with_id(app, TRAY_ID_MODELS, "Manage Models", true, None::<&str>)
.map_err(|e| e.to_string())?;
let inference = MenuItem::with_id(
app,
TRAY_ID_INFERENCE,
"Quick Inference",
true,
None::<&str>,
)
.map_err(|e| e.to_string())?;
let show = MenuItem::with_id(app, TRAY_ID_SHOW, "Show Window", true, None::<&str>)
.map_err(|e| e.to_string())?;
let hide = MenuItem::with_id(app, TRAY_ID_HIDE, "Hide Window", true, None::<&str>)
.map_err(|e| e.to_string())?;
let quit = MenuItem::with_id(app, TRAY_ID_QUIT, "Quit Inferno", true, None::<&str>)
.map_err(|e| e.to_string())?;
MenuBuilder::new(app)
.item(&dashboard)
.separator()
.item(&models)
.item(&inference)
.separator()
.item(&show)
.item(&hide)
.separator()
.item(&quit)
.build()
.map_err(|e| e.to_string())
}
fn handle_tray_event<R: Runtime>(app: &AppHandle<R>, event: TrayIconEvent) {
match event {
TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
TrayIconEvent::Click {
button: MouseButton::Right,
button_state: MouseButtonState::Up,
..
} => {
}
_ => {}
}
}
pub fn handle_tray_menu_event<R: Runtime>(app: &AppHandle<R>, menu_id: &str) {
match menu_id {
TRAY_ID_DASHBOARD | "dashboard" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
let _ = app.emit("navigate", "dashboard");
}
TRAY_ID_MODELS | "models" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
let _ = app.emit("navigate", "models");
tracing::info!("📦 Navigate to models page");
}
TRAY_ID_INFERENCE | "inference" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
let _ = app.emit("open-quick-inference", ());
tracing::info!("⚡ Open quick inference");
}
TRAY_ID_SHOW | "show" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
TRAY_ID_HIDE | "hide" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.hide();
}
}
TRAY_ID_QUIT | "quit" => {
app.exit(0);
}
_ => {
tracing::warn!("⚠️ Unknown tray menu item: {}", menu_id);
}
}
}
#[command]
pub async fn send_native_notification(notification: MacOSNotification) -> Result<(), String> {
tracing::info!(
"📬 Notification queued: {} - {}",
notification.title,
notification.body
);
Ok(())
}
#[cfg(feature = "tauri-plugin-notification")]
pub fn send_notification_with_app<R: Runtime>(
app: &AppHandle<R>,
notification: &MacOSNotification,
) -> Result<(), String> {
use tauri_plugin_notification::NotificationExt;
let mut builder = app.notification().builder();
builder = builder.title(¬ification.title).body(¬ification.body);
if let Some(ref icon) = notification.icon {
builder = builder.icon(icon);
}
builder.show().map_err(|e| e.to_string())?;
tracing::info!(
"📬 Notification sent: {} - {}",
notification.title,
notification.body
);
Ok(())
}
#[cfg(not(feature = "tauri-plugin-notification"))]
pub fn send_notification_with_app<R: Runtime>(
_app: &AppHandle<R>,
notification: &MacOSNotification,
) -> Result<(), String> {
tracing::info!(
"📬 Notification (plugin not available): {} - {}",
notification.title,
notification.body
);
Ok(())
}
#[command]
pub async fn get_system_appearance() -> Result<String, String> {
#[cfg(target_os = "macos")]
{
use std::process::Command;
let output = Command::new("defaults")
.args(["read", "-g", "AppleInterfaceStyle"])
.output();
match output {
Ok(result) if result.status.success() => {
let style = String::from_utf8_lossy(&result.stdout)
.trim()
.to_lowercase();
if style == "dark" {
Ok("dark".to_string())
} else {
Ok("light".to_string())
}
}
_ => Ok("light".to_string()),
}
}
#[cfg(not(target_os = "macos"))]
{
Ok("light".to_string())
}
}
#[command]
pub async fn set_window_vibrancy<R: Runtime>(
_window: Window<R>,
effect: VibrancyEffect,
) -> Result<(), String> {
#[cfg(target_os = "macos")]
{
tracing::info!("🎨 Window vibrancy effect requested: {:?}", effect);
let material_name = match effect {
VibrancyEffect::Sidebar => "sidebar",
VibrancyEffect::HeaderView => "headerView",
VibrancyEffect::Sheet => "sheet",
VibrancyEffect::WindowBackground => "windowBackground",
VibrancyEffect::HudWindow => "hudWindow",
VibrancyEffect::FullScreenUI => "fullScreenUI",
VibrancyEffect::Tooltip => "tooltip",
VibrancyEffect::ContentBackground => "contentBackground",
VibrancyEffect::UnderWindowBackground => "underWindowBackground",
VibrancyEffect::UnderPageBackground => "underPageBackground",
VibrancyEffect::Menu => "menu",
VibrancyEffect::Popover => "popover",
VibrancyEffect::Selection => "selection",
VibrancyEffect::Titlebar => "titlebar",
VibrancyEffect::AppearanceBased => "appearanceBased",
VibrancyEffect::Light => "light",
VibrancyEffect::Dark => "dark",
};
tracing::debug!(
"🎨 Vibrancy effect '{}' maps to NSVisualEffectMaterial.{}",
format!("{:?}", effect),
material_name
);
}
#[cfg(not(target_os = "macos"))]
{
tracing::info!(
"🎨 Window vibrancy not supported on this platform (requested: {:?})",
effect
);
}
Ok(())
}
#[command]
pub async fn toggle_always_on_top<R: Runtime>(window: Window<R>) -> Result<bool, String> {
let is_on_top = window.is_always_on_top().map_err(|e| e.to_string())?;
let new_state = !is_on_top;
window
.set_always_on_top(new_state)
.map_err(|e| e.to_string())?;
tracing::info!("📌 Always on top: {}", new_state);
Ok(new_state)
}
#[command]
pub async fn minimize_to_tray<R: Runtime>(window: Window<R>) -> Result<(), String> {
window.hide().map_err(|e| e.to_string())?;
tracing::info!("🫥 Window minimized to tray");
Ok(())
}
pub fn show_from_tray<R: Runtime>(app: &AppHandle<R>) -> Result<(), String> {
if let Some(window) = app.get_webview_window("main") {
window.show().map_err(|e| e.to_string())?;
window.set_focus().map_err(|e| e.to_string())?;
tracing::info!("👁️ Window shown from tray");
}
Ok(())
}
#[command]
pub async fn detect_metal_gpu() -> Result<MetalInfo, String> {
#[cfg(target_os = "macos")]
{
use std::process::Command;
let output = Command::new("system_profiler")
.arg("SPDisplaysDataType")
.arg("-json")
.output()
.map_err(|e| format!("Failed to run system_profiler: {}", e))?;
if !output.status.success() {
return Err("system_profiler command failed".to_string());
}
let json_str = String::from_utf8_lossy(&output.stdout);
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&json_str) {
if let Some(displays) = json["SPDisplaysDataType"].as_array() {
for display in displays {
if let Some(chipset_model) = display["sppci_model"].as_str() {
let available = true;
let memory_gb = if let Some(vram) = display["sppci_vram"].as_str() {
if vram.contains("GB") {
vram.split_whitespace()
.next()
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(0.0)
} else if vram.contains("MB") {
vram.split_whitespace()
.next()
.and_then(|s| s.parse::<f64>().ok())
.map(|mb| mb / 1024.0)
.unwrap_or(0.0)
} else {
0.0
}
} else {
if chipset_model.contains("Apple") {
if let Ok(sysinfo) = get_total_memory() {
sysinfo / 1024.0 / 1024.0 / 1024.0 } else {
0.0
}
} else {
0.0
}
};
let supports_metal_3 =
chipset_model.contains("Apple M") || chipset_model.contains("AMD");
return Ok(MetalInfo {
available,
device_name: chipset_model.to_string(),
memory_gb,
supports_metal_3,
});
}
}
}
}
Ok(MetalInfo {
available: true,
device_name: "Unknown Metal Device".to_string(),
memory_gb: 0.0,
supports_metal_3: false,
})
}
#[cfg(not(target_os = "macos"))]
{
Ok(MetalInfo {
available: false,
device_name: "Not macOS".to_string(),
memory_gb: 0.0,
supports_metal_3: false,
})
}
}
#[cfg(target_os = "macos")]
fn get_total_memory() -> Result<f64, String> {
use std::process::Command;
let output = Command::new("sysctl")
.arg("-n")
.arg("hw.memsize")
.output()
.map_err(|e| format!("Failed to get memory size: {}", e))?;
let mem_str = String::from_utf8_lossy(&output.stdout);
mem_str
.trim()
.parse::<f64>()
.map_err(|e| format!("Failed to parse memory size: {}", e))
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MetalInfo {
pub available: bool,
pub device_name: String,
pub memory_gb: f64,
pub supports_metal_3: bool,
}
#[command]
pub async fn detect_apple_silicon() -> Result<ChipInfo, String> {
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
{
use std::process::Command;
let brand_output = Command::new("sysctl")
.arg("-n")
.arg("machdep.cpu.brand_string")
.output()
.map_err(|e| format!("Failed to get CPU brand: {}", e))?;
let brand_string = String::from_utf8_lossy(&brand_output.stdout)
.trim()
.to_string();
let chip_name = if brand_string.contains("M1") {
if brand_string.contains("Pro") {
"Apple M1 Pro".to_string()
} else if brand_string.contains("Max") {
"Apple M1 Max".to_string()
} else if brand_string.contains("Ultra") {
"Apple M1 Ultra".to_string()
} else {
"Apple M1".to_string()
}
} else if brand_string.contains("M2") {
if brand_string.contains("Pro") {
"Apple M2 Pro".to_string()
} else if brand_string.contains("Max") {
"Apple M2 Max".to_string()
} else if brand_string.contains("Ultra") {
"Apple M2 Ultra".to_string()
} else {
"Apple M2".to_string()
}
} else if brand_string.contains("M3") {
if brand_string.contains("Pro") {
"Apple M3 Pro".to_string()
} else if brand_string.contains("Max") {
"Apple M3 Max".to_string()
} else {
"Apple M3".to_string()
}
} else if brand_string.contains("M4") {
if brand_string.contains("Pro") {
"Apple M4 Pro".to_string()
} else if brand_string.contains("Max") {
"Apple M4 Max".to_string()
} else {
"Apple M4".to_string()
}
} else {
brand_string.clone()
};
let perf_cores = Command::new("sysctl")
.arg("-n")
.arg("hw.perflevel0.physicalcpu")
.output()
.ok()
.and_then(|out| {
String::from_utf8_lossy(&out.stdout)
.trim()
.parse::<u32>()
.ok()
})
.unwrap_or(0);
let efficiency_cores = Command::new("sysctl")
.arg("-n")
.arg("hw.perflevel1.physicalcpu")
.output()
.ok()
.and_then(|out| {
String::from_utf8_lossy(&out.stdout)
.trim()
.parse::<u32>()
.ok()
})
.unwrap_or(0);
let neural_engine = true;
tracing::info!(
"🍎 Detected: {} (P:{} E:{} ANE:{})",
chip_name,
perf_cores,
efficiency_cores,
neural_engine
);
Ok(ChipInfo {
is_apple_silicon: true,
chip_name,
performance_cores: perf_cores,
efficiency_cores,
neural_engine,
})
}
#[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
{
use std::process::Command;
#[cfg(target_os = "macos")]
let cpu_name = Command::new("sysctl")
.arg("-n")
.arg("machdep.cpu.brand_string")
.output()
.ok()
.map(|out| String::from_utf8_lossy(&out.stdout).trim().to_string())
.unwrap_or_else(|| "Intel x86_64".to_string());
#[cfg(not(target_os = "macos"))]
let cpu_name = "x86_64".to_string();
Ok(ChipInfo {
is_apple_silicon: false,
chip_name: cpu_name,
performance_cores: 0,
efficiency_cores: 0,
neural_engine: false,
})
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ChipInfo {
pub is_apple_silicon: bool,
pub chip_name: String,
pub performance_cores: u32,
pub efficiency_cores: u32,
pub neural_engine: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vibrancy_effect_serialization() {
let effect = VibrancyEffect::Sidebar;
let json = serde_json::to_string(&effect).unwrap();
assert!(json.contains("Sidebar"));
}
#[tokio::test]
async fn test_system_appearance() {
let appearance = get_system_appearance().await;
assert!(appearance.is_ok());
let mode = appearance.unwrap();
assert!(mode == "light" || mode == "dark");
}
}