Skip to content
Draft
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
1 change: 1 addition & 0 deletions src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"core:window:allow-outer-size",
"core:window:allow-inner-size",
"core:window:allow-scale-factor",
"core:window:allow-start-dragging",
"opener:default",
"store:default",
"aptabase:allow-track-event",
Expand Down
174 changes: 171 additions & 3 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,22 @@ const DAILY_ACTIVE_TRACKED_DAY_KEY: &str = "analytics.daily_active_day";
const DAILY_ACTIVE_EVENT_NAME: &str = "app_started";
const MAX_CONCURRENT_PROBES: usize = 4;

// Mirrors the frontend `hideDockIcon` setting and its default. OpenUsage has
// always run as a menu bar-only app, so the Dock icon stays hidden unless the
// user opts in. Read natively at startup so the policy is applied before the
// Dock would otherwise show an icon.
#[cfg(target_os = "macos")]
const HIDE_DOCK_ICON_STORE_KEY: &str = "hideDockIcon";
#[cfg(target_os = "macos")]
const DEFAULT_HIDE_DOCK_ICON: bool = true;

// Mirrors the frontend `alwaysOnTop` setting. Only meaningful in Dock mode,
// where it keeps the window floating above other windows. Defaults to off.
#[cfg(target_os = "macos")]
const ALWAYS_ON_TOP_STORE_KEY: &str = "alwaysOnTop";
#[cfg(target_os = "macos")]
const DEFAULT_ALWAYS_ON_TOP: bool = false;

fn probe_worker_count(plugin_count: usize) -> usize {
plugin_count.min(MAX_CONCURRENT_PROBES)
}
Expand Down Expand Up @@ -384,6 +400,125 @@ fn get_log_path(app_handle: tauri::AppHandle) -> Result<String, String> {
log_path::for_app(&app_handle).map(|path| path.to_string_lossy().to_string())
}

/// Reads the persisted `hideDockIcon` preference from the settings store.
/// Falls back to the default when the store or key is unavailable so a missing
/// or unreadable setting never changes the long-standing menu bar-only behavior.
#[cfg(target_os = "macos")]
fn get_stored_hide_dock_icon(app_handle: &tauri::AppHandle) -> bool {
use tauri_plugin_store::StoreExt;

let store = match app_handle.store("settings.json") {
Ok(store) => store,
Err(error) => {
log::warn!(
"Failed to access settings store for dock icon visibility: {}",
error
);
return DEFAULT_HIDE_DOCK_ICON;
}
};

store
.get(HIDE_DOCK_ICON_STORE_KEY)
.and_then(|value| value.as_bool())
.unwrap_or(DEFAULT_HIDE_DOCK_ICON)
}

/// Reads the persisted `alwaysOnTop` preference from the settings store.
/// Falls back to the default when the store or key is unavailable.
#[cfg(target_os = "macos")]
fn get_stored_always_on_top(app_handle: &tauri::AppHandle) -> bool {
use tauri_plugin_store::StoreExt;

let store = match app_handle.store("settings.json") {
Ok(store) => store,
Err(error) => {
log::warn!("Failed to access settings store for always on top: {}", error);
return DEFAULT_ALWAYS_ON_TOP;
}
};

store
.get(ALWAYS_ON_TOP_STORE_KEY)
.and_then(|value| value.as_bool())
.unwrap_or(DEFAULT_ALWAYS_ON_TOP)
}

/// Applies Dock icon visibility by switching the macOS activation policy.
/// `Accessory` hides the Dock icon (menu bar-only); `Regular` shows it.
#[cfg(target_os = "macos")]
fn apply_dock_icon_visibility(app_handle: &tauri::AppHandle, hidden: bool) -> Result<(), String> {
let policy = if hidden {
tauri::ActivationPolicy::Accessory
} else {
tauri::ActivationPolicy::Regular
};
app_handle
.set_activation_policy(policy)
.map_err(|e| format!("Failed to set activation policy: {}", e))
}

/// Shows or hides the menu bar (tray) icon. The tray icon and the Dock icon are
/// mutually exclusive (see `update_dock_icon_visibility`), so the tray is shown
/// only when the Dock icon is hidden.
#[cfg(target_os = "macos")]
fn apply_tray_visibility(app_handle: &tauri::AppHandle, visible: bool) -> Result<(), String> {
match app_handle.tray_by_id("tray") {
Some(tray) => tray
.set_visible(visible)
.map_err(|e| format!("Failed to set tray visibility: {}", e)),
None => {
log::warn!("Tray icon 'tray' not found while updating visibility");
Ok(())
}
}
}

/// Switches OpenUsage between menu bar-only and Dock-only presentation.
/// `hidden = true` hides the Dock icon and shows the menu bar (tray) icon;
/// `hidden = false` shows the Dock icon and hides the menu bar icon. The two are
/// mutually exclusive so the menu bar never shows a redundant icon. No-op on
/// other platforms, where there is no Dock concept.
#[tauri::command]
fn update_dock_icon_visibility(
#[allow(unused)] app_handle: tauri::AppHandle,
#[allow(unused)] hidden: bool,
) -> Result<(), String> {
#[cfg(target_os = "macos")]
{
log::info!("Updating dock icon visibility: hidden={}", hidden);
apply_dock_icon_visibility(&app_handle, hidden)?;
// Tray is the inverse of the Dock icon: visible only when Dock is hidden.
apply_tray_visibility(&app_handle, hidden)?;

// In Dock-only mode the panel becomes a normal, draggable window centered
// on screen; in menu-bar mode it returns to the anchored dropdown.
let dock_mode = !hidden;
panel::set_dock_mode(dock_mode);
panel::apply_panel_presentation(&app_handle);
if dock_mode {
panel::position_panel_centered(&app_handle);
}
}
Ok(())
}

/// Toggles whether the Dock-mode window floats above other windows. Only has a
/// visible effect in Dock mode; the panel level is updated immediately.
#[tauri::command]
fn update_always_on_top(
#[allow(unused)] app_handle: tauri::AppHandle,
#[allow(unused)] enabled: bool,
) -> Result<(), String> {
#[cfg(target_os = "macos")]
{
log::info!("Updating always on top: enabled={}", enabled);
panel::set_always_on_top(enabled);
panel::apply_panel_presentation(&app_handle);
}
Ok(())
}

/// Update the global shortcut registration.
/// Pass `null` to disable the shortcut, or a shortcut string like "CommandOrControl+Shift+U".
#[cfg(desktop)]
Expand Down Expand Up @@ -530,11 +665,31 @@ pub fn run() {
start_probe_batch,
list_plugins,
get_log_path,
update_global_shortcut
update_global_shortcut,
update_dock_icon_visibility,
update_always_on_top
])
.setup(|app| {
// Apply the persisted Dock icon preference before anything else so the
// Dock never briefly shows an icon on launch. The app shows EITHER the
// menu bar (tray) icon OR the Dock icon, never both, to save menu bar
// space. Defaults to hidden Dock (menu bar-only). Tray visibility is
// applied below, right after the tray is created.
#[cfg(target_os = "macos")]
let hide_dock_icon = get_stored_hide_dock_icon(app.handle());
#[cfg(target_os = "macos")]
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
{
let policy = if hide_dock_icon {
tauri::ActivationPolicy::Accessory
} else {
tauri::ActivationPolicy::Regular
};
app.set_activation_policy(policy);
// Dock-only mode (Dock icon shown) makes the panel a normal,
// persistent window; record it so the panel behaves accordingly.
panel::set_dock_mode(!hide_dock_icon);
panel::set_always_on_top(get_stored_always_on_top(app.handle()));
}

#[cfg(target_os = "macos")]
{
Expand Down Expand Up @@ -582,6 +737,13 @@ pub fn run() {

tray::create(app.handle())?;

// Show the tray icon only when the Dock icon is hidden, so the user
// gets the menu bar icon OR the Dock icon, never both.
#[cfg(target_os = "macos")]
if let Err(error) = apply_tray_visibility(app.handle(), hide_dock_icon) {
log::warn!("Failed to set initial tray visibility: {}", error);
}

app.handle()
.plugin(tauri_plugin_updater::Builder::new().build())?;

Expand Down Expand Up @@ -621,10 +783,16 @@ pub fn run() {
})
.build(tauri::generate_context!())
.expect("error while building tauri application")
.run(|_, event| match event {
.run(|_app_handle, event| match event {
tauri::RunEvent::ExitRequested { .. } | tauri::RunEvent::Exit => {
local_http_api::flush_cache();
}
// In Dock-only mode there is no tray icon to click, so clicking the
// Dock icon (which triggers Reopen) is how the user opens the panel.
#[cfg(target_os = "macos")]
tauri::RunEvent::Reopen { .. } => {
panel::show_panel_dock(_app_handle);
}
_ => {}
});
}
Expand Down
105 changes: 105 additions & 0 deletions src-tauri/src/panel.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,42 @@
use std::sync::atomic::{AtomicBool, Ordering};

use tauri::{AppHandle, Manager, Position, Size};
use tauri_nspanel::{
CollectionBehavior, ManagerExt, PanelLevel, StyleMask, WebviewWindowExt, tauri_panel,
};

/// True while the app runs as a Dock-only window (no tray icon). In this mode
/// the panel behaves like a normal window: it does not auto-hide on blur and is
/// centered instead of anchored under the (absent) tray icon.
static DOCK_MODE: AtomicBool = AtomicBool::new(false);

/// When in Dock mode, keep the window floating above other windows.
static ALWAYS_ON_TOP: AtomicBool = AtomicBool::new(false);

/// Tracks whether the Dock-only window has been positioned at least once this
/// session. We center it once per launch (the first time it shows), then leave
/// it wherever the user dragged it. Leaving Dock mode resets this.
static DOCK_POSITIONED: AtomicBool = AtomicBool::new(false);

pub fn set_dock_mode(enabled: bool) {
DOCK_MODE.store(enabled, Ordering::SeqCst);
if !enabled {
DOCK_POSITIONED.store(false, Ordering::SeqCst);
}
}

pub fn set_always_on_top(enabled: bool) {
ALWAYS_ON_TOP.store(enabled, Ordering::SeqCst);
}

fn is_dock_mode() -> bool {
DOCK_MODE.load(Ordering::SeqCst)
}

fn is_dock_positioned() -> bool {
DOCK_POSITIONED.load(Ordering::SeqCst)
}

fn monitor_contains_physical_point(
origin_x: f64,
origin_y: f64,
Expand Down Expand Up @@ -116,6 +150,72 @@ pub fn show_panel(app_handle: &AppHandle) {
}
}

/// Show the panel in Dock-only mode, where there is no tray icon to anchor to.
/// It is centered the first time it shows each launch, then left wherever the
/// user dragged it for the rest of the session.
pub fn show_panel_dock(app_handle: &AppHandle) {
if let Some(panel) = get_or_init_panel!(app_handle) {
panel.show_and_make_key();
apply_panel_presentation(app_handle);
if !is_dock_positioned() {
position_panel_centered(app_handle);
}
}
}

/// Set the panel window level for the current mode. Menu-bar mode floats above
/// everything as a dropdown. Dock mode uses a normal window level, unless
/// "always on top" is enabled, in which case it floats above other windows.
pub fn apply_panel_presentation(app_handle: &AppHandle) {
let Ok(panel) = app_handle.get_webview_panel("main") else {
return;
};
let level = if is_dock_mode() {
if ALWAYS_ON_TOP.load(Ordering::SeqCst) {
PanelLevel::Floating.value()
} else {
PanelLevel::Normal.value()
}
} else {
PanelLevel::MainMenu.value() + 1
};
panel.set_level(level);
}

/// Center the window on the primary monitor. Dock mode places the window here
/// on launch (and when first switching to Dock mode); the user can drag it
/// afterwards. Marks the window as positioned for the session.
pub fn position_panel_centered(app_handle: &AppHandle) {
let Some(window) = app_handle.get_webview_window("main") else {
return;
};

let monitor = match window.primary_monitor() {
Ok(Some(monitor)) => monitor,
_ => return,
};

let scale = monitor.scale_factor();
let mon_logical_x = monitor.position().x as f64 / scale;
let mon_logical_y = monitor.position().y as f64 / scale;
let mon_logical_w = monitor.size().width as f64 / scale;
let mon_logical_h = monitor.size().height as f64 / scale;

let (panel_w, panel_h) = match (window.outer_size(), window.scale_factor()) {
(Ok(size), Ok(win_scale)) => (
size.width as f64 / win_scale,
size.height as f64 / win_scale,
),
_ => (400.0, 500.0),
};

let panel_x = mon_logical_x + (mon_logical_w - panel_w) / 2.0;
let panel_y = mon_logical_y + (mon_logical_h - panel_h) / 2.0;

set_panel_top_left_immediately(&window, app_handle, panel_x, panel_y, mon_logical_h);
DOCK_POSITIONED.store(true, Ordering::SeqCst);
}

/// Toggle panel visibility. If visible, hide it. If hidden, show it.
/// Used by global shortcut handler.
pub fn toggle_panel(app_handle: &AppHandle) {
Expand Down Expand Up @@ -178,6 +278,11 @@ pub fn init(app_handle: &tauri::AppHandle) -> tauri::Result<()> {

let handle = app_handle.clone();
event_handler.window_did_resign_key(move |_notification| {
// In Dock-only mode the panel is a normal, persistent window, so it
// must stay open when it loses focus instead of auto-hiding.
if is_dock_mode() {
return;
}
if let Ok(panel) = handle.get_webview_panel("main") {
panel.hide();
}
Expand Down
Loading
Loading