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
+31
View File
@@ -0,0 +1,31 @@
---
description: maCopy project overview and architecture
alwaysApply: true
---
# maCopy — macOS Clipboard Manager
Tauri 2 desktop app: Rust backend + React/TypeScript frontend.
## Architecture
- `src-tauri/src/lib.rs` — App setup, tray icon, global hotkey (Cmd+Shift+V), window management
- `src-tauri/src/clipboard.rs` — Background polling thread (500ms), SHA-256 dedup, arboard crate
- `src-tauri/src/db.rs` — SQLite via rusqlite, FTS5 full-text search, settings CRUD
- `src-tauri/src/commands.rs` — Tauri IPC commands exposed to frontend
- `src/App.tsx` — Main React component, state, keyboard nav, multi-select
- `src/components/` — SearchBar, ClipboardList, ContextMenu, SettingsPanel
## Key Patterns
- Window positions near cursor via CoreGraphics (`core-graphics` crate)
- Paste-to-previous-app: hide window → 250ms delay → AppleScript `Cmd+V`
- Window uses `titleBarStyle: "overlay"` with `hiddenTitle` for native resize + frameless look
- Settings stored in SQLite `settings` table as key-value pairs
- Clipboard entries deduplicated by SHA-256 hash, auto-trimmed to max_history
## Testing
- Rust: `cargo test` in `src-tauri/` — 27 unit tests (db, clipboard hashing)
- Frontend: `npx vitest run` — 47 tests (App, SearchBar, ClipboardList, ContextMenu, SettingsPanel)
- Tauri APIs mocked in `src/test/setup.ts`
+33
View File
@@ -0,0 +1,33 @@
---
description: React/TypeScript frontend patterns for Tauri app
globs: src/**/*.{ts,tsx}
alwaysApply: false
---
# Frontend Conventions
## Component Structure
- Functional components with hooks, no class components
- Types in `src/types.ts`, shared between components
- Test files colocated: `Component.test.tsx` next to `Component.tsx`
- Test factories in `src/test/factories.ts` (`makeEntry`, `makeSettings`)
## Tauri IPC
- Use `invoke()` from `@tauri-apps/api/core` to call Rust commands
- Use `writeText()` from `@tauri-apps/plugin-clipboard-manager` for clipboard writes
- Mock all Tauri APIs in `src/test/setup.ts` for Vitest
## State Management
- App-level state in `App.tsx` via `useState`/`useCallback`
- Multi-select: `selectedIds` (Set), `anchorIndex`, `focusIndex`
- Selection actions: `selectOnly` (plain click), `toggleSelect` (Cmd+Click), `selectRange` (Shift+Click/Arrow)
## Styling
- Tailwind CSS v4 with custom theme tokens in `src/index.css`
- Theme colors: `--color-surface`, `--color-accent`, `--color-text-primary`, etc.
- Dark mode via `@media (prefers-color-scheme: dark)` overrides in CSS
- Never use inline styles except for dynamic positioning (ContextMenu)
+28
View File
@@ -0,0 +1,28 @@
---
description: Rust backend conventions for Tauri commands and database
globs: src-tauri/src/**/*.rs
alwaysApply: false
---
# Rust Backend Conventions
## Tauri Commands
- All IPC commands live in `commands.rs`, annotated with `#[tauri::command]`
- Commands that need DB access take `State<'_, DbState>`
- Commands that need the app handle take `app: tauri::AppHandle`
- Return `Result<T, String>` — map errors with `.map_err(|e| e.to_string())`
- Register every new command in `lib.rs` → `invoke_handler`
## Database (db.rs)
- `Database` wraps `Mutex<Connection>` for thread safety
- Use `Database::in_memory()` in tests, `Database::new()` in production
- Settings are key-value in the `settings` table; add defaults in `init_tables()`
- FTS5 table `clipboard_fts` syncs via triggers on insert/delete
## macOS-Specific Code
- Gate with `#[cfg(target_os = "macos")]`
- Cursor position: use `core-graphics` crate's `CGEvent` (not Tauri's `cursor_position()` which is window-relative)
- AppleScript for paste simulation: always use `.output()` not `.spawn()` to ensure execution completes
+29
View File
@@ -0,0 +1,29 @@
---
description: Testing conventions for Rust and TypeScript
globs: "**/*.test.{ts,tsx}"
alwaysApply: false
---
# Testing Conventions
## Rust Tests (cargo test)
- In-module `#[cfg(test)]` blocks using `Database::in_memory()`
- Test naming: `snake_case` describing behavior (e.g. `trim_preserves_pinned_entries`)
- All tests must pass before committing: `cd src-tauri && cargo test`
## Frontend Tests (Vitest)
- Use `@testing-library/react` with `jsdom` environment
- Mock Tauri APIs in `src/test/setup.ts` — never call real IPC in tests
- Use `makeEntry()` and `makeSettings()` factories for test data
- Call `resetIdCounter()` in `beforeEach` when IDs matter
- When testing async Tauri commands, use `waitFor()` assertions
- Guard `scrollIntoView` with optional chaining (`el?.scrollIntoView?.()`) since jsdom doesn't implement it
## Running Tests
```bash
cd src-tauri && cargo test # Rust: 27 tests
npx vitest run # Frontend: 47 tests
```