mod clipboard; mod commands; mod db; use commands::{DbState, PausedState}; use db::Database; use std::sync::atomic::AtomicBool; use std::sync::Arc; use tauri::{ menu::{CheckMenuItem, Menu, MenuItem, PredefinedMenuItem}, tray::TrayIconBuilder, Emitter, Manager, WindowEvent, }; use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, Modifiers, Shortcut, ShortcutState}; /// Get the global mouse position using CoreGraphics. /// Returns logical (x, y) in screen coordinates with origin at top-left. #[cfg(target_os = "macos")] fn get_mouse_position() -> Option<(f64, f64)> { use core_graphics::event::CGEvent; use core_graphics::event_source::{CGEventSource, CGEventSourceStateID}; let source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState).ok()?; let event = CGEvent::new(source).ok()?; let loc = event.location(); Some((loc.x, loc.y)) } #[cfg(not(target_os = "macos"))] fn get_mouse_position() -> Option<(f64, f64)> { None } fn show_window(app: &tauri::AppHandle) { if let Some(window) = app.get_webview_window("main") { if let Some(db) = app.try_state::() { if let Ok(settings) = db.0.get_settings() { let _ = window.set_size(tauri::LogicalSize::new( settings.window_width as f64, settings.window_height as f64, )); position_window(&window, &settings.window_position); } } let _ = window.show(); let _ = window.set_focus(); } } fn toggle_window(app: &tauri::AppHandle) { if let Some(window) = app.get_webview_window("main") { if window.is_visible().unwrap_or(false) { let _ = window.hide(); } else { show_window(app); } } } fn position_window(window: &tauri::WebviewWindow, position: &str) { let get_monitor_info = |w: &tauri::WebviewWindow| -> Option<(f64, f64, f64)> { let monitor = w.current_monitor().ok()??; let size = monitor.size(); let scale = monitor.scale_factor(); Some((size.width as f64 / scale, size.height as f64 / scale, scale)) }; let win_w = 420.0_f64; let win_h = 560.0_f64; let (ww, wh) = window .outer_size() .ok() .map(|s| { let scale = window .scale_factor() .unwrap_or(1.0); (s.width as f64 / scale, s.height as f64 / scale) }) .unwrap_or((win_w, win_h)); match position { "center" => { let _ = window.center(); } "top-right" => { if let Some((sw, _, _)) = get_monitor_info(window) { let _ = window.set_position(tauri::LogicalPosition::new(sw - ww - 12.0, 30.0)); } } "top-left" => { let _ = window.set_position(tauri::LogicalPosition::new(12.0, 30.0)); } "bottom-right" => { if let Some((sw, sh, _)) = get_monitor_info(window) { let _ = window.set_position(tauri::LogicalPosition::new( sw - ww - 12.0, sh - wh - 12.0, )); } } "bottom-left" => { if let Some((_, sh, _)) = get_monitor_info(window) { let _ = window.set_position(tauri::LogicalPosition::new(12.0, sh - wh - 12.0)); } } // "cursor" — position near the mouse pointer _ => { if let Some((mx, my)) = get_mouse_position() { if let Some((sw, sh, _)) = get_monitor_info(window) { // Keep window on-screen let x = (mx - ww / 2.0).clamp(8.0, sw - ww - 8.0); let y = (my + 12.0).clamp(8.0, sh - wh - 8.0); let _ = window.set_position(tauri::LogicalPosition::new(x, y)); } else { let _ = window.set_position(tauri::LogicalPosition::new(mx - ww / 2.0, my + 12.0)); } } else { let _ = window.center(); } } } } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let db = Arc::new(Database::new().expect("Failed to initialize database")); let paused = Arc::new(AtomicBool::new(false)); let db_for_polling = db.clone(); let paused_for_polling = paused.clone(); tauri::Builder::default() .plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_autostart::init( MacosLauncher::LaunchAgent, Some(vec![]), )) .plugin( tauri_plugin_global_shortcut::Builder::new() .with_handler(|app, shortcut, event| { if event.state == ShortcutState::Pressed { let expected = Shortcut::new( Some(Modifiers::SUPER | Modifiers::SHIFT), Code::KeyV, ); if shortcut == &expected { toggle_window(app); } } }) .build(), ) .manage(DbState(db.clone())) .manage(PausedState(paused.clone())) .invoke_handler(tauri::generate_handler![ commands::get_entries, commands::search_entries, commands::delete_entry, commands::toggle_pin, commands::clear_all, commands::get_settings, commands::set_setting, commands::get_paused, commands::set_paused, commands::save_window_size, commands::paste_and_refocus, ]) .setup(move |app| { let shortcut = Shortcut::new( Some(Modifiers::SUPER | Modifiers::SHIFT), Code::KeyV, ); app.global_shortcut().register(shortcut)?; let show_i = MenuItem::with_id(app, "show", "Show maCopy", true, None::<&str>)?; let pause_i = CheckMenuItem::with_id(app, "pause", "Pause Monitoring", true, false, None::<&str>)?; let sep = PredefinedMenuItem::separator(app)?; let settings_i = MenuItem::with_id(app, "settings", "Settings…", true, None::<&str>)?; let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; let menu = Menu::with_items(app, &[&show_i, &pause_i, &sep, &settings_i, &quit_i])?; let _tray = TrayIconBuilder::with_id("macopy-tray") .icon(app.default_window_icon().unwrap().clone()) .menu(&menu) .show_menu_on_left_click(false) .tooltip("maCopy — Clipboard Manager") .on_menu_event({ let paused_clone = paused_for_polling.clone(); move |app, event| match event.id.as_ref() { "show" => toggle_window(app), "pause" => { let current = paused_clone.load(std::sync::atomic::Ordering::Relaxed); paused_clone .store(!current, std::sync::atomic::Ordering::Relaxed); } "settings" => { if let Some(window) = app.get_webview_window("main") { let _ = window.show(); let _ = window.set_focus(); let _ = window.emit("open-settings", ()); } } "quit" => app.exit(0), _ => {} } }) .on_tray_icon_event(|tray, event| { if let tauri::tray::TrayIconEvent::Click { .. } = event { toggle_window(tray.app_handle()); } }) .build(app)?; #[cfg(target_os = "macos")] app.set_activation_policy(tauri::ActivationPolicy::Accessory); if let Some(window) = app.get_webview_window("main") { let w = window.clone(); window.on_window_event(move |event| { if let WindowEvent::Focused(false) = event { let _ = w.hide(); } }); } clipboard::start_polling(db_for_polling, paused_for_polling); Ok(()) }) .run(tauri::generate_context!()) .expect("error while running tauri application"); }