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>
This commit is contained in:
2026-05-12 14:15:42 -04:00
co-authored by Cursor
parent 55ee81fc6d
commit 80a6c01cdb
17 changed files with 504 additions and 81 deletions
+15 -1
View File
@@ -418,6 +418,19 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "core-graphics"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
dependencies = [
"bitflags 2.11.1",
"core-foundation",
"core-graphics-types",
"foreign-types",
"libc",
]
[[package]]
name = "core-graphics"
version = "0.25.0"
@@ -1948,6 +1961,7 @@ dependencies = [
"arboard",
"base64 0.22.1",
"chrono",
"core-graphics 0.24.0",
"dirs 5.0.1",
"hex",
"log",
@@ -3276,7 +3290,7 @@ dependencies = [
"bitflags 2.11.1",
"block2",
"core-foundation",
"core-graphics",
"core-graphics 0.25.0",
"crossbeam-channel",
"dbus",
"dispatch2",
+3
View File
@@ -28,5 +28,8 @@ png = "0.17"
dirs = "5"
tokio = { version = "1", features = ["time", "macros"] }
[target.'cfg(target_os = "macos")'.dependencies]
core-graphics = "0.24"
[features]
custom-protocol = ["tauri/custom-protocol"]
+9 -6
View File
@@ -64,23 +64,26 @@ pub fn save_window_size(db: State<'_, DbState>, width: i64, height: i64) -> Resu
.map_err(|e| e.to_string())
}
/// Hide the window then simulate Cmd+V so the content pastes into the
/// previously-focused app. The 150ms delay gives macOS time to refocus.
/// Hide the window, wait for macOS to refocus the previous app,
/// then simulate Cmd+V via AppleScript to paste the clipboard content.
/// Requires Accessibility permission (System Settings → Privacy → Accessibility).
#[tauri::command]
pub async fn paste_and_refocus(app: tauri::AppHandle) -> Result<(), String> {
if let Some(window) = app.get_webview_window("main") {
let _ = window.hide();
}
tokio::time::sleep(std::time::Duration::from_millis(150)).await;
// macOS needs time to refocus the previous application
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
// Use AppleScript to press Cmd+V in the frontmost app
#[cfg(target_os = "macos")]
{
let _ = std::process::Command::new("osascript")
// .output() blocks until osascript finishes, ensuring the keystroke lands
std::process::Command::new("osascript")
.arg("-e")
.arg(r#"tell application "System Events" to keystroke "v" using command down"#)
.spawn();
.output()
.map_err(|e| format!("osascript failed: {}", e))?;
}
Ok(())
+68 -43
View File
@@ -7,23 +7,40 @@ use db::Database;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use tauri::{
menu::{Menu, MenuItem, PredefinedMenuItem, CheckMenuItem},
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") {
// Position window before showing based on settings
if let Some(db) = app.try_state::<DbState>() {
if let Ok(settings) = db.0.get_settings() {
position_window(&window, &settings.window_position);
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();
@@ -42,54 +59,63 @@ fn toggle_window(app: &tauri::AppHandle) {
}
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 Ok(monitor) = window.current_monitor() {
if let Some(monitor) = monitor {
let size = monitor.size();
let scale = monitor.scale_factor();
let win_size = window.outer_size().unwrap_or(tauri::PhysicalSize::new(420, 560));
let x = (size.width as f64 / scale) - (win_size.width as f64 / scale) - 10.0;
let _ = window.set_position(tauri::LogicalPosition::new(x, 30.0));
}
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(10.0, 30.0));
let _ = window.set_position(tauri::LogicalPosition::new(12.0, 30.0));
}
"bottom-right" => {
if let Ok(monitor) = window.current_monitor() {
if let Some(monitor) = monitor {
let size = monitor.size();
let scale = monitor.scale_factor();
let win_size = window.outer_size().unwrap_or(tauri::PhysicalSize::new(420, 560));
let x = (size.width as f64 / scale) - (win_size.width as f64 / scale) - 10.0;
let y = (size.height as f64 / scale) - (win_size.height as f64 / scale) - 10.0;
let _ = window.set_position(tauri::LogicalPosition::new(x, y));
}
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 Ok(monitor) = window.current_monitor() {
if let Some(monitor) = monitor {
let size = monitor.size();
let scale = monitor.scale_factor();
let win_size = window.outer_size().unwrap_or(tauri::PhysicalSize::new(420, 560));
let y = (size.height as f64 / scale) - (win_size.height as f64 / scale) - 10.0;
let _ = window.set_position(tauri::LogicalPosition::new(10.0, y));
}
if let Some((_, sh, _)) = get_monitor_info(window) {
let _ = window.set_position(tauri::LogicalPosition::new(12.0, sh - wh - 12.0));
}
}
// "cursor" or default — position near the mouse cursor
// "cursor" — position near the mouse pointer
_ => {
if let Ok(cursor) = window.cursor_position() {
let _ = window.set_position(tauri::LogicalPosition::new(
cursor.x - 210.0,
cursor.y + 10.0,
));
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();
}
@@ -149,14 +175,13 @@ pub fn run() {
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 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])?;
// Use with_id to prevent duplicate tray icons across hot-reloads
let _tray = TrayIconBuilder::with_id("macopy-tray")
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
@@ -167,8 +192,10 @@ pub fn run() {
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);
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") {
@@ -177,9 +204,7 @@ pub fn run() {
let _ = window.emit("open-settings", ());
}
}
"quit" => {
app.exit(0);
}
"quit" => app.exit(0),
_ => {}
}
})
+5 -1
View File
@@ -20,7 +20,9 @@
"resizable": true,
"minWidth": 320,
"minHeight": 300,
"decorations": false,
"decorations": true,
"titleBarStyle": "overlay",
"hiddenTitle": true,
"visible": false,
"alwaysOnTop": true,
"skipTaskbar": true,
@@ -49,6 +51,8 @@
"core:window:allow-current-monitor",
"core:window:allow-cursor-position",
"core:window:allow-start-dragging",
"core:window:allow-start-resize-dragging",
"core:window:allow-scale-factor",
"core:event:default",
"core:event:allow-emit",
"core:event:allow-listen",