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:
Generated
+15
-1
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
@@ -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),
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user