- Use CoreGraphics (core-graphics crate) for global mouse position instead of Tauri's window-relative cursor_position() which fails when hidden - Switch to titleBarStyle overlay with hiddenTitle for native resize handles while keeping the frameless look (decorations:false had no resize affordance) - Fix paste_and_refocus: use .output() instead of .spawn() so osascript actually completes, increase delay to 250ms for reliable app refocus - Add comprehensive App.test.tsx (7 integration tests) bringing total to 47 - Add multi-select and type badge tests for ClipboardList and ContextMenu - Update Tauri mock in setup.ts with innerSize/scaleFactor for resize tests - Create .cursor/rules/ with 4 rule files for project conventions - Add npm run lint and npm run check scripts - Update README with full usage table and current test counts (74 total) Co-authored-by: Cursor <cursoragent@cursor.com>
237 lines
8.6 KiB
Rust
237 lines
8.6 KiB
Rust
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::<DbState>() {
|
|
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");
|
|
}
|