tauri-plugin-user-input 0.1.1

Tauri plugin for user input (monitor and simulate keyboard and mouse events)
Documentation
# 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

| Task | Location | Notes |
|------|----------|-------|
| 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:**

| Symbol | Type | Location | Role |
|--------|------|----------|------|
| `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`).