Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
378ccb5
docs: add Hebrew typing trainer design spec
erimeilis Jul 16, 2026
51907c8
chore: upgrade TypeScript 5.9.3 -> 7.0.2
erimeilis Jul 16, 2026
c992bbe
docs: add Hebrew typing trainer implementation plan
erimeilis Jul 16, 2026
5b80651
feat(trainer): add shared types and Hebrew layout data
erimeilis Jul 16, 2026
c4d9fc8
feat(trainer): add DomKeySource keystroke adapter
erimeilis Jul 16, 2026
3ad61e0
feat(trainer): add TauriTapKeySource keystroke adapter
erimeilis Jul 16, 2026
cf100c2
test(trainer): cover TauriTapKeySource stop-before-resolve race
erimeilis Jul 16, 2026
c302e55
feat(trainer): add TrainerStore persistence interface + adapters
erimeilis Jul 16, 2026
8f06ef3
feat(trainer): add set_trainer_mode command and unthrottled key-event
erimeilis Jul 16, 2026
2819c6c
fix(trainer): robust revert + error logging + main-thread dispatch fo…
erimeilis Jul 16, 2026
0541166
feat(trainer): Key trainer visuals via TrainerKeyboardContext
erimeilis Jul 16, 2026
1099127
fix(trainer): hide/dim all key label variants for guidance modes
erimeilis Jul 16, 2026
45b3f32
feat(trainer): Keyboard accepts trainerView and tags trainable keys w…
erimeilis Jul 16, 2026
6e43d78
test(trainer): mock @tauri-apps/api/event in Keyboard.trainer.test to…
erimeilis Jul 16, 2026
b7b049b
feat(trainer): trainer entry/exit shell and App toggle
erimeilis Jul 16, 2026
6e8a8b1
feat(trainer): Hebrew curriculum ladder with sofit and confusable groups
erimeilis Jul 16, 2026
0906874
feat(trainer): per-key confidence model
erimeilis Jul 16, 2026
c7f2506
feat(trainer): accuracy-first stage gating
erimeilis Jul 16, 2026
a517ee8
feat(trainer): real-word selection with weak-key ranking and finger-d…
erimeilis Jul 17, 2026
b16b959
feat(trainer): session scoring (wpm, per-char + whole-word accuracy)
erimeilis Jul 17, 2026
e345857
feat(trainer): Hebrew error classification (sofit, confusable, real-w…
erimeilis Jul 17, 2026
0174768
feat(trainer): useTypingSession hook over a KeySource
erimeilis Jul 17, 2026
6ef9eff
fix(trainer): reset useTypingSession on target change; ref strictness…
erimeilis Jul 17, 2026
7599da1
fix(trainer): reset hadErrorHere after markThrough advance so error d…
erimeilis Jul 17, 2026
e2fe9ce
feat(trainer): PracticePanel with RTL live coloring
erimeilis Jul 17, 2026
8906d59
feat(trainer): keyboardView builder (heatmap + guidance modes)
erimeilis Jul 17, 2026
251cc31
feat(trainer): placement test and stage seeding
erimeilis Jul 17, 2026
d525303
feat(trainer): settings/progress/stats persistence helpers
erimeilis Jul 17, 2026
8fd1598
feat(trainer): session summary view
erimeilis Jul 17, 2026
267d894
feat(trainer): assemble core session loop (select->type->score->advance)
erimeilis Jul 17, 2026
a8ac9a0
feat(trainer): Hebrew prose/quotes corpus and sentence selection
erimeilis Jul 17, 2026
08a9620
feat(trainer): custom text paste with Hebrew sanitization
erimeilis Jul 17, 2026
a6b5315
fix(trainer): sanitizeHebrew collapses newlines/tabs to spaces (multi…
erimeilis Jul 17, 2026
b39fb82
feat(trainer): progress graphs (inline SVG)
erimeilis Jul 17, 2026
00b232f
feat(trainer): meaning-aware real-word mistype feedback
erimeilis Jul 17, 2026
60e9cc1
feat(trainer): streaks, daily goal, and skill stars
erimeilis Jul 17, 2026
712cab6
feat(trainer): celebratory and fault animations
erimeilis Jul 17, 2026
8568e31
feat(trainer): ghost race against personal best
erimeilis Jul 17, 2026
3848f8a
feat(trainer): text-theme-tied background
erimeilis Jul 17, 2026
7d5ecf0
feat(trainer): settings controls (guidance/strictness/capture) + UX p…
erimeilis Jul 17, 2026
27e9d95
feat(trainer): trainer UX pass — common window, gating, controls, fee…
erimeilis Jul 19, 2026
5430b84
refactor(trainer): organize src/trainer into role-based folders
erimeilis Jul 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ build

# Claude and docs
.claude
docs
docs/*
!docs/plans/
99 changes: 99 additions & 0 deletions docs/plans/2026-02-28-virtual-typing-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Virtual Keyboard Typing Design

## Goal

Make the keyboard overlay functional for typing. Clicking keys on the overlay inputs characters into whatever application currently has focus, like macOS Accessibility Keyboard.

## Decisions

- **Focus handling**: Non-activating window (NSPanel) — clicks don't steal focus from target app
- **Key injection**: `rdev::simulate` — sends raw keycodes, macOS applies active input source (Hebrew/English)
- **Modifier behavior**: Sticky modifiers — click Shift, it stays active, next key is modified, then Shift auto-releases. Double-click locks.
- **Input method**: Raw keycodes, not Unicode characters. macOS handles layout mapping.

## Architecture

### 1. Non-Activating Window (Rust/macOS)

In `lib.rs` setup, after window creation, access the native `NSWindow` and set it as non-activating:

```rust
#[cfg(target_os = "macos")]
{
use cocoa::appkit::NSWindow;
use cocoa::base::id;

let ns_window = window.ns_window().unwrap() as id;
unsafe {
// NSWindowStyleMaskNonactivatingPanel = 1 << 7 = 128
ns_window.setStyleMask_(ns_window.styleMask() | 128);
}
}
```

Requires `cocoa` crate added to `Cargo.toml`.

### 2. Key Simulation (Rust backend)

New file `src-tauri/src/key_simulator.rs`:

- Tauri command: `simulate_key(key_code: String, modifiers: Vec<String>)`
- Presses active modifiers, presses/releases target key, releases modifiers
- Uses `rdev::simulate()` which wraps `CGEventPost` on macOS
- Includes `key_from_string()` — inverse of existing `format_key_code()`

Sequence for clicking "A" with Shift sticky:
```
Shift↓ → A↓ → A↑ → Shift↑
```

Small delay (~20ms) between simulate calls may be needed for macOS to process events reliably.

### 3. Sticky Modifier State (React)

In `Keyboard.tsx`:

- State: `stickyModifiers: Set<string>` tracks active modifiers
- State: `lockedModifiers: Set<string>` tracks double-click-locked modifiers
- Click modifier key → toggle in stickyModifiers
- Double-click modifier → toggle in lockedModifiers
- Click regular key → invoke `simulate_key` with current modifiers → clear stickyModifiers (keep lockedModifiers)
- Visual: `.key-sticky` class for single-click active, `.key-locked` for double-click locked

### 4. Data Flow

```
User clicks key in overlay
→ Key.onClick fires
→ Keyboard.handleKeyClick(keyCode) called
→ Collects stickyModifiers + lockedModifiers into modifiers array
→ invoke("simulate_key", { key_code, modifiers })
→ Rust: rdev::simulate(KeyPress/KeyRelease) sequence
→ macOS CGEvent posted to focused app
→ React: clear stickyModifiers (keep lockedModifiers)
→ Physical keyboard listener picks up the simulated event
→ Key shows pressed animation briefly
```

### 5. Key Mapping

Reuse the same key code strings from `keyboard_listener.rs::format_key_code()`. Add inverse function `key_from_string()` with the same mapping table.

Modifier keys: ShiftLeft, ShiftRight, ControlLeft, Alt (Option), MetaLeft (Command), MetaRight.

## Files to Create/Modify

| File | Change |
|------|--------|
| `src-tauri/src/key_simulator.rs` | New — simulate_key command + string-to-Key mapping |
| `src-tauri/src/lib.rs` | Register simulate_key command, add NSPanel non-activating setup |
| `src-tauri/Cargo.toml` | Add `cocoa` crate |
| `src/components/Keyboard.tsx` | Add sticky modifier state, wire onMouseClick to invoke |
| `src/components/Keyboard.css` | Add .key-sticky and .key-locked visual styles |

## Risks

- **Accessibility permissions**: `rdev::simulate` requires Accessibility access. We already request this for listening, so it should work.
- **NSPanel conversion**: Converting an existing NSWindow to non-activating after creation may have edge cases. Fallback: create the window as NSPanel from the start.
- **Simulate timing**: macOS may need small delays between simulated key events. Start with 0ms, add delay if events are dropped.
- **Focus edge cases**: Some apps may handle synthetic events differently than real keypresses.
Loading