From a58e2cbcef580e124f79ae8f4b0957a8224ffc73 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 15:42:45 +0000 Subject: [PATCH] feat(tauri): add the tray status line and lock indicator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete tray parity with the Go app's status display: - Add a disabled status line at the top of the tray menu ("Locked: ()" / "Disconnected — no player"); the presenter updates its text from the poll thread via a shared handle populated when the tray is built. - The TrayPresenter now also sets a menu-bar lock indicator (HD● / HD○, rendered on macOS where tray titles show) alongside the existing tooltip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R8X1NxFiaLGJzwUD5wgRTv --- src-tauri/src/main.rs | 58 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index c5016ce..38fe619 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -3,7 +3,7 @@ mod backend; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use std::time::Duration; use tauri::menu::{CheckMenuItem, IsMenuItem, Menu, MenuItem, PredefinedMenuItem, Submenu}; @@ -63,22 +63,34 @@ fn run_tray() { #[cfg(target_os = "macos")] app.set_activation_policy(tauri::ActivationPolicy::Accessory); + // The disabled status line lives in the tray menu (built below); the + // presenter updates it, so share a slot it can reach once it exists. + let status_item: StatusSlot = Arc::new(Mutex::new(None)); let presenter: Arc<dyn StatusPresenter + Send + Sync> = Arc::new(TrayPresenter { app: app.handle().clone(), + status_item: status_item.clone(), }); let backend = backend::start(presenter, BIND, POLL) .map_err(|e| -> Box<dyn std::error::Error> { e.into() })?; - build_tray(app, backend)?; + build_tray(app, backend, status_item)?; Ok(()) }) .run(tauri::generate_context!()) .expect("error while running HyperDeck Adapter"); } -/// Builds the system-tray menu: a Profile submenu (Auto + one entry per profile), -/// Re-home, Check for Updates…, and Quit. -fn build_tray(app: &tauri::App, backend: backend::Backend) -> tauri::Result<()> { +/// A shared handle to the disabled status menu line, populated once the tray is +/// built so the [`TrayPresenter`] can update it from the poll thread. +type StatusSlot = Arc<Mutex<Option<MenuItem<tauri::Wry>>>>; + +/// Builds the system-tray menu: a disabled status line, a Profile submenu (Auto + +/// one entry per profile), Re-home, Check for Updates…, and Quit. +fn build_tray( + app: &tauri::App, + backend: backend::Backend, + status_item: StatusSlot, +) -> tauri::Result<()> { use hyperdeck_core::port::Transport; let backend::Backend { @@ -89,6 +101,17 @@ fn build_tray(app: &tauri::App, backend: backend::Backend) -> tauri::Result<()> active, } = backend; + // Disabled line at the top showing the current player lock; the presenter + // updates its text as the lock changes. + let status = MenuItem::with_id( + app, + "status", + status_text(&LockState::default()), + false, + None::<&str>, + )?; + *status_item.lock().unwrap() = Some(status.clone()); + // Profile submenu: "Auto (match any)" plus one checkbox per profile id, with // the checkmark on the persisted selection (Auto when none / unknown). let checked = checked_profile(&profile_ids, &active); @@ -127,7 +150,8 @@ fn build_tray(app: &tauri::App, backend: backend::Backend) -> tauri::Result<()> checks.push((id.clone(), profile_items[i + 1].clone())); } - let sep = PredefinedMenuItem::separator(app)?; + let sep1 = PredefinedMenuItem::separator(app)?; + let sep2 = PredefinedMenuItem::separator(app)?; let rehome = MenuItem::with_id(app, "rehome", "Re-home", true, None::<&str>)?; let check = MenuItem::with_id( app, @@ -137,7 +161,10 @@ fn build_tray(app: &tauri::App, backend: backend::Backend) -> tauri::Result<()> None::<&str>, )?; let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; - let menu = Menu::with_items(app, &[&profile_menu, &sep, &rehome, &check, &quit])?; + let menu = Menu::with_items( + app, + &[&status, &sep1, &profile_menu, &sep2, &rehome, &check, &quit], + )?; let icon = app.default_window_icon().expect("bundle icon").clone(); TrayIconBuilder::with_id("main") @@ -221,20 +248,35 @@ async fn check_for_updates(app: tauri::AppHandle) { } } -/// Reflects lock status in the tray tooltip. +/// Reflects lock status in the tray: the disabled status menu line, the tooltip, +/// and (on macOS, where tray titles render) a lock indicator. struct TrayPresenter { app: tauri::AppHandle, + status_item: StatusSlot, } impl StatusPresenter for TrayPresenter { fn present(&self, lock: &LockState) { let text = status_text(lock); + if let Some(item) = self.status_item.lock().unwrap().as_ref() { + let _ = item.set_text(&text); + } if let Some(tray) = self.app.tray_by_id("main") { let _ = tray.set_tooltip(Some(&text)); + let _ = tray.set_title(Some(lock_indicator(lock.locked))); } } } +/// The menu-bar lock indicator (rendered on macOS): filled when locked. +fn lock_indicator(locked: bool) -> &'static str { + if locked { + "HD●" + } else { + "HD○" + } +} + /// Logs lock status (headless mode). struct LogPresenter;