From 8e52a0947a99c1d226a12287929027337d2eedbb Mon Sep 17 00:00:00 2001 From: Native Muttaqien <83034829+samlehoy@users.noreply.github.com> Date: Sat, 30 May 2026 14:01:03 +0700 Subject: [PATCH 01/22] feat: Add Windows support --- .github/workflows/publish.yml | 5 +- src-tauri/Cargo.toml | 3 +- src-tauri/src/lib.rs | 17 +- src-tauri/src/panel.rs | 582 +++++++++++++++++++++------------- 4 files changed, 371 insertions(+), 236 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c15fd3dd..33bbcc71 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -19,6 +19,8 @@ jobs: args: "--target aarch64-apple-darwin" - platform: macos-latest args: "--target x86_64-apple-darwin" + - platform: windows-latest + args: "--target x86_64-pc-windows-msvc" runs-on: ${{ matrix.platform }} env: RELEASE_TAG: ${{ github.ref_name }} @@ -29,7 +31,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: - targets: aarch64-apple-darwin,x86_64-apple-darwin + targets: aarch64-apple-darwin,x86_64-apple-darwin,x86_64-pc-windows-msvc - uses: swatinem/rust-cache@v2 with: workspaces: "./src-tauri -> target" @@ -81,6 +83,7 @@ jobs: fi - name: Import Apple Developer Certificate + if: matrix.platform == 'macos-latest' env: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 902e6516..6ba50359 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -22,7 +22,7 @@ tauri = { version = "2", features = ["macos-private-api", "tray-icon", "image-pn tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" -tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2.1" } + time = { version = "0.3.47", features = ["formatting"] } dirs = "6" log = "0.4" @@ -44,6 +44,7 @@ aes-gcm = "0.10.3" sha2 = "0.11" [target.'cfg(target_os = "macos")'.dependencies] +tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2.1" } objc2 = "0.6" objc2-foundation = { version = "0.3", features = ["NSProcessInfo", "NSString"] } objc2-app-kit = { version = "0.3", features = ["NSEvent", "NSScreen", "NSGraphics"] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 702aa988..3bb08bc1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -200,10 +200,7 @@ fn init_panel(app_handle: tauri::AppHandle) { #[tauri::command] fn hide_panel(app_handle: tauri::AppHandle) { - use tauri_nspanel::ManagerExt; - if let Ok(panel) = app_handle.get_webview_panel("main") { - panel.hide(); - } + panel::hide_panel(&app_handle); } #[tauri::command] @@ -504,11 +501,17 @@ pub fn run() { let runtime = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime"); let _guard = runtime.enter(); - tauri::Builder::default() + let mut builder = tauri::Builder::default() .plugin(tauri_plugin_aptabase::Builder::new("A-US-6435241436").build()) .plugin(tauri_plugin_opener::init()) - .plugin(tauri_plugin_store::Builder::default().build()) - .plugin(tauri_nspanel::init()) + .plugin(tauri_plugin_store::Builder::default().build()); + + #[cfg(target_os = "macos")] + { + builder = builder.plugin(tauri_nspanel::init()); + } + + builder .plugin( tauri_plugin_log::Builder::new() .targets([ diff --git a/src-tauri/src/panel.rs b/src-tauri/src/panel.rs index ce440aee..d33e38f9 100644 --- a/src-tauri/src/panel.rs +++ b/src-tauri/src/panel.rs @@ -1,278 +1,406 @@ -use tauri::{AppHandle, Manager, Position, Size}; -use tauri_nspanel::{ - CollectionBehavior, ManagerExt, PanelLevel, StyleMask, WebviewWindowExt, tauri_panel, -}; - -fn monitor_contains_physical_point( - origin_x: f64, - origin_y: f64, - width: f64, - height: f64, - point_x: f64, - point_y: f64, -) -> bool { - point_x >= origin_x - && point_x < origin_x + width - && point_y >= origin_y - && point_y < origin_y + height -} +#[cfg(target_os = "macos")] +pub use macos::*; -unsafe fn set_panel_frame_top_left(panel: &tauri_nspanel::NSPanel, x: f64, y: f64) { - let point = tauri_nspanel::NSPoint::new(x, y); - let _: () = objc2::msg_send![panel, setFrameTopLeftPoint: point]; -} +#[cfg(not(target_os = "macos"))] +pub use windows::*; -fn set_panel_top_left_immediately( - window: &tauri::WebviewWindow, - app_handle: &AppHandle, - panel_x: f64, - panel_y: f64, - primary_logical_h: f64, -) { - let Ok(panel_handle) = app_handle.get_webview_panel("main") else { - return; +#[cfg(target_os = "macos")] +mod macos { + use tauri::{AppHandle, Manager, Position, Size}; + use tauri_nspanel::{ + CollectionBehavior, ManagerExt, PanelLevel, StyleMask, WebviewWindowExt, tauri_panel, }; - let target_x = panel_x; - let target_y = primary_logical_h - panel_y; + fn monitor_contains_physical_point( + origin_x: f64, + origin_y: f64, + width: f64, + height: f64, + point_x: f64, + point_y: f64, + ) -> bool { + point_x >= origin_x + && point_x < origin_x + width + && point_y >= origin_y + && point_y < origin_y + height + } - if objc2_foundation::MainThreadMarker::new().is_some() { - unsafe { - set_panel_frame_top_left(panel_handle.as_panel(), target_x, target_y); - } - return; + unsafe fn set_panel_frame_top_left(panel: &tauri_nspanel::NSPanel, x: f64, y: f64) { + let point = tauri_nspanel::NSPoint::new(x, y); + let _: () = objc2::msg_send![panel, setFrameTopLeftPoint: point]; } - let (tx, rx) = std::sync::mpsc::channel(); - let panel_handle = panel_handle.clone(); + fn set_panel_top_left_immediately( + window: &tauri::WebviewWindow, + app_handle: &AppHandle, + panel_x: f64, + panel_y: f64, + primary_logical_h: f64, + ) { + let Ok(panel_handle) = app_handle.get_webview_panel("main") else { + return; + }; + + let target_x = panel_x; + let target_y = primary_logical_h - panel_y; + + if objc2_foundation::MainThreadMarker::new().is_some() { + unsafe { + set_panel_frame_top_left(panel_handle.as_panel(), target_x, target_y); + } + return; + } + + let (tx, rx) = std::sync::mpsc::channel(); + let panel_handle = panel_handle.clone(); - if let Err(error) = window.run_on_main_thread(move || { - unsafe { - set_panel_frame_top_left(panel_handle.as_panel(), target_x, target_y); + if let Err(error) = window.run_on_main_thread(move || { + unsafe { + set_panel_frame_top_left(panel_handle.as_panel(), target_x, target_y); + } + let _ = tx.send(()); + }) { + log::warn!("Failed to position panel on main thread: {}", error); + return; } - let _ = tx.send(()); - }) { - log::warn!("Failed to position panel on main thread: {}", error); - return; - } - if rx.recv().is_err() { - log::warn!("Failed waiting for panel position on main thread"); + if rx.recv().is_err() { + log::warn!("Failed waiting for panel position on main thread"); + } } -} -/// Macro to get existing panel or initialize it if needed. -/// Returns Option - Some if panel is available, None on error. -macro_rules! get_or_init_panel { - ($app_handle:expr) => { - match $app_handle.get_webview_panel("main") { - Ok(panel) => Some(panel), - Err(_) => { - if let Err(err) = crate::panel::init($app_handle) { - log::error!("Failed to init panel: {}", err); - None - } else { - match $app_handle.get_webview_panel("main") { - Ok(panel) => Some(panel), - Err(err) => { - log::error!("Panel missing after init: {:?}", err); - None + macro_rules! get_or_init_panel { + ($app_handle:expr) => { + match $app_handle.get_webview_panel("main") { + Ok(panel) => Some(panel), + Err(_) => { + if let Err(err) = crate::panel::init($app_handle) { + log::error!("Failed to init panel: {}", err); + None + } else { + match $app_handle.get_webview_panel("main") { + Ok(panel) => Some(panel), + Err(err) => { + log::error!("Panel missing after init: {:?}", err); + None + } } } } } - } - }; -} + }; + } -// Export macro for use in other modules -pub(crate) use get_or_init_panel; + pub(crate) use get_or_init_panel; -/// Retrieve the tray icon rect and position the panel beneath it. -/// No-ops gracefully if the tray icon or its rect is unavailable. -fn position_panel_from_tray(app_handle: &AppHandle) { - let Some(tray) = app_handle.tray_by_id("tray") else { - log::debug!("position_panel_from_tray: tray icon not found"); - return; - }; - match tray.rect() { - Ok(Some(rect)) => { - position_panel_at_tray_icon(app_handle, rect.position, rect.size); + fn position_panel_from_tray(app_handle: &AppHandle) { + let Some(tray) = app_handle.tray_by_id("tray") else { + log::debug!("position_panel_from_tray: tray icon not found"); + return; + }; + match tray.rect() { + Ok(Some(rect)) => { + position_panel_at_tray_icon(app_handle, rect.position, rect.size); + } + Ok(None) => { + log::debug!("position_panel_from_tray: tray rect not available yet"); + } + Err(e) => { + log::warn!("position_panel_from_tray: failed to get tray rect: {}", e); + } } - Ok(None) => { - log::debug!("position_panel_from_tray: tray rect not available yet"); + } + + pub fn show_panel(app_handle: &AppHandle) { + if let Some(panel) = get_or_init_panel!(app_handle) { + panel.show_and_make_key(); + position_panel_from_tray(app_handle); } - Err(e) => { - log::warn!("position_panel_from_tray: failed to get tray rect: {}", e); + } + + pub fn hide_panel(app_handle: &AppHandle) { + if let Ok(panel) = app_handle.get_webview_panel("main") { + panel.hide(); } } -} -/// Show the panel (initializing if needed), positioned under the tray icon. -pub fn show_panel(app_handle: &AppHandle) { - if let Some(panel) = get_or_init_panel!(app_handle) { - panel.show_and_make_key(); - position_panel_from_tray(app_handle); + pub fn toggle_panel(app_handle: &AppHandle) { + let Some(panel) = get_or_init_panel!(app_handle) else { + return; + }; + + if panel.is_visible() { + log::debug!("toggle_panel: hiding panel"); + panel.hide(); + } else { + log::debug!("toggle_panel: showing panel"); + panel.show_and_make_key(); + position_panel_from_tray(app_handle); + } } -} -/// Toggle panel visibility. If visible, hide it. If hidden, show it. -/// Used by global shortcut handler. -pub fn toggle_panel(app_handle: &AppHandle) { - let Some(panel) = get_or_init_panel!(app_handle) else { - return; - }; + tauri_panel! { + panel!(OpenUsagePanel { + config: { + can_become_key_window: true, + is_floating_panel: true + } + }) - if panel.is_visible() { - log::debug!("toggle_panel: hiding panel"); - panel.hide(); - } else { - log::debug!("toggle_panel: showing panel"); - panel.show_and_make_key(); - position_panel_from_tray(app_handle); + panel_event!(OpenUsagePanelEventHandler { + window_did_resign_key(notification: &NSNotification) -> () + }) } -} -// Define our panel class and event handler together -tauri_panel! { - panel!(OpenUsagePanel { - config: { - can_become_key_window: true, - is_floating_panel: true + pub fn init(app_handle: &tauri::AppHandle) -> tauri::Result<()> { + if app_handle.get_webview_panel("main").is_ok() { + return Ok(()); } - }) - panel_event!(OpenUsagePanelEventHandler { - window_did_resign_key(notification: &NSNotification) -> () - }) -} - -pub fn init(app_handle: &tauri::AppHandle) -> tauri::Result<()> { - if app_handle.get_webview_panel("main").is_ok() { - return Ok(()); - } + let window = app_handle.get_webview_window("main").unwrap(); - let window = app_handle.get_webview_window("main").unwrap(); + let panel = window.to_panel::()?; - let panel = window.to_panel::()?; + panel.set_has_shadow(false); + panel.set_opaque(false); + panel.set_level(PanelLevel::MainMenu.value() + 1); - // Disable native shadow - it causes gray border on transparent windows - // Let CSS handle shadow via shadow-xl class - panel.set_has_shadow(false); - panel.set_opaque(false); + panel.set_collection_behavior( + CollectionBehavior::new() + .move_to_active_space() + .full_screen_auxiliary() + .value(), + ); - // Configure panel behavior - panel.set_level(PanelLevel::MainMenu.value() + 1); + panel.set_style_mask(StyleMask::empty().nonactivating_panel().value()); - panel.set_collection_behavior( - CollectionBehavior::new() - .move_to_active_space() - .full_screen_auxiliary() - .value(), - ); + let event_handler = OpenUsagePanelEventHandler::new(); + let handle = app_handle.clone(); + event_handler.window_did_resign_key(move |_notification| { + if let Ok(panel) = handle.get_webview_panel("main") { + panel.hide(); + } + }); - panel.set_style_mask(StyleMask::empty().nonactivating_panel().value()); + panel.set_event_handler(Some(event_handler.as_ref())); - // Set up event handler to hide panel when it loses focus - let event_handler = OpenUsagePanelEventHandler::new(); + Ok(()) + } - let handle = app_handle.clone(); - event_handler.window_did_resign_key(move |_notification| { - if let Ok(panel) = handle.get_webview_panel("main") { - panel.hide(); - } - }); + pub fn position_panel_at_tray_icon( + app_handle: &tauri::AppHandle, + icon_position: Position, + icon_size: Size, + ) { + let window = app_handle.get_webview_window("main").unwrap(); + + let (icon_phys_x, icon_phys_y) = match &icon_position { + Position::Physical(pos) => (pos.x as f64, pos.y as f64), + Position::Logical(pos) => (pos.x, pos.y), + }; + let (icon_phys_w, icon_phys_h) = match &icon_size { + Size::Physical(s) => (s.width as f64, s.height as f64), + Size::Logical(s) => (s.width, s.height), + }; + + let monitors = window.available_monitors().expect("failed to get monitors"); + let primary_logical_h = window + .primary_monitor() + .ok() + .flatten() + .map(|m| m.size().height as f64 / m.scale_factor()) + .unwrap_or(0.0); + + let icon_center_x = icon_phys_x + (icon_phys_w / 2.0); + let icon_center_y = icon_phys_y + (icon_phys_h / 2.0); + + let found_monitor = monitors.iter().find(|monitor| { + let origin = monitor.position(); + let size = monitor.size(); + monitor_contains_physical_point( + origin.x as f64, + origin.y as f64, + size.width as f64, + size.height as f64, + icon_center_x, + icon_center_y, + ) + }); + + let monitor = match found_monitor { + Some(m) => m.clone(), + None => { + log::warn!( + "No monitor found for tray rect center at ({:.0}, {:.0}), using primary", + icon_center_x, + icon_center_y + ); + match window.primary_monitor() { + Ok(Some(m)) => m, + _ => return, + } + } + }; + + let target_scale = monitor.scale_factor(); + let mon_phys_x = monitor.position().x as f64; + let mon_phys_y = monitor.position().y as f64; + let mon_logical_x = mon_phys_x / target_scale; + let mon_logical_y = mon_phys_y / target_scale; + + let icon_logical_x = mon_logical_x + (icon_phys_x - mon_phys_x) / target_scale; + let icon_logical_y = mon_logical_y + (icon_phys_y - mon_phys_y) / target_scale; + let icon_logical_w = icon_phys_w / target_scale; + let icon_logical_h = icon_phys_h / target_scale; + + let panel_width = match (window.outer_size(), window.scale_factor()) { + (Ok(s), Ok(win_scale)) => s.width as f64 / win_scale, + _ => { + let conf: serde_json::Value = serde_json::from_str(include_str!("../tauri.conf.json")) + .expect("tauri.conf.json must be valid JSON"); + conf["app"]["windows"][0]["width"] + .as_f64() + .expect("width must be set in tauri.conf.json") + } + }; - panel.set_event_handler(Some(event_handler.as_ref())); + let icon_center_x = icon_logical_x + (icon_logical_w / 2.0); + let panel_x = icon_center_x - (panel_width / 2.0); + let nudge_up: f64 = 6.0; + let panel_y = icon_logical_y + icon_logical_h - nudge_up; - Ok(()) + set_panel_top_left_immediately(&window, app_handle, panel_x, panel_y, primary_logical_h); + } } -pub fn position_panel_at_tray_icon( - app_handle: &tauri::AppHandle, - icon_position: Position, - icon_size: Size, -) { - let window = app_handle.get_webview_window("main").unwrap(); +#[cfg(not(target_os = "macos"))] +mod windows { + use tauri::{AppHandle, Manager, Position, Size}; - let (icon_phys_x, icon_phys_y) = match &icon_position { - Position::Physical(pos) => (pos.x as f64, pos.y as f64), - Position::Logical(pos) => (pos.x, pos.y), - }; - let (icon_phys_w, icon_phys_h) = match &icon_size { - Size::Physical(s) => (s.width as f64, s.height as f64), - Size::Logical(s) => (s.width, s.height), - }; + macro_rules! get_window { + ($app_handle:expr) => { + $app_handle.get_webview_window("main").ok() + }; + } - let monitors = window.available_monitors().expect("failed to get monitors"); - let primary_logical_h = window - .primary_monitor() - .ok() - .flatten() - .map(|m| m.size().height as f64 / m.scale_factor()) - .unwrap_or(0.0); - - let icon_center_x = icon_phys_x + (icon_phys_w / 2.0); - let icon_center_y = icon_phys_y + (icon_phys_h / 2.0); - - let found_monitor = monitors.iter().find(|monitor| { - let origin = monitor.position(); - let size = monitor.size(); - monitor_contains_physical_point( - origin.x as f64, - origin.y as f64, - size.width as f64, - size.height as f64, - icon_center_x, - icon_center_y, - ) - }); - - let monitor = match found_monitor { - Some(m) => m.clone(), - None => { - log::warn!( - "No monitor found for tray rect center at ({:.0}, {:.0}), using primary", - icon_center_x, - icon_center_y - ); - match window.primary_monitor() { - Ok(Some(m)) => m, - _ => return, + pub fn show_panel(app_handle: &AppHandle) { + if let Some(window) = get_window!(app_handle) { + let _ = window.show(); + let _ = window.set_focus(); + position_panel_from_tray(app_handle); + } + } + + pub fn hide_panel(app_handle: &AppHandle) { + if let Some(window) = get_window!(app_handle) { + let _ = window.hide(); + } + } + + pub fn toggle_panel(app_handle: &AppHandle) { + let Some(window) = get_window!(app_handle) else { + return; + }; + + if window.is_visible().unwrap_or(false) { + log::debug!("toggle_panel: hiding window"); + let _ = window.hide(); + } else { + log::debug!("toggle_panel: showing window"); + let _ = window.show(); + let _ = window.set_focus(); + position_panel_from_tray(app_handle); + } + } + + pub fn init(app_handle: &tauri::AppHandle) -> tauri::Result<()> { + if let Some(window) = get_window!(app_handle) { + let _ = window.set_decorations(false); + let _ = window.set_always_on_top(true); + let _ = window.set_skip_taskbar(true); + + // Handle window blur to hide it + let handle = app_handle.clone(); + window.on_window_event(move |event| { + if let tauri::WindowEvent::Focused(false) = event { + if let Some(w) = get_window!(handle) { + let _ = w.hide(); + } + } + }); + } + Ok(()) + } + + fn position_panel_from_tray(app_handle: &AppHandle) { + let Some(tray) = app_handle.tray_by_id("tray") else { + return; + }; + match tray.rect() { + Ok(Some(rect)) => { + position_panel_at_tray_icon(app_handle, rect.position, rect.size); } + _ => {} } - }; + } - let target_scale = monitor.scale_factor(); - let mon_phys_x = monitor.position().x as f64; - let mon_phys_y = monitor.position().y as f64; - let mon_logical_x = mon_phys_x / target_scale; - let mon_logical_y = mon_phys_y / target_scale; - - let icon_logical_x = mon_logical_x + (icon_phys_x - mon_phys_x) / target_scale; - let icon_logical_y = mon_logical_y + (icon_phys_y - mon_phys_y) / target_scale; - let icon_logical_w = icon_phys_w / target_scale; - let icon_logical_h = icon_phys_h / target_scale; - - // Read panel width from the window, converted to logical points. - // outer_size() returns physical pixels at the window's current scale factor. - // If the window isn't available yet, parse the configured width from tauri.conf.json - // (embedded at compile time) so it stays in sync automatically. - let panel_width = match (window.outer_size(), window.scale_factor()) { - (Ok(s), Ok(win_scale)) => s.width as f64 / win_scale, - _ => { - let conf: serde_json::Value = serde_json::from_str(include_str!("../tauri.conf.json")) - .expect("tauri.conf.json must be valid JSON"); - conf["app"]["windows"][0]["width"] - .as_f64() - .expect("width must be set in tauri.conf.json") + pub fn position_panel_at_tray_icon( + app_handle: &tauri::AppHandle, + icon_position: Position, + icon_size: Size, + ) { + let window = app_handle.get_webview_window("main").unwrap(); + + let (icon_phys_x, icon_phys_y) = match &icon_position { + Position::Physical(pos) => (pos.x as f64, pos.y as f64), + Position::Logical(pos) => (pos.x, pos.y), + }; + let (icon_phys_w, icon_phys_h) = match &icon_size { + Size::Physical(s) => (s.width as f64, s.height as f64), + Size::Logical(s) => (s.width, s.height), + }; + + let outer_size = window.outer_size().unwrap_or(tauri::PhysicalSize::new(400, 500)); + let window_phys_w = outer_size.width as f64; + let window_phys_h = outer_size.height as f64; + + let monitors = window.available_monitors().unwrap_or_default(); + let icon_center_x = icon_phys_x + (icon_phys_w / 2.0); + let icon_center_y = icon_phys_y + (icon_phys_h / 2.0); + + let mut monitor_bounds = None; + for monitor in monitors { + let pos = monitor.position(); + let size = monitor.size(); + if icon_center_x >= pos.x as f64 && icon_center_x <= (pos.x + size.width as i32) as f64 + && icon_center_y >= pos.y as f64 && icon_center_y <= (pos.y + size.height as i32) as f64 { + monitor_bounds = Some((pos, size)); + break; + } } - }; - let icon_center_x = icon_logical_x + (icon_logical_w / 2.0); - let panel_x = icon_center_x - (panel_width / 2.0); - let nudge_up: f64 = 6.0; - let panel_y = icon_logical_y + icon_logical_h - nudge_up; + let gap = 10.0; + let mut target_x = icon_center_x - (window_phys_w / 2.0); + let mut target_y = icon_phys_y - window_phys_h - gap; - set_panel_top_left_immediately(&window, app_handle, panel_x, panel_y, primary_logical_h); + if let Some((pos, size)) = monitor_bounds { + let mon_right = pos.x as f64 + size.width as f64; + if target_x + window_phys_w > mon_right { + target_x = mon_right - window_phys_w; + } + if target_x < pos.x as f64 { + target_x = pos.x as f64; + } + // Ensure we don't go below screen bounds either (e.g. if tray is at top somehow) + if target_y < pos.y as f64 { + // place below icon instead + target_y = icon_phys_y + icon_phys_h + gap; + } + } + + let _ = window.set_position(tauri::Position::Physical(tauri::PhysicalPosition::new( + target_x as i32, + target_y as i32, + ))); + } } From d4f07831a9f54c2f35d6d60231af0759d1bf5f56 Mon Sep 17 00:00:00 2001 From: Native Muttaqien <83034829+samlehoy@users.noreply.github.com> Date: Sat, 30 May 2026 14:01:31 +0700 Subject: [PATCH 02/22] chore: bump version to 0.6.25 --- package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index b153240f..f13551ed 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openusage", "private": true, - "version": "0.6.24", + "version": "0.6.25", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6ba50359..ca90a94a 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openusage" -version = "0.6.24" +version = "0.6.25" description = "OpenUsage is an open source AI subscription limit tracker" authors = ["Robin Ebers"] edition = "2024" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index badb8861..cffcc799 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenUsage", - "version": "0.6.24", + "version": "0.6.25", "identifier": "com.sunstory.openusage", "build": { "beforeDevCommand": "bun run bundle:plugins && bun run dev", From 27063706cf7295e09d81ad634e7ab19d642f6ead Mon Sep 17 00:00:00 2001 From: Native Muttaqien <83034829+samlehoy@users.noreply.github.com> Date: Sun, 31 May 2026 13:58:08 +0700 Subject: [PATCH 03/22] fix: correct github action shell for windows --- .github/workflows/publish.yml | 6 +++++- package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 33bbcc71..25f319fc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -43,6 +43,7 @@ jobs: - name: Bundle plugins run: bun run bundle:plugins - name: Verify bundled plugins + shell: bash run: | COUNT=$(find src-tauri/resources/bundled_plugins -maxdepth 2 -name plugin.json | wc -l | tr -d ' ') if [[ "$COUNT" -lt 1 ]]; then @@ -51,6 +52,7 @@ jobs: fi - name: Validate release tag + shell: bash run: | if [[ -z "$RELEASE_TAG" ]]; then echo "Missing RELEASE_TAG (push a v* tag)." @@ -62,6 +64,7 @@ jobs: fi - name: Validate app version matches tag + shell: bash run: | TAG_VERSION="${RELEASE_TAG#v}" @@ -83,7 +86,7 @@ jobs: fi - name: Import Apple Developer Certificate - if: matrix.platform == 'macos-latest' + if: matrix.platform == 'macos-latest' && secrets.APPLE_CERTIFICATE != '' env: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} @@ -121,6 +124,7 @@ jobs: args: ${{ matrix.args }} - name: Verify updater assets uploaded + shell: bash env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | diff --git a/package.json b/package.json index f13551ed..84eba514 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openusage", "private": true, - "version": "0.6.25", + "version": "0.6.26", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ca90a94a..e6fd8aed 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openusage" -version = "0.6.25" +version = "0.6.26" description = "OpenUsage is an open source AI subscription limit tracker" authors = ["Robin Ebers"] edition = "2024" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index cffcc799..6325e6d5 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenUsage", - "version": "0.6.25", + "version": "0.6.26", "identifier": "com.sunstory.openusage", "build": { "beforeDevCommand": "bun run bundle:plugins && bun run dev", From d561e463feb0b64e306a7e3bdc5670d8622eea4e Mon Sep 17 00:00:00 2001 From: Native Muttaqien <83034829+samlehoy@users.noreply.github.com> Date: Sun, 31 May 2026 14:01:53 +0700 Subject: [PATCH 04/22] fix: move secrets check to bash script --- .github/workflows/publish.yml | 6 +++++- package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 25f319fc..d47132af 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -86,12 +86,16 @@ jobs: fi - name: Import Apple Developer Certificate - if: matrix.platform == 'macos-latest' && secrets.APPLE_CERTIFICATE != '' + if: matrix.platform == 'macos-latest' env: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} run: | + if [[ -z "$APPLE_CERTIFICATE" ]]; then + echo "Skipping Apple Certificate import because secret is empty." + exit 0 + fi echo "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12 security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security default-keychain -s build.keychain diff --git a/package.json b/package.json index 84eba514..371e7a88 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openusage", "private": true, - "version": "0.6.26", + "version": "0.6.27", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index e6fd8aed..7229297f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openusage" -version = "0.6.26" +version = "0.6.27" description = "OpenUsage is an open source AI subscription limit tracker" authors = ["Robin Ebers"] edition = "2024" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 6325e6d5..42471350 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenUsage", - "version": "0.6.26", + "version": "0.6.27", "identifier": "com.sunstory.openusage", "build": { "beforeDevCommand": "bun run bundle:plugins && bun run dev", From 948c47467e391961aead782e679211a05d588608 Mon Sep 17 00:00:00 2001 From: Native Muttaqien <83034829+samlehoy@users.noreply.github.com> Date: Sun, 31 May 2026 14:14:42 +0700 Subject: [PATCH 05/22] fix: resolve Windows rust compilation errors and disable macos matrix --- .github/workflows/publish.yml | 4 ---- package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/lib.rs | 1 + src-tauri/src/panel.rs | 4 ++-- src-tauri/src/tray.rs | 20 +++----------------- src-tauri/tauri.conf.json | 2 +- 7 files changed, 9 insertions(+), 26 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d47132af..291ecf1d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,10 +15,6 @@ jobs: fail-fast: false matrix: include: - - platform: macos-latest - args: "--target aarch64-apple-darwin" - - platform: macos-latest - args: "--target x86_64-apple-darwin" - platform: windows-latest args: "--target x86_64-pc-windows-msvc" runs-on: ${{ matrix.platform }} diff --git a/package.json b/package.json index 371e7a88..c26c3828 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openusage", "private": true, - "version": "0.6.27", + "version": "0.6.28", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7229297f..64572a01 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openusage" -version = "0.6.27" +version = "0.6.28" description = "OpenUsage is an open source AI subscription limit tracker" authors = ["Robin Ebers"] edition = "2024" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3bb08bc1..3aa7f485 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -501,6 +501,7 @@ pub fn run() { let runtime = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime"); let _guard = runtime.enter(); + #[allow(unused_mut)] let mut builder = tauri::Builder::default() .plugin(tauri_plugin_aptabase::Builder::new("A-US-6435241436").build()) .plugin(tauri_plugin_opener::init()) diff --git a/src-tauri/src/panel.rs b/src-tauri/src/panel.rs index d33e38f9..01416194 100644 --- a/src-tauri/src/panel.rs +++ b/src-tauri/src/panel.rs @@ -279,7 +279,7 @@ mod windows { macro_rules! get_window { ($app_handle:expr) => { - $app_handle.get_webview_window("main").ok() + $app_handle.get_webview_window("main") }; } @@ -374,7 +374,7 @@ mod windows { let size = monitor.size(); if icon_center_x >= pos.x as f64 && icon_center_x <= (pos.x + size.width as i32) as f64 && icon_center_y >= pos.y as f64 && icon_center_y <= (pos.y + size.height as i32) as f64 { - monitor_bounds = Some((pos, size)); + monitor_bounds = Some((*pos, *size)); break; } } diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index 545f29b0..a0ce56f0 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -3,10 +3,9 @@ use tauri::menu::{CheckMenuItem, Menu, MenuItem, PredefinedMenuItem, Submenu}; use tauri::path::BaseDirectory; use tauri::tray::{MouseButtonState, TrayIconBuilder, TrayIconEvent}; use tauri::{AppHandle, Emitter, Manager}; -use tauri_nspanel::ManagerExt; use tauri_plugin_store::StoreExt; -use crate::panel::{get_or_init_panel, position_panel_at_tray_icon, show_panel}; +use crate::panel::{show_panel, toggle_panel}; const LOG_LEVEL_STORE_KEY: &str = "logLevel"; @@ -183,24 +182,11 @@ pub fn create(app_handle: &AppHandle) -> tauri::Result<()> { let app_handle = tray.app_handle(); if let TrayIconEvent::Click { - button_state, rect, .. + button_state, .. } = event { if button_state == MouseButtonState::Up { - let Some(panel) = get_or_init_panel!(app_handle) else { - return; - }; - - if panel.is_visible() { - log::debug!("tray click: hiding panel"); - panel.hide(); - return; - } - log::debug!("tray click: showing panel"); - - // macOS quirk: must show window before positioning to another monitor - panel.show_and_make_key(); - position_panel_at_tray_icon(app_handle, rect.position, rect.size); + toggle_panel(app_handle); } } }) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 42471350..0435aa10 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenUsage", - "version": "0.6.27", + "version": "0.6.28", "identifier": "com.sunstory.openusage", "build": { "beforeDevCommand": "bun run bundle:plugins && bun run dev", From 8e2dd69ee4b7030df6ae1f68e630f006a9d62979 Mon Sep 17 00:00:00 2001 From: Native Muttaqien <83034829+samlehoy@users.noreply.github.com> Date: Sun, 31 May 2026 14:31:13 +0700 Subject: [PATCH 06/22] fix: gracefully disable updater on forks missing private keys --- .github/workflows/publish.yml | 21 ++++++++++++++++++++- package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 291ecf1d..7ec45260 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -101,6 +101,24 @@ jobs: security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain rm certificate.p12 + - name: Handle missing updater key (for forks) + shell: bash + env: + TAURI_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + run: | + if [[ -z "$TAURI_KEY" ]]; then + echo "HAS_TAURI_KEY=false" >> $GITHUB_ENV + node -e " + const fs = require('fs'); + const conf = JSON.parse(fs.readFileSync('./src-tauri/tauri.conf.json')); + if(conf.bundle) conf.bundle.createUpdaterArtifacts = false; + if(conf.plugins && conf.plugins.updater) delete conf.plugins.updater; + fs.writeFileSync('./src-tauri/tauri.conf.json', JSON.stringify(conf, null, 2)); + " + else + echo "HAS_TAURI_KEY=true" >> $GITHUB_ENV + fi + - uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -120,10 +138,11 @@ jobs: releaseName: ${{ env.RELEASE_TAG }} releaseDraft: false prerelease: false - includeUpdaterJson: true + includeUpdaterJson: ${{ env.HAS_TAURI_KEY == 'true' }} args: ${{ matrix.args }} - name: Verify updater assets uploaded + if: env.HAS_TAURI_KEY == 'true' shell: bash env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/package.json b/package.json index c26c3828..002733e5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openusage", "private": true, - "version": "0.6.28", + "version": "0.6.29", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 64572a01..5ada96f8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openusage" -version = "0.6.28" +version = "0.6.29" description = "OpenUsage is an open source AI subscription limit tracker" authors = ["Robin Ebers"] edition = "2024" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 0435aa10..5eefc8b7 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenUsage", - "version": "0.6.28", + "version": "0.6.29", "identifier": "com.sunstory.openusage", "build": { "beforeDevCommand": "bun run bundle:plugins && bun run dev", From 9d818ef71294767cfd6a0251a445467bc8741cfd Mon Sep 17 00:00:00 2001 From: Native Muttaqien <83034829+samlehoy@users.noreply.github.com> Date: Sun, 31 May 2026 14:56:44 +0700 Subject: [PATCH 07/22] fix: gracefully handle updater initialization failure --- package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/lib.rs | 15 ++++++++++++--- src-tauri/tauri.conf.json | 2 +- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 002733e5..1906b8df 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openusage", "private": true, - "version": "0.6.29", + "version": "0.6.30", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5ada96f8..931c00e9 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openusage" -version = "0.6.29" +version = "0.6.30" description = "OpenUsage is an open source AI subscription limit tracker" authors = ["Robin Ebers"] edition = "2024" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3aa7f485..294df2a4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -587,10 +587,19 @@ pub fn run() { local_http_api::init(&app_data_dir, known_plugin_ids); local_http_api::start_server(); - tray::create(app.handle())?; + log::info!("Setup: Starting tray creation"); + if let Err(e) = tray::create(app.handle()) { + log::error!("Setup: Tray creation failed: {:?}", e); + } + log::info!("Setup: Starting panel init"); + if let Err(e) = panel::init(app.handle()) { + log::error!("Setup: Panel init failed: {:?}", e); + } + log::info!("Setup: Finished"); - app.handle() - .plugin(tauri_plugin_updater::Builder::new().build())?; + if let Err(e) = app.handle().plugin(tauri_plugin_updater::Builder::new().build()) { + log::warn!("Failed to initialize updater plugin: {}", e); + } // Register global shortcut from stored settings #[cfg(desktop)] diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 5eefc8b7..f0cc3507 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenUsage", - "version": "0.6.29", + "version": "0.6.30", "identifier": "com.sunstory.openusage", "build": { "beforeDevCommand": "bun run bundle:plugins && bun run dev", From beb7637fa67f2cf7bb5a73879729627d5e86c739 Mon Sep 17 00:00:00 2001 From: Native Muttaqien <83034829+samlehoy@users.noreply.github.com> Date: Sun, 31 May 2026 15:10:19 +0700 Subject: [PATCH 08/22] docs: update README to reflect Windows fork --- README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 29885b01..b26c146c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# Track all your AI coding subscriptions in one place +# OpenUsage for Windows +> **Note**: This is a Windows-focused fork of the original [OpenUsage](https://github.com/robinebers/openusage) for macOS. See your usage at a glance from your menu bar. No digging through dashboards. @@ -6,7 +7,7 @@ See your usage at a glance from your menu bar. No digging through dashboards. ## Download -[**Download the latest release**](https://github.com/robinebers/openusage/releases/latest) (macOS, Apple Silicon & Intel) +[**Download the latest Windows release**](https://github.com/samlehoy/openusage/releases/latest) The app auto-updates. Install once and you're set. @@ -43,7 +44,7 @@ OpenUsage lives in your menu bar and shows you how much of your AI coding subscr Community contributions welcome. -Want a provider that's not listed? [Open an issue.](https://github.com/robinebers/openusage/issues/new) +Want a provider that's not listed? [Open an issue.](https://github.com/samlehoy/openusage/issues/new) ## Open Source, Community Driven @@ -53,11 +54,11 @@ I maintain the project as a guide and quality gatekeeper, but this is your app a Plugins are currently bundled as we build our the API, but soon will be made flexible so you can build and load their own. - + - - - Star History Chart + + + Star History Chart @@ -65,7 +66,7 @@ Plugins are currently bundled as we build our the API, but soon will be made fle - **Add a provider.** Each one is just a plugin. See the [Plugin API](docs/plugins/api.md). - **Fix a bug.** PRs welcome. Provide before/after screenshots. -- **Request a feature.** [Open an issue](https://github.com/robinebers/openusage/issues/new) and make your case. +- **Request a feature.** [Open an issue](https://github.com/samlehoy/openusage/issues/new) and make your case. Keep it simple. No feature creep, no AI-generated commit messages, test your changes. From d6e4c5dcca45b1ccea38c993ff20ade6cb84be28 Mon Sep 17 00:00:00 2001 From: Native Muttaqien <83034829+samlehoy@users.noreply.github.com> Date: Sun, 31 May 2026 15:16:24 +0700 Subject: [PATCH 09/22] fix: hide console windows spawned by plugin commands on Windows --- package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/lib.rs | 1 + src-tauri/src/plugin_engine/host_api.rs | 33 +++++++++++++------------ src-tauri/tauri.conf.json | 2 +- 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 1906b8df..a42f2faf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openusage", "private": true, - "version": "0.6.30", + "version": "0.6.31", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 931c00e9..32e9a7ce 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openusage" -version = "0.6.30" +version = "0.6.31" description = "OpenUsage is an open source AI subscription limit tracker" authors = ["Robin Ebers"] edition = "2024" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 294df2a4..ca18058c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -5,6 +5,7 @@ mod local_http_api; mod panel; mod plugin_engine; mod tray; +pub mod utils; #[cfg(target_os = "macos")] mod webkit_config; diff --git a/src-tauri/src/plugin_engine/host_api.rs b/src-tauri/src/plugin_engine/host_api.rs index be532de3..1746482a 100644 --- a/src-tauri/src/plugin_engine/host_api.rs +++ b/src-tauri/src/plugin_engine/host_api.rs @@ -127,7 +127,7 @@ fn read_env_from_process(name: &str) -> Option { } fn read_command_stdout(program: &str, args: &[&str]) -> Option { - let output = Command::new(program).args(args).output().ok()?; + let output = crate::utils::command_new(program).args(args).output().ok()?; if !output.status.success() { return None; } @@ -1306,7 +1306,7 @@ fn inject_ls<'js>(ctx: &Ctx<'js>, host: &Object<'js>, plugin_id: &str) -> rquick opts.markers ); - let ps_output = match std::process::Command::new("/bin/ps") + let ps_output = match crate::utils::command_new("/bin/ps") .args(["-ax", "-o", "pid=,command="]) .output() { @@ -1423,7 +1423,7 @@ fn inject_ls<'js>(ctx: &Ctx<'js>, host: &Object<'js>, plugin_id: &str) -> rquick .copied(); let ports = if let Some(lsof) = lsof_path { - match std::process::Command::new(lsof) + match crate::utils::command_new(lsof) .args([ "-nP", "-iTCP", @@ -1825,7 +1825,7 @@ fn ccusage_enriched_path() -> Option { } fn ccusage_runner_available(candidate: &str, enriched_path: Option<&OsStr>) -> bool { - let mut command = std::process::Command::new(candidate); + let mut command = crate::utils::command_new(candidate); command.arg("--version"); if let Some(path) = enriched_path { command.env("PATH", path); @@ -2130,7 +2130,7 @@ fn run_ccusage_with_runner_timeout( ) -> CcusageRunnerResult { let args = ccusage_runner_args(kind, opts, provider, flavor); let enriched_path = ccusage_enriched_path(); - let mut command = std::process::Command::new(program); + let mut command = crate::utils::command_new(program); configure_ccusage_command(&mut command, &args, enriched_path.as_deref()); if let Some(home_path) = ccusage_home_override(opts, provider) { @@ -2395,7 +2395,7 @@ fn inject_keychain<'js>( )); } log::info!("[plugin:{}] keychain read: service={}", pid_read, service); - let output = std::process::Command::new("security") + let output = crate::utils::command_new("security") .args(keychain_find_generic_password_args(&service)) .output() .map_err(|e| { @@ -2451,7 +2451,7 @@ fn inject_keychain<'js>( service, redacted_account ); - let output = std::process::Command::new("security") + let output = crate::utils::command_new("security") .args(&args) .output() .map_err(|e| { @@ -2503,7 +2503,7 @@ fn inject_keychain<'js>( log::info!("[plugin:{}] keychain write: service={}", pid_write, service); let mut account_arg: Option = None; - let find_output = std::process::Command::new("security") + let find_output = crate::utils::command_new("security") .args(["find-generic-password", "-s", &service]) .output(); @@ -2523,13 +2523,13 @@ fn inject_keychain<'js>( } let output = if let Some(ref acct) = account_arg { - std::process::Command::new("security") + crate::utils::command_new("security") .args(keychain_add_generic_password_args_for_account( &service, acct, &value, )) .output() } else { - std::process::Command::new("security") + crate::utils::command_new("security") .args(keychain_add_generic_password_args(&service, &value)) .output() } @@ -2584,7 +2584,7 @@ fn inject_keychain<'js>( service, redacted_account ); - let output = std::process::Command::new("security") + let output = crate::utils::command_new("security") .args(&args) .output() .map_err(|e| { @@ -2643,7 +2643,7 @@ fn inject_sqlite<'js>(ctx: &Ctx<'js>, host: &Object<'js>) -> rquickjs::Result<() // Prefer a normal read-only open so WAL contents are visible (common for app state DBs). // Fall back to immutable=1 to bypass WAL/SHM lock issues after macOS sleep. - let primary = std::process::Command::new("sqlite3") + let primary = crate::utils::command_new("sqlite3") .args(["-readonly", "-json", &expanded, &sql]) .output() .map_err(|e| { @@ -2661,7 +2661,7 @@ fn inject_sqlite<'js>(ctx: &Ctx<'js>, host: &Object<'js>) -> rquickjs::Result<() .replace('#', "%23") .replace('?', "%3F"); let uri_path = format!("file:{}?immutable=1", encoded); - let fallback = std::process::Command::new("sqlite3") + let fallback = crate::utils::command_new("sqlite3") .args(["-readonly", "-json", &uri_path, &sql]) .output() .map_err(|e| { @@ -2698,7 +2698,7 @@ fn inject_sqlite<'js>(ctx: &Ctx<'js>, host: &Object<'js>) -> rquickjs::Result<() )); } let expanded = expand_path(&db_path); - let output = std::process::Command::new("sqlite3") + let output = crate::utils::command_new("sqlite3") .args([&expanded, &sql]) .output() .map_err(|e| { @@ -3926,7 +3926,7 @@ mod tests { #[test] fn configure_ccusage_command_sets_path_override() { - let mut command = std::process::Command::new("echo"); + let mut command = crate::utils::command_new("echo"); let args = vec!["daily".to_string(), "--json".to_string()]; let path = std::env::join_paths([ std::path::PathBuf::from("/tmp/bin"), @@ -3951,7 +3951,7 @@ mod tests { #[test] fn configure_ccusage_command_skips_path_override_when_absent() { - let mut command = std::process::Command::new("echo"); + let mut command = crate::utils::command_new("echo"); let args = vec!["daily".to_string()]; configure_ccusage_command(&mut command, &args, None); @@ -4321,3 +4321,4 @@ wait let _ = std::fs::remove_dir_all(&dir); } } + diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f0cc3507..4d08911c 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenUsage", - "version": "0.6.30", + "version": "0.6.31", "identifier": "com.sunstory.openusage", "build": { "beforeDevCommand": "bun run bundle:plugins && bun run dev", From 5b158597c1e56cb1f2601a55051cd47651d32679 Mon Sep 17 00:00:00 2001 From: Native Muttaqien <83034829+samlehoy@users.noreply.github.com> Date: Sun, 31 May 2026 15:28:42 +0700 Subject: [PATCH 10/22] fix: add missing utils.rs file for windows console hiding --- package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/utils.rs | 12 ++++++++++++ src-tauri/tauri.conf.json | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 src-tauri/src/utils.rs diff --git a/package.json b/package.json index a42f2faf..f43f79c5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openusage", "private": true, - "version": "0.6.31", + "version": "0.6.32", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 32e9a7ce..fdba0f36 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openusage" -version = "0.6.31" +version = "0.6.32" description = "OpenUsage is an open source AI subscription limit tracker" authors = ["Robin Ebers"] edition = "2024" diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs new file mode 100644 index 00000000..f6175f9e --- /dev/null +++ b/src-tauri/src/utils.rs @@ -0,0 +1,12 @@ +use std::process::Command; + +pub fn command_new>(program: S) -> Command { + let mut cmd = Command::new(program); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x08000000; + cmd.creation_flags(CREATE_NO_WINDOW); + } + cmd +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 4d08911c..976bb2ff 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenUsage", - "version": "0.6.31", + "version": "0.6.32", "identifier": "com.sunstory.openusage", "build": { "beforeDevCommand": "bun run bundle:plugins && bun run dev", From 3dadfea3876881c8ea414ffa2c2d98c1bc9f4cf6 Mon Sep 17 00:00:00 2001 From: Native Muttaqien <83034829+samlehoy@users.noreply.github.com> Date: Sun, 31 May 2026 16:14:59 +0700 Subject: [PATCH 11/22] fix: add windows specific plugin paths, enable dragging, and remove window shadow --- package.json | 2 +- plugins/antigravity/plugin.js | 9 ++++++--- plugins/windsurf/plugin.js | 9 ++++++--- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 3 ++- src/components/app/app-shell.tsx | 3 ++- 6 files changed, 18 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index f43f79c5..f5768875 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openusage", "private": true, - "version": "0.6.32", + "version": "0.6.33", "type": "module", "scripts": { "dev": "vite", diff --git a/plugins/antigravity/plugin.js b/plugins/antigravity/plugin.js index 2b8d515b..6b26a4d3 100644 --- a/plugins/antigravity/plugin.js +++ b/plugins/antigravity/plugin.js @@ -1,6 +1,7 @@ (function () { var LS_SERVICE = "exa.language_server_pb.LanguageServerService" - var STATE_DB = "~/Library/Application Support/Antigravity/User/globalStorage/state.vscdb" + var STATE_DB_MAC = "~/Library/Application Support/Antigravity/User/globalStorage/state.vscdb" + var STATE_DB_WIN = "~/AppData/Roaming/Antigravity/User/globalStorage/state.vscdb" var CLOUD_CODE_URLS = [ "https://daily-cloudcode-pa.googleapis.com", "https://cloudcode-pa.googleapis.com", @@ -94,8 +95,9 @@ function loadOAuthTokens(ctx) { try { + var dbPath = ctx.app.platform === "windows" ? STATE_DB_WIN : STATE_DB_MAC var rows = ctx.host.sqlite.query( - STATE_DB, + dbPath, "SELECT value FROM ItemTable WHERE key = '" + OAUTH_TOKEN_KEY + "' LIMIT 1" ) var parsed = ctx.util.tryParseJson(rows) @@ -187,8 +189,9 @@ // --- LS discovery --- function discoverLs(ctx) { + var procName = ctx.app.platform === "windows" ? "language_server_windows.exe" : "language_server_macos" return ctx.host.ls.discover({ - processName: "language_server_macos", + processName: procName, markers: ["antigravity"], csrfFlag: "--csrf_token", portFlag: "--extension_server_port", diff --git a/plugins/windsurf/plugin.js b/plugins/windsurf/plugin.js index eb167b60..51842fc6 100644 --- a/plugins/windsurf/plugin.js +++ b/plugins/windsurf/plugin.js @@ -11,12 +11,14 @@ { marker: "windsurf", ideName: "windsurf", - stateDb: "~/Library/Application Support/Windsurf/User/globalStorage/state.vscdb", + stateDbWin: "~/AppData/Roaming/Windsurf/User/globalStorage/state.vscdb", + stateDbMac: "~/Library/Application Support/Windsurf/User/globalStorage/state.vscdb", }, { marker: "windsurf-next", ideName: "windsurf-next", - stateDb: "~/Library/Application Support/Windsurf - Next/User/globalStorage/state.vscdb", + stateDbWin: "~/AppData/Roaming/Windsurf - Next/User/globalStorage/state.vscdb", + stateDbMac: "~/Library/Application Support/Windsurf - Next/User/globalStorage/state.vscdb", }, ] @@ -38,8 +40,9 @@ function loadApiKey(ctx, variant) { try { + var dbPath = ctx.app.platform === "windows" ? variant.stateDbWin : variant.stateDbMac var rows = ctx.host.sqlite.query( - variant.stateDb, + dbPath, "SELECT value FROM ItemTable WHERE key = 'windsurfAuthStatus' LIMIT 1" ) var parsed = ctx.util.tryParseJson(rows) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index fdba0f36..eca92c4f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openusage" -version = "0.6.32" +version = "0.6.33" description = "OpenUsage is an open source AI subscription limit tracker" authors = ["Robin Ebers"] edition = "2024" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 976bb2ff..e9f8cf2b 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenUsage", - "version": "0.6.32", + "version": "0.6.33", "identifier": "com.sunstory.openusage", "build": { "beforeDevCommand": "bun run bundle:plugins && bun run dev", @@ -19,6 +19,7 @@ "resizable": false, "decorations": false, "transparent": true, + "shadow": false, "visible": false } ], diff --git a/src/components/app/app-shell.tsx b/src/components/app/app-shell.tsx index 8de76733..4d2ea834 100644 --- a/src/components/app/app-shell.tsx +++ b/src/components/app/app-shell.tsx @@ -70,9 +70,10 @@ export function AppShell({
-
+
Date: Sun, 31 May 2026 16:38:50 +0700 Subject: [PATCH 12/22] fix: process discovery on windows, overlapping taskbar, drag on transparent regions, and remove top arrow --- package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/lib.rs | 13 ++++++++++++- src-tauri/src/plugin_engine/host_api.rs | 17 ++++++++++++++--- src-tauri/tauri.conf.json | 4 ++-- src/components/app/app-shell.tsx | 6 ++++-- src/hooks/app/use-panel.ts | 3 ++- 7 files changed, 36 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index f5768875..f9564da0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openusage", "private": true, - "version": "0.6.33", + "version": "0.6.34", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index eca92c4f..74b3f5a0 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openusage" -version = "0.6.33" +version = "0.6.34" description = "OpenUsage is an open source AI subscription limit tracker" authors = ["Robin Ebers"] edition = "2024" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ca18058c..b528a458 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -215,6 +215,16 @@ fn open_devtools(#[allow(unused)] app_handle: tauri::AppHandle) { } } +#[tauri::command] +fn reposition_panel(app_handle: tauri::AppHandle) { + use tauri::Manager; + if let Some(tray) = app_handle.tray_by_id("tray") { + if let Ok(Some(rect)) = tray.rect() { + panel::position_panel_at_tray_icon(&app_handle, rect.position, rect.size); + } + } +} + #[tauri::command] async fn start_probe_batch( app_handle: tauri::AppHandle, @@ -538,7 +548,8 @@ pub fn run() { start_probe_batch, list_plugins, get_log_path, - update_global_shortcut + update_global_shortcut, + reposition_panel ]) .setup(|app| { #[cfg(target_os = "macos")] diff --git a/src-tauri/src/plugin_engine/host_api.rs b/src-tauri/src/plugin_engine/host_api.rs index 1746482a..76ec8e70 100644 --- a/src-tauri/src/plugin_engine/host_api.rs +++ b/src-tauri/src/plugin_engine/host_api.rs @@ -1306,10 +1306,21 @@ fn inject_ls<'js>(ctx: &Ctx<'js>, host: &Object<'js>, plugin_id: &str) -> rquick opts.markers ); - let ps_output = match crate::utils::command_new("/bin/ps") + #[cfg(windows)] + let ps_output_res = crate::utils::command_new("powershell") + .args([ + "-NoProfile", + "-Command", + "Get-CimInstance Win32_Process | ForEach-Object { \"$($_.ProcessId) $($_.CommandLine)\" }", + ]) + .output(); + + #[cfg(not(windows))] + let ps_output_res = crate::utils::command_new("/bin/ps") .args(["-ax", "-o", "pid=,command="]) - .output() - { + .output(); + + let ps_output = match ps_output_res { Ok(o) => o, Err(e) => { log::warn!("[plugin:{}] ps failed: {}", pid, e); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index e9f8cf2b..8a6cfd7d 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenUsage", - "version": "0.6.33", + "version": "0.6.34", "identifier": "com.sunstory.openusage", "build": { "beforeDevCommand": "bun run bundle:plugins && bun run dev", @@ -16,7 +16,7 @@ "title": "OpenUsage", "width": 400, "height": 500, - "resizable": false, + "resizable": true, "decorations": false, "transparent": true, "shadow": false, diff --git a/src/components/app/app-shell.tsx b/src/components/app/app-shell.tsx index 4d2ea834..0bdd17c3 100644 --- a/src/components/app/app-shell.tsx +++ b/src/components/app/app-shell.tsx @@ -65,15 +65,17 @@ export function AppShell({ const appVersion = useAppVersion() const { updateStatus, triggerInstall, checkForUpdates } = useAppUpdate() + const isWindows = navigator.userAgent.includes("Windows") return (
-
+ {!isWindows &&
}
Date: Sun, 31 May 2026 17:01:25 +0700 Subject: [PATCH 13/22] feat: implement window pinning and fix antigravity detection --- package.json | 2 +- plugins/antigravity/plugin.js | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/lib.rs | 21 +++++++++++++++++++-- src-tauri/src/panel.rs | 28 ++++++++++++++++++++-------- src-tauri/tauri.conf.json | 2 +- src/components/app/app-shell.tsx | 1 + src/components/side-nav.tsx | 26 +++++++++++++++++++++++--- 8 files changed, 67 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index f9564da0..794a5b69 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openusage", "private": true, - "version": "0.6.34", + "version": "0.6.35", "type": "module", "scripts": { "dev": "vite", diff --git a/plugins/antigravity/plugin.js b/plugins/antigravity/plugin.js index 6b26a4d3..a288d920 100644 --- a/plugins/antigravity/plugin.js +++ b/plugins/antigravity/plugin.js @@ -189,7 +189,7 @@ // --- LS discovery --- function discoverLs(ctx) { - var procName = ctx.app.platform === "windows" ? "language_server_windows.exe" : "language_server_macos" + var procName = ctx.app.platform === "windows" ? "language_server.exe" : "language_server_macos" return ctx.host.ls.discover({ processName: procName, markers: ["antigravity"], diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 74b3f5a0..a3e8b61f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openusage" -version = "0.6.34" +version = "0.6.35" description = "OpenUsage is an open source AI subscription limit tracker" authors = ["Robin Ebers"] edition = "2024" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b528a458..f196bda1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -11,9 +11,11 @@ mod webkit_config; use std::collections::{HashMap, HashSet, VecDeque}; use std::path::PathBuf; -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, OnceLock}; +pub static IS_PINNED: AtomicBool = AtomicBool::new(false); + use serde::Serialize; use tauri::Emitter; use tauri_plugin_aptabase::EventTracker; @@ -218,6 +220,9 @@ fn open_devtools(#[allow(unused)] app_handle: tauri::AppHandle) { #[tauri::command] fn reposition_panel(app_handle: tauri::AppHandle) { use tauri::Manager; + if crate::IS_PINNED.load(std::sync::atomic::Ordering::SeqCst) { + return; + } if let Some(tray) = app_handle.tray_by_id("tray") { if let Ok(Some(rect)) = tray.rect() { panel::position_panel_at_tray_icon(&app_handle, rect.position, rect.size); @@ -225,6 +230,16 @@ fn reposition_panel(app_handle: tauri::AppHandle) { } } +#[tauri::command] +fn set_pinned(pinned: bool) { + crate::IS_PINNED.store(pinned, std::sync::atomic::Ordering::SeqCst); +} + +#[tauri::command] +fn is_pinned() -> bool { + crate::IS_PINNED.load(std::sync::atomic::Ordering::SeqCst) +} + #[tauri::command] async fn start_probe_batch( app_handle: tauri::AppHandle, @@ -549,7 +564,9 @@ pub fn run() { list_plugins, get_log_path, update_global_shortcut, - reposition_panel + reposition_panel, + set_pinned, + is_pinned ]) .setup(|app| { #[cfg(target_os = "macos")] diff --git a/src-tauri/src/panel.rs b/src-tauri/src/panel.rs index 01416194..f278637c 100644 --- a/src-tauri/src/panel.rs +++ b/src-tauri/src/panel.rs @@ -114,7 +114,9 @@ mod macos { pub fn show_panel(app_handle: &AppHandle) { if let Some(panel) = get_or_init_panel!(app_handle) { panel.show_and_make_key(); - position_panel_from_tray(app_handle); + if !crate::IS_PINNED.load(std::sync::atomic::Ordering::SeqCst) { + position_panel_from_tray(app_handle); + } } } @@ -135,7 +137,9 @@ mod macos { } else { log::debug!("toggle_panel: showing panel"); panel.show_and_make_key(); - position_panel_from_tray(app_handle); + if !crate::IS_PINNED.load(std::sync::atomic::Ordering::SeqCst) { + position_panel_from_tray(app_handle); + } } } @@ -177,8 +181,10 @@ mod macos { let event_handler = OpenUsagePanelEventHandler::new(); let handle = app_handle.clone(); event_handler.window_did_resign_key(move |_notification| { - if let Ok(panel) = handle.get_webview_panel("main") { - panel.hide(); + if !crate::IS_PINNED.load(std::sync::atomic::Ordering::SeqCst) { + if let Ok(panel) = handle.get_webview_panel("main") { + panel.hide(); + } } }); @@ -287,7 +293,9 @@ mod windows { if let Some(window) = get_window!(app_handle) { let _ = window.show(); let _ = window.set_focus(); - position_panel_from_tray(app_handle); + if !crate::IS_PINNED.load(std::sync::atomic::Ordering::SeqCst) { + position_panel_from_tray(app_handle); + } } } @@ -309,7 +317,9 @@ mod windows { log::debug!("toggle_panel: showing window"); let _ = window.show(); let _ = window.set_focus(); - position_panel_from_tray(app_handle); + if !crate::IS_PINNED.load(std::sync::atomic::Ordering::SeqCst) { + position_panel_from_tray(app_handle); + } } } @@ -323,8 +333,10 @@ mod windows { let handle = app_handle.clone(); window.on_window_event(move |event| { if let tauri::WindowEvent::Focused(false) = event { - if let Some(w) = get_window!(handle) { - let _ = w.hide(); + if !crate::IS_PINNED.load(std::sync::atomic::Ordering::SeqCst) { + if let Some(w) = get_window!(handle) { + let _ = w.hide(); + } } } }); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 8a6cfd7d..95168da2 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenUsage", - "version": "0.6.34", + "version": "0.6.35", "identifier": "com.sunstory.openusage", "build": { "beforeDevCommand": "bun run bundle:plugins && bun run dev", diff --git a/src/components/app/app-shell.tsx b/src/components/app/app-shell.tsx index 0bdd17c3..e86e90ac 100644 --- a/src/components/app/app-shell.tsx +++ b/src/components/app/app-shell.tsx @@ -78,6 +78,7 @@ export function AppShell({ {!isWindows &&
}
diff --git a/src/components/side-nav.tsx b/src/components/side-nav.tsx index b363e888..7bc2f060 100644 --- a/src/components/side-nav.tsx +++ b/src/components/side-nav.tsx @@ -1,5 +1,5 @@ -import { useCallback } from "react" -import { CircleHelp, Settings } from "lucide-react" +import { useCallback, useEffect, useState } from "react" +import { CircleHelp, Settings, Pin, PinOff } from "lucide-react" import { openUrl } from "@tauri-apps/plugin-opener" import { invoke } from "@tauri-apps/api/core" import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu" @@ -148,6 +148,17 @@ export function SideNav({ onReorder, }: SideNavProps) { const isDark = useDarkMode() + const [isPinned, setIsPinned] = useState(false) + + useEffect(() => { + invoke("is_pinned").then(setIsPinned).catch(console.error) + }, []) + + const togglePin = () => { + const next = !isPinned + setIsPinned(next) + invoke("set_pinned", { pinned: next }).catch(console.error) + } const sensors = useSensors( useSensor(PointerSensor, { @@ -215,7 +226,7 @@ export function SideNav({ ) return ( -