ilia 80a6c01cdb Fix cursor positioning, resize, and paste; add App tests and Cursor rules
- 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>
2026-05-12 14:15:42 -04:00

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");
}