Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions src-tauri/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@ use hyperdeck_core::{clipsource, stateprobe};
use hyperdeck_os::injector;
use hyperdeck_os::vlchttp::VlcController;

/// Handles the UI needs once the backend is running. `lock_manager`, `selection`,
/// `profile_ids`, and `active` are consumed by the tray profile submenu (added
/// next); `deck` powers Re-home today.
#[allow(dead_code)]
/// Handles the UI needs once the backend is running: `deck` powers Re-home, and
/// `lock_manager` / `selection` / `profile_ids` / `active` drive the tray Profile
/// submenu (pin a profile, persist it, move the checkmark).
pub struct Backend {
pub deck: Arc<VirtualDeck>,
pub lock_manager: Arc<LockManager>,
Expand Down
140 changes: 123 additions & 17 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ mod backend;
use std::sync::Arc;
use std::time::Duration;

use tauri::menu::{Menu, MenuItem};
use tauri::menu::{CheckMenuItem, IsMenuItem, Menu, MenuItem, PredefinedMenuItem, Submenu};
use tauri::tray::TrayIconBuilder;

use hyperdeck_core::domain::LockState;
Expand Down Expand Up @@ -69,17 +69,65 @@ fn run_tray() {
let backend = backend::start(presenter, BIND, POLL)
.map_err(|e| -> Box<dyn std::error::Error> { e.into() })?;

build_tray(app, backend.deck)?;
build_tray(app, backend)?;
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running HyperDeck Adapter");
}

/// Builds the system-tray menu (Re-home, Check for Updates…, Quit).
fn build_tray(app: &tauri::App, deck: Arc<hyperdeck_core::app::VirtualDeck>) -> tauri::Result<()> {
/// 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<()> {
use hyperdeck_core::port::Transport;

let backend::Backend {
deck,
lock_manager,
selection,
profile_ids,
active,
} = backend;

// 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);
let mut profile_items: Vec<CheckMenuItem<tauri::Wry>> =
Vec::with_capacity(profile_ids.len() + 1);
profile_items.push(CheckMenuItem::with_id(
app,
"profile:",
"Auto (match any)",
true,
checked.is_empty(),
None::<&str>,
)?);
for id in &profile_ids {
profile_items.push(CheckMenuItem::with_id(
app,
format!("profile:{id}"),
id,
true,
checked == *id,
None::<&str>,
)?);
}
let item_refs: Vec<&dyn IsMenuItem<tauri::Wry>> = profile_items
.iter()
.map(|i| i as &dyn IsMenuItem<tauri::Wry>)
.collect();
let profile_menu =
Submenu::with_id_and_items(app, "profile_menu", "Profile", true, &item_refs)?;

// (id, item) pairs for moving the checkmark on selection; "" is the Auto entry.
let mut checks: Vec<(String, CheckMenuItem<tauri::Wry>)> =
Vec::with_capacity(profile_items.len());
checks.push((String::new(), profile_items[0].clone()));
for (i, id) in profile_ids.iter().enumerate() {
checks.push((id.clone(), profile_items[i + 1].clone()));
}

let sep = PredefinedMenuItem::separator(app)?;
let rehome = MenuItem::with_id(app, "rehome", "Re-home", true, None::<&str>)?;
let check = MenuItem::with_id(
app,
Expand All @@ -89,32 +137,66 @@ fn build_tray(app: &tauri::App, deck: Arc<hyperdeck_core::app::VirtualDeck>) ->
None::<&str>,
)?;
let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&rehome, &check, &quit])?;
let menu = Menu::with_items(app, &[&profile_menu, &sep, &rehome, &check, &quit])?;

let icon = app.default_window_icon().expect("bundle icon").clone();
TrayIconBuilder::with_id("main")
.icon(icon)
.tooltip("HyperDeck Adapter")
.menu(&menu)
.on_menu_event(move |app, event| match event.id.as_ref() {
"rehome" => {
if let Err(e) = deck.rehome() {
log::warn!("re-home failed: {e}");
}
.on_menu_event(move |app, event| {
let id = event.id.as_ref();
if let Some(profile_id) = id.strip_prefix("profile:") {
select_profile(&lock_manager, &selection, &checks, profile_id);
return;
}
"check_update" => {
let handle = app.clone();
tauri::async_runtime::spawn(async move {
check_for_updates(handle).await;
});
match id {
"rehome" => {
if let Err(e) = deck.rehome() {
log::warn!("re-home failed: {e}");
}
}
"check_update" => {
let handle = app.clone();
tauri::async_runtime::spawn(async move {
check_for_updates(handle).await;
});
}
"quit" => app.exit(0),
_ => {}
}
"quit" => app.exit(0),
_ => {}
})
.build(app)?;
Ok(())
}

/// Pins `id` (empty = Auto): persists the selection, re-points the lock manager
/// (which re-polls immediately), and moves the checkmark to the chosen entry.
fn select_profile(
lock_manager: &hyperdeck_core::app::LockManager,
selection: &hyperdeck_core::config::SelectionStore,
checks: &[(String, CheckMenuItem<tauri::Wry>)],
id: &str,
) {
if let Err(e) = selection.save(id) {
log::warn!("persist profile selection failed: {e}");
}
lock_manager.set_active(id);
for (key, item) in checks {
let _ = item.set_checked(key == id);
}
}

/// Port of the Go `checkedProfile`: the id whose entry shows a checkmark — the
/// active id when it names a known profile, otherwise "" (the Auto entry).
fn checked_profile(profiles: &[String], active: &str) -> String {
if active.is_empty() || !profiles.iter().any(|p| p == active) {
String::new()
} else {
active.to_string()
}
}

/// Checks for, downloads, verifies, and installs an update, then relaunches.
async fn check_for_updates(app: tauri::AppHandle) {
use tauri_plugin_updater::UpdaterExt;
Expand Down Expand Up @@ -168,3 +250,27 @@ fn status_text(lock: &LockState) -> String {
_ => "Disconnected — no player".to_string(),
}
}

#[cfg(test)]
mod tests {
use super::checked_profile;

fn ids() -> Vec<String> {
vec!["vlc".to_string(), "mitti".to_string()]
}

#[test]
fn empty_active_is_auto() {
assert_eq!(checked_profile(&ids(), ""), "");
}

#[test]
fn known_active_is_checked() {
assert_eq!(checked_profile(&ids(), "mitti"), "mitti");
}

#[test]
fn unknown_active_falls_back_to_auto() {
assert_eq!(checked_profile(&ids(), "deleted"), "");
}
}
Loading