# PROJECT KNOWLEDGE BASE
**Project:** tauri-plugin-user-input
**Generated:** 2026-02-06
**Commit:** d06ebbc
**Branch:** dev
## OVERVIEW
Cross-platform Tauri v2 plugin for user input monitoring and simulation. Uses monio for event listening + key simulation, enigo for text/mouse/scroll. Desktop-only (mobile stubbed).
**Stack:** Rust (tauri v2, monio, enigo) + TypeScript (guest-js bindings)
## STRUCTURE
```
.
├── src/ # Rust backend (~560 lines)
│ ├── lib.rs # Plugin init, UserInputExt trait, command registration
│ ├── desktop.rs # Desktop impl (monio Hook + enigo singleton)
│ ├── mobile.rs # Mobile stub (only ping)
│ ├── commands.rs # 10 Tauri IPC command handlers
│ ├── models.rs # InputEvent, EventType, InputEventData + monio→plugin conversion
│ └── error.rs # Error enum (Io + PluginInvoke)
├── guest-js/ # TypeScript frontend API (~277 lines)
│ ├── index.ts # 10 exported functions (invoke wrappers)
│ └── types.ts # EventType, Key (183 variants), InputEvent (valibot schema)
├── build.rs # Permission auto-generation from COMMANDS array
├── permissions/ # Tauri v2 permission files
│ ├── default.toml # Default permission set (all 10 commands allowed)
│ └── autogenerated/# DO NOT EDIT — generated by build.rs
├── examples/
│ └── tauri-app/ # Svelte + Vite + TailwindCSS demo app
└── rollup.config.js # Bundles guest-js → ESM + CJS (dist-js/)
```
## WHERE TO LOOK
| Add a command | `src/commands.rs` + `build.rs` COMMANDS array + `src/lib.rs` generate_handler | All three must be updated |
| Event types / data shapes | `src/models.rs` | `From<monio::Event>` conversion logic is here |
| Event listening lifecycle | `src/desktop.rs` lines 64-107 | Hook::new() + run_async + stop |
| Key simulation | `src/desktop.rs` lines 129-137 | monio::key_press/key_release/key_tap |
| Text/mouse/scroll simulation | `src/desktop.rs` lines 140-170 | Uses ENIGO global singleton |
| JS API functions | `guest-js/index.ts` | Thin invoke() wrappers over IPC |
| JS type definitions | `guest-js/types.ts` | Key union type, valibot InputEvent schema |
| Permissions | `permissions/default.toml` | Manual; `autogenerated/` is build output |
| Mobile stub | `src/mobile.rs` | Only has ping — all features unimplemented |
## CODE MAP
**Entry Points:**
- `src/lib.rs::init()` — Plugin factory, registers all 10 commands
- `src/lib.rs::UserInputExt` — Extension trait: `app_handle.user_input()` → `&UserInput<R>`
- `guest-js/index.ts` — All 10 TS functions exported
**Key Symbols:**
| `UserInputExt<R>` | trait | lib.rs:25 | Extension trait for AppHandle access |
| `UserInput<R>` | struct | desktop.rs:38 | Holds Hook + channels + event state |
| `ENIGO` | static LazyLock | desktop.rs:14 | Global enigo singleton (Send+Sync via unsafe) |
| `InputEvent` | struct | models.rs:19 | Serialized event sent to frontend |
| `EventType` | enum | models.rs:44 | 9 variants (KeyPress/Release/Click, Button*, MouseMove/Dragged, Wheel) |
| `InputEventData` | enum | models.rs:28 | Flattened payload: Button/Position/DeltaPosition/Key |
| `monio::Hook` | external | desktop.rs:40 | Stored in `Arc<Mutex<Option<Hook>>>`, drives listener lifecycle |
**Event Flow:**
```
monio::Hook::run_async(callback)
→ handle_monio_event()
→ InputEvent::from(monio::Event)
→ emit_to(window_labels) + channel.send(channels)
```
**Simulation Split:**
- Keys: `monio::key_press/key_release/key_tap` (stable on macOS)
- Text/Mouse/Scroll: `enigo` via ENIGO singleton
## CONVENTIONS
**Rust:**
- Platform split: `#[cfg(desktop)]` / `#[cfg(mobile)]` at module level
- Commands: `#[command] pub(crate) async fn` pattern
- State: `Arc<Mutex<T>>` for all shared state in UserInput
- Serde: `#[serde(rename_all = "camelCase")]` on all models
- monio types need `recorder` feature for serde support
**TypeScript:**
- Command namespace: `plugin:user-input|{command_name}`
- Event streaming via Tauri `Channel<T>`
- Valibot for runtime type validation (runtime dependency)
- Rollup bundles to ESM + CJS in dist-js/
**Build Pipeline:**
- `build.rs` reads COMMANDS array → generates `permissions/autogenerated/commands/*.toml`
- `pnpm build` → rollup → dist-js/ (ESM + CJS + .d.ts)
- `cargo build` → compiles Rust + runs build.rs
## ANTI-PATTERNS (THIS PROJECT)
**DO NOT:**
- Create multiple enigo instances — use the global `ENIGO` singleton
- Forget to update ALL THREE places when adding a command: `commands.rs`, `build.rs` COMMANDS array, `lib.rs` generate_handler
- Manually edit files in `permissions/autogenerated/` — they are regenerated on build
- Use `unwrap()` on channel.send() or emit_to() — receivers may drop (use `let _ =`)
- Add `assert!`/`assert_eq!` in non-test code — they panic in release builds
**KNOWN ISSUES:**
- Error handling inconsistency: some methods return `Result<(), Error>`, others `Result<(), String>`
- `println!` debug output left in desktop.rs (lines 91, 92, 119) — should use proper logging
- `PingRequest`/`PingResponse` in models.rs are dead code (only used by mobile stub)
- `EventType::ButtonClick` variant exists but is never produced from monio events
## COMMANDS
```bash
# Build TypeScript API
pnpm build
# Build Rust
cargo build
# Test Rust
cargo test
# Run example app
cd examples/tauri-app && pnpm install && pnpm tauri dev
```
## NOTES
**ENIGO Safety:** `SafeEnigo` wraps `Mutex<enigo::Enigo>` with `unsafe impl Send + Sync`. Enigo's internal CGEventSource is not Send, but access is serialized through the Mutex. Single global instance only.
**Mobile:** Fully stubbed. `mobile.rs` only has `ping()`. All 10 commands are registered but will fail on mobile — no native Android/iOS implementation exists.
**Permissions:** The `default` permission set allows all 10 commands. For fine-grained control, use individual `allow-{command}` / `deny-{command}` permissions.
**MSRV:** 1.80.0 (required for `std::sync::LazyLock`).