Skip to content

Conversation

@dannysmith
Copy link
Owner

@dannysmith dannysmith commented Nov 22, 2025

Summary

This PR documents analysis and proposes an implementation plan for improving Handy's keyboard shortcut system.

See the full document: docs/keyboard-shortcuts-plan.md

Problem Statement

Handy's keyboard shortcut system needs improvement to support:

  1. macOS fn/globe key as a trigger (most requested feature)
  2. Cancel recording functionality mid-transcription
  3. Multiple shortcut bindings for different actions (transcribe, transcribe+LLM, translate)
  4. Extensibility via settings JSON for power users

Key Findings

  • PR Support mac Fn shortcut handling cjpais/Handy#136 on upstream proved fn key capture works with Accessibility permission only using objc2-app-kit crates
  • @cjpais wants a unified keyboard handling approach, not multiple systems bolted together
  • Willing to accept JSON-configurable bindings before UI exists

Proposed Approach

  1. Phase 1: Add macOS-specific fn key monitor using objc2-app-kit (supplement, not replace tauri-plugin-global-shortcut)
  2. Phase 2: Add cancel recording shortcut (dynamic registration, Escape default)
  3. Phase 3: Extend settings schema for multiple bindings (JSON-first, minimal UI)
  4. Phase 4: Design unified shortcuts UI

Test Plan

  • Review the analysis document for accuracy
  • Validate the proposed architecture against requirements
  • Confirm approach aligns with upstream maintainer preferences

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • macOS Fn-key support for recording and a “Use fn” action in shortcuts UI.
    • Dynamic Cancel binding: Escape cancels an active recording (discards audio, hides overlays and cleans up the temporary binding).
  • Documentation

    • Added user-facing docs describing Fn-key behaviour, cancel workflow, testing and integration notes.
  • Settings

    • Cancel shortcut defaults updated and marked as runtime-dynamic so it’s active only during recording.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Nov 22, 2025

Walkthrough

Adds macOS Fn-key monitoring, new macOS Cargo deps, routing in the shortcut layer to use an Fn monitor, dynamic runtime registration for a Cancel binding, UI support to set the fn binding, and adjusted cancel flow to discard recordings without triggering transcription.

Changes

Cohort / File(s) Change Summary
macOS Fn Monitor
src-tauri/src/shortcut/fn_monitor.rs
New macOS-only module: main-thread NSEvent FlagsChanged monitor, Accessibility permission checks/prompts, thread-safe registry and lifecycle, and public register_fn_binding / unregister_fn_binding.
Shortcut routing & dispatch
src-tauri/src/shortcut/mod.rs
Integrated fn_monitor, added is_fn_binding() helper, routing for fn vs existing shortcut paths, centralized dispatch_binding_event(), dynamic binding API (register_dynamic_binding / unregister_dynamic_binding), and validation updated to accept macOS-only fn.
Cancel action & actions map
src-tauri/src/actions.rs
Added CancelAction implementing ShortcutAction; start() cancels recording, stop() noop; ACTION_MAP now includes "cancel".
Settings model
src-tauri/src/settings.rs
ShortcutBinding gains pub dynamic: bool with #[serde(default)]; default settings mark the cancel binding as dynamic (registered only during recording).
Frontend UI
src/components/settings/HandyShortcut.tsx
Added isFnBinding() helper, shows "fn (Globe)" when relevant, updates tooltip/title for fn recording behaviour, and adds a macOS-only “Use fn” action to set the binding to fn.
Utilities / cancel behaviour
src-tauri/src/utils.rs
cancel_current_operation updated to discard recording audio first, reset toggles and UI/tray state, and avoid calling action.stop() to prevent transcription.
Type declarations (TS)
src/bindings.ts
ShortcutBinding type extended with optional dynamic?: boolean and documentation describing runtime registration semantics.
Cargo / macOS dependencies
src-tauri/Cargo.toml
Removed rdev; added macOS-targeted deps: objc2, objc2-app-kit (NSEvent), objc2-foundation, and block2.
Docs
docs/fn-key-and-cancel-implementation.md
New documentation: implementation overview, testing checklist, architecture notes, and frontend/back-end coordination points.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Frontend as Settings UI
    participant Shortcut as Shortcut System
    participant FnMonitor as Fn Monitor (macOS)
    participant EventStream as macOS Event Stream

    User->>Frontend: Click "Use fn" button
    Frontend->>Shortcut: register_binding("fn")
    Shortcut->>Shortcut: is_fn_binding() -> true
    Shortcut->>FnMonitor: register_fn_binding(binding_id)
    FnMonitor->>EventStream: Install NSEvent FlagsChanged listener (main thread)
    EventStream-->>FnMonitor: FlagsChanged events (Fn press/release)
    FnMonitor->>Shortcut: dispatch_binding_event(binding_id, "fn", Pressed/Released)
    Shortcut->>Frontend: notify registration success
Loading
sequenceDiagram
    participant User
    participant EventStream as macOS Event Stream
    participant FnMonitor as Fn Monitor
    participant Shortcut as Shortcut System
    participant Cancel as CancelAction
    participant Recording as Recording Session
    participant Overlay as Overlay System

    Note over Recording,User: During active recording (cancel binding registered)
    User->>EventStream: Press Escape (or Fn+Escape)
    EventStream->>FnMonitor: Key / FlagsChanged event
    FnMonitor->>Shortcut: dispatch_binding_event("cancel", "escape" or "fn+escape", Pressed)
    Shortcut->>Cancel: invoke CancelAction.start()
    Cancel->>Recording: cancel recording operation
    Cancel->>Overlay: hide recording overlay
    Cancel->>Shortcut: unregister_dynamic_binding("cancel")
    Shortcut-->>Cancel: acknowledge unregistration
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Pay special attention to:
    • src-tauri/src/shortcut/fn_monitor.rs: Objective‑C interop, main-thread scheduling, Accessibility permission handling and synchronization.
    • src-tauri/src/shortcut/mod.rs: correctness of dispatch_binding_event, dynamic binding lifecycle and edge cases (idempotence, concurrency).
    • src-tauri/src/actions.rs and src-tauri/src/utils.rs: CancelAction semantics and ensuring cancellation does not trigger transcription or leave stale state.
    • src-tauri/Cargo.toml: macOS-targeted dependency declarations and conditional compilation.

Poem

🐰
I hopped onto macOS to find the Fn key,
A tiny globe that whispers, "let it be."
Escape to cancel, tidy and light,
One soft tap and the session's right.
Carrots, code and moonlit night! 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately summarizes the main changes: enabling macOS fn key support and adding Escape-key cancel functionality for non-PTT transcriptions.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch keyboard-shortcuts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
docs/keyboard-shortcuts-plan.md (3)

34-34: Specify language identifier for fenced code blocks.

Both ASCII architecture diagrams lack language identifiers in their code fence declarations. This violates the markdownlint MD040 rule and impacts syntax highlighting and accessibility.

Apply these diffs to resolve the issue:

-```
+```text
 ┌─────────────────────────────────────────────────────────────┐

And for line 92:

-```
+```text
 ┌─────────────────────────────────────────────────────────────┐

Also applies to: 92-92


124-129: Clarify integration between macOS fn key monitor and standard shortcuts handler.

Phase 1 mentions supplementing tauri-plugin-global-shortcut with objc2-app-kit for fn key handling. Consider adding a brief note explaining:

  1. How fn key presses will be distinguished from standard multi-key combinations
  2. How the system prevents duplicate event handling or missed events
  3. Whether the CGEventTap listener will have priority over the tauri plugin or run alongside it

This would help implementation and prevent subtle race conditions during development.


74-87: Consider including macOS permission evolution note.

The permission matrix and research findings are solid. However, given that macOS permission handling evolves with OS versions, consider adding a note about:

  1. Minimum macOS version requirements for the proposed approach
  2. Any known issues with specific macOS versions (Ventura, Sonoma, etc.)
  3. Whether Input Monitoring permission can be avoided entirely (as implied)

This would help future implementers navigate potential compatibility issues.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5085485 and d02a1f6.

📒 Files selected for processing (1)
  • docs/keyboard-shortcuts-plan.md (1 hunks)
🧰 Additional context used
🪛 markdownlint-cli2 (0.18.1)
docs/keyboard-shortcuts-plan.md

34-34: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


92-92: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (2)
docs/keyboard-shortcuts-plan.md (2)

86-86: Verify objc2 crate availability and maintenance status.

The plan relies on using objc2-app-kit crates directly (lines 86, 153). Whilst PR cjpais#136 demonstrated this approach works, you should confirm:

  1. Whether these crates are still actively maintained and have no security issues
  2. Whether they're suitable for long-term maintenance in Handy
  3. Whether there are any breaking changes in newer versions

Consider running a verification check on the crates' current status and maintenance frequency.

Also applies to: 151-153


6-7: Well-researched and comprehensive planning document.

The analysis is thorough, well-structured, and directly addresses the PR objectives. Key strengths:

  • Clear problem statement with four specific, prioritised features
  • Documented research with evidence from upstream PRs
  • Explicit maintainer feedback integration
  • Reasonable phased approach with clear dependencies
  • Justified technical decisions with rationale

The document provides a solid foundation for implementation discussions and reviews.

Also applies to: 15-23

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
docs/keyboard-shortcuts-plan.md (3)

327-336: Resolve Open Questions before beginning Phase 1.

Four Open Questions are listed but remain unanswered. Three should be decided before implementation starts:

  1. fn key in toggle mode (line 331): Restricting fn to push-to-talk only would require architectural changes if decided post-implementation. Recommend deciding before Phase 2.

  2. Permission prompting strategy (line 333): Whether to prompt on first launch or when user attempts fn binding affects Phase 1 groundwork and user onboarding flow.

  3. Secure Input status visibility (line 336): This affects Phase 1 architecture (does fn monitor need built-in status detection?).

Question 1 (Windows/Linux alternatives) can reasonably be deferred to Phase 2+.

Consider resolving these before Phase 1 to avoid rework and ensure coherent permission/UX strategy.


119-133: Consider runtime Secure Input detection as Phase 1 task.

The Secure Input blocking limitation (lines 121–133) is well-documented, and detection via ioreg is proposed. However, the current Implementation Plan does not mention adding runtime detection or user status indication.

If the goal is to minimise user confusion when Handy stops working unexpectedly, consider whether Phase 1 (Foundation) should include basic Secure Input detection infrastructure, or whether this is deferred to Phase 5 (Polish) as a stretch goal.

Current phrasing ("consider status indicator" in line 309) suggests this is stretch. If so, recommend explicitly noting in Phase 1 that runtime detection is omitted and only documented as a limitation.


34-34: Add language identifiers to fenced code blocks.

Markdown code blocks at lines 34, 96, 180, and 211 lack language specifications, which reduces syntax highlighting and readability. Consider:

  • Line 34: Add ```text or ```plaintext for ASCII diagram
  • Line 96: Change ``` to ```bash for the shell command
  • Line 180: Add ```text for ASCII diagram
  • Line 211: Add ```text for file structure listing
  ### 1. Secure Input Blocking
  
  When Secure Input is enabled (password fields, 1Password, Terminal "Secure Keyboard Entry"):
  - **All event monitoring stops receiving events**
  - Users will wonder why Handy stopped working
  - No workaround exists - this is a macOS security feature
  
  **Detection possible via:**
- ```bash
+ ```bash
  ioreg -l -w 0 | grep kCGSSessionSecureInputPID


Also applies to: 96-96, 180-180, 211-211

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used**: CodeRabbit UI

**Review profile**: CHILL

**Plan**: Pro

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between d02a1f69651db841faf14a2a1b7e99d6833afb72 and 41d38263a33d212983b0b131e0c4b79fa1ea677f.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `docs/keyboard-shortcuts-plan.md` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.18.1)</summary>

<details>
<summary>docs/keyboard-shortcuts-plan.md</summary>

34-34: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

---

96-96: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

---

180-180: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

---

211-211: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>docs/keyboard-shortcuts-plan.md (1)</summary><blockquote>

`73-73`: **Removal of `rdev` dependency in Phase 1 is safe to proceed.**

Verification confirms that `rdev` has no active usage in the Rust codebase and is a genuine dangling dependency. Additionally, the proposed objc2 ecosystem versions (0.6.x, objc2-app-kit 0.3.x, objc2-foundation 0.3.x, block2 0.6.x) are all currently available and compatible with the current tauri 2.9.1 environment.

</blockquote></details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment on lines 269 to 276
1. Remove dangling `rdev` dependency from Cargo.toml
2. Add direct dependencies (macOS only):
```toml
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6"
objc2-app-kit = "0.3"
objc2-foundation = "0.3"
block2 = "0.6"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Verify dependency versions and compatibility before Phase 1.

Phase 1 specifies exact versions for the objc2 ecosystem: objc2 0.6, objc2-app-kit 0.3, objc2-foundation 0.3, and block2 0.6. These need verification for:

  1. Availability on crates.io
  2. Compatibility with current Tauri/macOS deployment target
  3. Compatibility with existing transitive dependencies (e.g., from tauri-nspanel)

Additionally, confirm whether objc2 crates are already available as transitive dependencies (line 72) or if direct additions are required.


🏁 Script executed:

#!/bin/bash
# Check if objc2 ecosystem crates are available and compatible
curl -s https://crates.io/api/v1/crates/objc2 | jq '.versions[] | select(.num == "0.6") | .num'
curl -s https://crates.io/api/v1/crates/objc2-app-kit | jq '.versions[] | select(.num == "0.3") | .num'
curl -s https://crates.io/api/v1/crates/block2 | jq '.versions[] | select(.num == "0.6") | .num'

Length of output: 0


🌐 Web query:

What is the latest version of objc2 crate, and is version 0.6 still current as of November 2025?

💡 Result:

The latest released objc2 is 0.6.3 (current as of Nov 22, 2025). Version 0.6.0 (or 0.6) is outdated — the 0.6.x series has newer patch releases (0.6.3). [1][2]

Sources:
[1] docs.rs (objc2 0.6.3).
[2] deps.rs (objc2 0.6.3 metadata).


🌐 Web query:

What is the latest version of objc2-app-kit crate?

💡 Result:

The latest released version is 0.3.2. [1][2]

Sources:
[1] docs.rs — objc2-app-kit (0.3.2).
[2] deps.rs — objc2-app-kit 0.3.2.


I need to verify the remaining dependency versions before finalising the assessment.

Update dependency versions to latest patches in Phase 1 plan.

The specified versions are outdated. objc2-foundation and related crates have newer patch releases available:

  • objc2 0.6 (latest 0.6.3)
  • objc2-app-kit 0.3 (latest 0.3.2)
  • objc2-foundation 0.3 (latest 0.3.1)
  • block2 0.6 (latest 0.6.1)

Update lines 269–276 to use the latest patch versions to ensure compatibility and bug fixes.

🤖 Prompt for AI Agents
In docs/keyboard-shortcuts-plan.md around lines 269 to 276, the Phase 1 plan
lists macOS-only crate versions that are outdated; update the dependency entries
to the latest patch releases by changing objc2 to 0.6.3, objc2-app-kit to 0.3.2,
objc2-foundation to 0.3.1, and block2 to 0.6.1 so the Cargo.toml snippets
reflect the current patch versions for compatibility and bug fixes.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (7)
docs/fn-key-and-cancel-implementation.md (7)

10-19: Clarify scope of "minimal UI changes" relative to "DO NOT TOUCH" warning.

Lines 10–19 emphasise not touching the existing HandyShortcut.tsx, yet lines 57–59 propose adding a "Use fn" button. This creates an apparent contradiction. Whilst adding a single button is indeed minimal, this should be explicitly framed as an exception to the "DO NOT TOUCH" rule rather than a contradiction.

Consider rewording to clarify: "The existing HandyShortcut.tsx must not be restructured or rewritten. A small, isolated 'Use fn' button may be added that calls updateBinding(id, "fn") without modifying the suspend/resume flow."

Also applies to: 57-59


3-3: Minor grammar improvement.

The phrase "how to correctly implement" reads more naturally as "how to implement correctly".


58-58: Add missing comma after closing backtick.

Change "Use fn""Use fn" with appropriate punctuation.


184-184: Rephrase to avoid awkward noun usage.

"The suspend → change_binding → resume flow" reads more clearly as "The suspend–change_binding–resume flow" or "The flow: suspend → change_binding → resume". Avoid using "suspend" as a bare noun in this context.


190-190: Remove word repetition.

"Don't change change_binding behavior" should be "Don't modify change_binding behaviour" or "Don't change the change_binding function's behaviour".


210-210: Specify language for fenced code block.

Line 210 opens a code block without a language identifier. Add the language: ```text or ```plaintext.


221-231: Testing checklist is comprehensive but lacks coverage for edge cases.

The checklist covers the happy path well, but consider adding:

  • Attempt to bind "fn" on non-macOS platforms (should fail validation)
  • Attempt to press Escape when recording is not active (should not trigger cancel)
  • Multiple rapid Escape presses during recording (should handle gracefully)
  • Cancel action during various recording states (initialising, transcribing, error state)
  • Accessibility permission denied (fn monitor should fail gracefully)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 41d3826 and d1300a1.

📒 Files selected for processing (1)
  • docs/fn-key-and-cancel-implementation.md (1 hunks)
🧰 Additional context used
🪛 LanguageTool
docs/fn-key-and-cancel-implementation.md

[style] ~3-~3: Try moving the adverb to make the sentence clearer.
Context: ...el Shortcut This document captures how to correctly implement these two features, based on a failed attempt that broke t...

(SPLIT_INFINITIVE)


[uncategorized] ~58-~58: Possible missing comma found.
Context: ...nge)**: - Add a "Use fn" button that calls updateBinding(id, "fn") - That's i...

(AI_HYDRA_LEO_MISSING_COMMA)


[grammar] ~184-~184: The verb ‘suspend’ does not usually follow articles like ‘The’. Check that ‘suspend’ is spelled correctly; using ‘suspend’ as a noun may be non-standard.
Context: ...## 2. Don't try to "fix" suspend/resume The suspend → change_binding → resume flow has a do...

(A_INFINITIVE)


[duplication] ~190-~190: Possible typo: you repeated a word.
Context: ...sting transcribe binding. ### 4. Don't change change_binding behavior I modified `change_bin...

(ENGLISH_WORD_REPEAT_RULE)


[style] ~194-~194: Consider shortening or rephrasing this to strengthen your wording.
Context: ...preserved. ### 5. Test incrementally I made changes to: - shortcut/mod.rs - shortcut/fn_monito...

(MAKE_CHANGES)

🪛 markdownlint-cli2 (0.18.1)
docs/fn-key-and-cancel-implementation.md

210-210: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (6)
docs/fn-key-and-cancel-implementation.md (6)

63-84: Technical completeness: fn_monitor event handler integration.

The code example shows the monitoring logic but does not clarify:

  • How is the app_handle passed into the closure?
  • How does dispatch_binding_event() route "fn" events to the correct action (transcribe)?
  • Is the handler lifetime managed correctly (stored in app state, or global)?

Consider adding a note that explains the glue code or links to the actual implementation pattern used elsewhere in the codebase.


110-132: Error handling missing from CancelAction.

The CancelAction::start() spawns a background thread to unregister the binding after a 10 ms delay. However:

  • What if unregister_dynamic_binding() fails?
  • What if the user presses Escape again before the 10 ms delay completes?
  • Should there be a flag to prevent double-cancellation?

Add notes on these edge cases or propose defensive logic.


147-171: Clarify integration points for dynamic binding registration.

Lines 147–151 define the public API (register_dynamic_binding, unregister_dynamic_binding), but the implementation is not shown. Lines 163–171 show where these are called in actions.rs, but:

  • What if register_dynamic_binding() fails during transcription start? Should the transcription abort?
  • Should failure be logged?
  • Is there any cleanup required if unregister_dynamic_binding() fails?

Document the expected error semantics or propose error-handling code patterns.


92-98: Main thread requirement not integrated into fn_monitor spec.

The document states (lines 92–98) that the fn monitor "must run on main thread" and shows the pattern app.run_on_main_thread(move || { ... }). However, the fn_monitor code example (lines 63–84) does not show where this wrapping occurs.

Update the fn_monitor code example to show that the entire monitoring setup (lines 63–84) must be wrapped in app.run_on_main_thread() to avoid confusion during implementation.


39-43: Clarify routing logic for fn binding detection.

The proposed routing (line 41–42) delegates to fn_monitor::register_fn_binding() only if binding == "fn". However, it's unclear:

  • Is the check case-sensitive? (The helper uses eq_ignore_ascii_case, line 43, so it should be case-insensitive.)
  • Should validation occur before or after the routing?
  • What happens if someone tries to bind "fn" on non-macOS platforms?

Recommend explicitly stating: "Case-insensitive check on macOS only; on non-macOS, validation should reject 'fn' as invalid." Consider showing the validation logic in the code example.


134-145: Settings schema for dynamic bindings could be more robust.

The cancel binding is marked dynamic: true to skip registration at startup (line 156–161). However:

  • What prevents a user from manually editing the JSON to change dynamic: true to dynamic: false?
  • Should the UI hide dynamic bindings from the user?
  • Is there any validation to ensure only known dynamic bindings (e.g., "cancel") are registered this way?

Consider documenting the intended user-facing behaviour and any validation guards.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
src/components/settings/HandyShortcut.tsx (1)

20-23: Keep fn‑binding detection consistent between frontend and backend

The small isFnBinding helper is fine, but you now have parallel logic here and in src-tauri/src/shortcut/mod.rs::is_fn_binding. To avoid future drift if you ever broaden fn semantics (for example to treat "fn+…" specially), consider at least documenting that these must stay in sync, or centralising the canonical string ("fn") in a shared constant.

src-tauri/src/shortcut/mod.rs (3)

14-41: Binding routing cleanly separates fn bindings from normal shortcuts

The is_fn_binding + register_binding / unregister_binding wrappers give you a neat switch between the fn monitor and the existing global‑shortcut path, and the use of cfg(target_os = "macos") keeps non‑macOS builds free of fn‑specific code. This should make it much easier to evolve fn handling without touching the rest of the shortcut plumbing.

If you ever find yourself cloning ShortcutBinding a lot, you could have these helpers take &ShortcutBinding instead and clone only when necessary, but that’s an optimisation rather than a requirement.


655-674: Consider rejecting fn bindings on non‑macOS at validation time

validate_shortcut_string special‑cases "fn" under cfg(target_os = "macos"), which is good. On other platforms though, "fn" is treated as a non‑modifier key (it’s not in the modifiers list), so validation passes, and the user only sees an error later when the string fails to parse as a shortcut in _register_shortcut.

It would be clearer to users if "fn" were rejected up front on non‑macOS, for example by:

  • Adding "fn" to the modifiers list for non‑macOS and explicitly erroring on modifier‑only combos, or
  • Adding a non‑macOS if is_fn_binding(raw) { return Err("fn key is only supported on macOS".into()); } branch.

That way, people editing JSON or migrating configs don’t get a confusing “cannot parse shortcut” error.


701-747: Unified dispatch helper looks correct; watch settings read cost if this becomes hot

Refactoring the registration closure to delegate to dispatch_binding_event is a nice simplification and ensures fn‑monitor and global‑shortcut events share identical behaviour (push‑to‑talk vs toggle, shared ManagedToggleState, and ACTION_MAP lookup).

dispatch_binding_event calls get_settings(app) on every key event, which (per settings.rs) re‑deserialises the stored settings. That’s probably fine for current usage, but if shortcut activity becomes very frequent you might eventually want to cache just the push_to_talk flag in app state instead of reloading the whole settings object on every press/release.

Also applies to: 779-787

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d1300a1 and df19f05.

⛔ Files ignored due to path filters (1)
  • src-tauri/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • src-tauri/Cargo.toml (1 hunks)
  • src-tauri/src/shortcut/fn_monitor.rs (1 hunks)
  • src-tauri/src/shortcut/mod.rs (8 hunks)
  • src/components/settings/HandyShortcut.tsx (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/components/settings/HandyShortcut.tsx (2)
src-tauri/src/shortcut/mod.rs (2)
  • binding (760-760)
  • binding (799-799)
src/lib/utils/keyboard.ts (1)
  • formatKeyCombination (161-168)
src-tauri/src/shortcut/fn_monitor.rs (1)
src-tauri/src/shortcut/mod.rs (4)
  • app (722-722)
  • binding (760-760)
  • binding (799-799)
  • dispatch_binding_event (703-747)
src-tauri/src/shortcut/mod.rs (2)
src-tauri/src/shortcut/fn_monitor.rs (2)
  • register_fn_binding (107-133)
  • unregister_fn_binding (136-157)
src-tauri/src/settings.rs (2)
  • load_or_create_app_settings (427-455)
  • get_settings (457-473)
🔇 Additional comments (3)
src-tauri/Cargo.toml (1)

87-92: macOS-only Objective‑C bridge dependencies look appropriate

The added objc2 / AppKit / Foundation / block2 dependencies under the macOS target cleanly scope the new fn‑monitor functionality to macOS without affecting other platforms. This wiring matches the new fn_monitor module’s usage and keeps the global dependency surface reasonable. Please just confirm these exact versions and feature flags are what you’ve tested with across your supported macOS versions.

src-tauri/src/shortcut/fn_monitor.rs (1)

1-296: fn/Globe monitor implementation and state handling look robust

The fn monitor module is well‑structured: you gate initialisation on Accessibility permission, install the global NSEvent monitor on the main thread with a clear handshake, and keep runtime state in a shared FnMonitorState while ensuring the mutex is released before dispatching actions. The fn_pressed edge detection and cloning of FnBindingEntrys also avoid redundant dispatches and lock contention. I don’t see any correctness issues in the way you start/stop monitoring or route fn events into dispatch_binding_event.

src/components/settings/HandyShortcut.tsx (1)

293-346: Review comment is incorrect—the code is actually correct for Tauri v2

This codebase uses Tauri v2 (@tauri-apps/plugin-os ~2.3.2), where type() is a synchronous function returning OsType directly. The code at line 46 (const detectedType = type();) is therefore correct and requires no await.

The review comment incorrectly assumes Tauri v1 API, where type() is asynchronous and returns a Promise. In Tauri v2, the function is synchronous, so the switch statement will match properly, osType will be set correctly, and macOS users will see the "Use fn" button as intended.

Likely an incorrect or invalid review comment.

* origin/main:
  cf
  Add wayland paste support using wtype or dotool (cjpais#376)
  feat(audio): async play test sound (cjpais#375)
  feat: Add disabled option for pasting. (cjpais#364)
  WIP: Add tauri-specta for generating TS bindings (cjpais#322)
@dannysmith dannysmith changed the title Keyboard Shortcuts: Analysis & Implementation Plan Enable fn key for transcription on macos and Escape key for cancelling non-PTT transcriptions Nov 28, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src-tauri/src/settings.rs (1)

76-88: Existing users will miss the new “cancel” binding unless settings are migrated

get_default_settings correctly initialises both "transcribe" (dynamic: false) and "cancel" (dynamic: true, Escape) bindings, but load_or_create_app_settings simply deserialises whatever is in settings_store.json. For users who already have a stored bindings map without "cancel", register_dynamic_binding(app, "cancel") will fail with “Dynamic binding 'cancel' not found in settings”, so Escape-to-cancel will never actually register for them.

Consider adding a lightweight migration step after loading settings to ensure new default bindings exist, for example:

 pub fn load_or_create_app_settings(app: &AppHandle) -> AppSettings {
-    let settings = if let Some(settings_value) = store.get("settings") {
+    let mut settings = if let Some(settings_value) = store.get("settings") {
         // Parse the entire settings object
         match serde_json::from_value::<AppSettings>(settings_value) {
             Ok(settings) => {
                 debug!("Found existing settings: {:?}", settings);
                 settings
             }
             Err(e) => {
                 warn!("Failed to parse settings: {}", e);
                 // Fall back to default settings if parsing fails
                 let default_settings = get_default_settings();
                 store.set("settings", serde_json::to_value(&default_settings).unwrap());
                 default_settings
             }
         }
     } else {
         let default_settings = get_default_settings();
         store.set("settings", serde_json::to_value(&default_settings).unwrap());
         default_settings
     };
+
+    // Migration: ensure the new "cancel" binding exists for existing users
+    if !settings.bindings.contains_key("cancel") {
+        let mut defaults = get_default_settings();
+        if let Some(cancel) = defaults.bindings.remove("cancel") {
+            settings.bindings.insert(cancel.id.clone(), cancel);
+            store.set("settings", serde_json::to_value(&settings).unwrap());
+        }
+    }
 
-    settings
+    settings
 }

This way, the Escape-to-cancel feature is available for both new and existing installations without requiring users to reset their settings.

Also applies to: 429-451

src/components/settings/HandyShortcut.tsx (1)

283-291: Avoid using the first binding entry as the “primary” now that dynamic bindings exist

With the new "cancel" binding added in AppSettings.bindings (and marked dynamic: true), relying on Object.values(bindings)[0] / Object.keys(bindings)[0] can end up selecting the cancel binding instead of the main “transcribe” shortcut, as HashMap serialisation order is not guaranteed. That would both expose a dynamic binding in the UI (contrary to the doc comment) and cause the “Use fn” button to target the wrong binding.

It would be safer to pick a non-dynamic binding explicitly, e.g.:

-        const primaryBinding = Object.values(bindings)[0];
-        const primaryId = Object.keys(bindings)[0];
+        const bindingIds = Object.keys(bindings);
+        const primaryId =
+          bindingIds.find((id) => !bindings[id]?.dynamic) ?? bindingIds[0];
+        const primaryBinding = bindings[primaryId];

This keeps the component focused on the user-editable shortcut even as more dynamic bindings are added in future.

Also applies to: 293-339

♻️ Duplicate comments (1)
src-tauri/src/shortcut/mod.rs (1)

64-107: Dynamic binding helpers assume settings contain the binding (see migration note in settings.rs)

register_dynamic_binding / unregister_dynamic_binding correctly enforce binding.dynamic and route through register_binding/unregister_binding, but they depend on the binding id being present in settings.bindings. For existing users whose stored settings don’t yet contain the new "cancel" entry, this will yield a "Dynamic binding 'cancel' not found in settings" error when you try to enable Escape-to-cancel (though you already swallow it at call sites).

The underlying fix is to ensure new default bindings are injected into existing settings on load; see the migration suggestion in src-tauri/src/settings.rs::load_or_create_app_settings.

🧹 Nitpick comments (2)
docs/fn-key-and-cancel-implementation.md (1)

90-100: Add a language to the file-structure code fence

The implementation notes look accurate and very helpful. To satisfy markdownlint (MD040) and improve rendering, consider adding a language to the file-tree code block (for example ```text before src-tauri/src/).

src-tauri/src/actions.rs (1)

265-272: Dynamic cancel action wiring works, but its lifetime in toggle mode is slightly broader than described

The TranscribeAction/CancelAction wiring looks coherent:

  • start: register dynamic "cancel" via register_dynamic_binding on the main thread;
  • stop: unregister it again; and
  • CancelAction::start delegates to cancel_current_operation without touching registration, avoiding the global_shortcut internal lock issue you noted.

One nuance in toggle mode: because CancelAction::start resets all toggle states and cancels recording without ever triggering TranscribeAction::stop for that session, the "cancel" shortcut remains registered until the next full transcribe start/stop cycle. Functionally this mostly just means Escape stays globally bound (calling a largely idempotent cancel path) even when nothing is recording, which is slightly looser than the “only active while recording” wording in the docs.

If you want the implementation to more closely match the documented lifetime in toggle mode as well, you might consider scheduling an unregister_dynamic_binding("cancel") via run_on_main_thread from outside the shortcut callback path (e.g. after cancel_current_operation completes), while still respecting the deadlock constraint.

Also applies to: 283-290, 425-447, 472-483

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 078d50b and f470aac.

📒 Files selected for processing (8)
  • docs/fn-key-and-cancel-implementation.md (1 hunks)
  • src-tauri/src/actions.rs (4 hunks)
  • src-tauri/src/settings.rs (2 hunks)
  • src-tauri/src/shortcut/fn_monitor.rs (1 hunks)
  • src-tauri/src/shortcut/mod.rs (7 hunks)
  • src-tauri/src/utils.rs (1 hunks)
  • src/bindings.ts (1 hunks)
  • src/components/settings/HandyShortcut.tsx (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src-tauri/src/shortcut/fn_monitor.rs
🧰 Additional context used
🧬 Code graph analysis (4)
src-tauri/src/utils.rs (5)
src-tauri/src/actions.rs (5)
  • app (214-214)
  • app (221-221)
  • app (292-292)
  • app (293-293)
  • app (294-294)
src-tauri/src/shortcut/mod.rs (1)
  • app (819-819)
src-tauri/src/commands/audio.rs (2)
  • app (51-51)
  • app (102-102)
src-tauri/src/tray.rs (3)
  • app (58-58)
  • app (134-134)
  • change_tray_icon (57-74)
src-tauri/src/overlay.rs (1)
  • hide_recording_overlay (234-247)
src/components/settings/HandyShortcut.tsx (2)
src-tauri/src/shortcut/mod.rs (2)
  • binding (856-856)
  • binding (899-899)
src/lib/utils/keyboard.ts (1)
  • formatKeyCombination (161-168)
src-tauri/src/actions.rs (2)
src-tauri/src/shortcut/mod.rs (3)
  • app (819-819)
  • register_dynamic_binding (72-94)
  • unregister_dynamic_binding (97-107)
src-tauri/src/utils.rs (3)
  • app (25-25)
  • app (34-34)
  • cancel_current_operation (18-48)
src-tauri/src/shortcut/mod.rs (4)
src-tauri/src/utils.rs (2)
  • app (25-25)
  • app (34-34)
src/bindings.ts (1)
  • ShortcutBinding (613-619)
src-tauri/src/shortcut/fn_monitor.rs (2)
  • register_fn_binding (105-131)
  • unregister_fn_binding (134-155)
src-tauri/src/settings.rs (2)
  • load_or_create_app_settings (512-540)
  • get_settings (542-558)
🪛 markdownlint-cli2 (0.18.1)
docs/fn-key-and-cancel-implementation.md

92-92: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (5)
src/bindings.ts (1)

613-619: Dynamic binding flag shape looks sound

The dynamic?: boolean flag and its documentation align with the Rust ShortcutBinding (#[serde(default)] pub dynamic: bool), so older settings remain valid (field simply absent / false) while still allowing dynamic bindings to be modelled explicitly. No changes needed here.

src-tauri/src/utils.rs (1)

14-47: Cancellation flow correctly discards recording and resets UI

The refactored cancel_current_operation cleanly cancels audio, removes mute, resets all toggle states without calling action.stop(), and normalises overlay/tray state to Idle. This matches the “cancel without transcribing” requirement and avoids lock re-entrancy issues given the updated dispatch_binding_event logic.

src-tauri/src/shortcut/mod.rs (2)

789-847: Centralised dispatch logic is robust and avoids lock re-entrancy issues

dispatch_binding_event unifies handling of all shortcuts (including fn-monitor-driven ones) and:

  • respects push_to_talk vs toggle mode;
  • uses ManagedToggleState only in toggle mode; and
  • crucially, releases the toggle-state mutex before invoking action.start/stop, which is important now that CancelAction’s cancel_current_operation also needs to acquire that lock.

This significantly reduces the chance of deadlocks and keeps behaviour consistent across global_shortcut and fn-monitor paths.


18-21: fn-specific routing and validation look correct

The is_fn_binding helper plus register_binding/unregister_binding routing, combined with the "fn" special case in validate_shortcut_string, give a clear macOS-only path for fn/globe bindings while keeping the existing validation intact for all other platforms. _register_shortcut delegating to dispatch_binding_event in its callback means fn-monitor and regular shortcuts share the same action semantics. This all looks coherent.

Also applies to: 737-760, 849-895

src/components/settings/HandyShortcut.tsx (1)

42-71: The review comment is factually incorrect — type() is synchronous, not async.

According to the official Tauri v2 documentation, type() from @tauri-apps/plugin-os has the signature function type(): OsType and returns a string directly, not a Promise. The code at line 46 correctly calls const detectedType = type(); without await, and the switch statement will properly evaluate the OS type. osType will be set correctly based on the actual operating system, and the macOS-specific UI will activate as intended.

The suggested diff to add await is unnecessary and incorrect.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src-tauri/src/shortcut/mod.rs (1)

64-107: Verify API visibility for dynamic binding functions.

The idempotent registration pattern (unregister-then-register) is well-designed for avoiding conflicts and deadlocks. However, these functions are marked pub, making them part of the crate's public API.

If these functions are only intended for internal use (called from within the same crate), consider restricting visibility to pub(crate) to prevent external misuse.

-pub fn register_dynamic_binding(app: &AppHandle, binding_id: &str) -> Result<(), String> {
+pub(crate) fn register_dynamic_binding(app: &AppHandle, binding_id: &str) -> Result<(), String> {
-pub fn unregister_dynamic_binding(app: &AppHandle, binding_id: &str) -> Result<(), String> {
+pub(crate) fn unregister_dynamic_binding(app: &AppHandle, binding_id: &str) -> Result<(), String> {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f470aac and 536f2c1.

📒 Files selected for processing (1)
  • src-tauri/src/shortcut/mod.rs (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src-tauri/src/shortcut/mod.rs (3)
src-tauri/src/actions.rs (5)
  • app (214-214)
  • app (221-221)
  • app (292-292)
  • app (293-293)
  • app (294-294)
src-tauri/src/shortcut/fn_monitor.rs (2)
  • register_fn_binding (105-131)
  • unregister_fn_binding (134-155)
src-tauri/src/settings.rs (2)
  • load_or_create_app_settings (512-540)
  • get_settings (542-558)
🔇 Additional comments (7)
src-tauri/src/shortcut/mod.rs (7)

18-46: LGTM! Clean routing abstraction for platform-specific bindings.

The helper functions correctly route fn-key bindings to the macOS-specific monitor and delegate other shortcuts to the standard registration path. The platform-conditional compilation is appropriate.


48-62: LGTM! Dynamic binding lifecycle handled correctly.

Skipping dynamic bindings during initialization is appropriate, as they're intended to be registered/unregistered at runtime based on application state.


116-180: LGTM! Consistent use of routing helpers.

The function correctly uses the new register_binding and unregister_binding helpers, and appropriately downgrades the log level for expected unregister failures.


737-762: LGTM! Platform-appropriate validation for fn key.

The special case handling for the macOS fn key is correctly implemented with appropriate platform guards and clear error messages for unsupported platforms.


764-789: LGTM! Routing helpers applied consistently.

Both functions correctly use the new routing helpers to handle platform-specific binding types.


791-849: Well-designed dispatch with explicit deadlock avoidance.

The central dispatch function correctly handles both PTT and toggle modes. The pattern of acquiring the lock, determining the action, releasing the lock, and then calling the action (lines 816-840) effectively prevents deadlocks.

Note: Line 824 uses .expect() on lock acquisition, which will panic if the lock is poisoned. This is typically acceptable for critical state that cannot be recovered, but ensure this aligns with the application's error handling strategy.


851-919: LGTM! Centralized dispatch improves maintainability.

The refactoring to delegate to dispatch_binding_event eliminates duplication and ensures consistent handling of both PTT and toggle modes across all binding types. The improved error message formatting and debug logging are helpful additions.

Resolved conflicts:
- actions.rs: Keep dynamic binding approach, add recording_started check from main
- shortcut/mod.rs: Keep dispatch_binding_event, add Linux guards from PR cjpais#392
- utils.rs: Keep proper cancel_current_operation ordering (cancel first)
- HandyShortcut.tsx: Keep fn key UI support
- settings.rs: Remove duplicate cancel binding from main's PR cjpais#224

Added Linux guards to register_dynamic_binding() and unregister_dynamic_binding()
to match PR cjpais#392's stability fix for Linux.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src-tauri/src/shortcut/mod.rs (1)

176-189: Consider generalising dynamic binding handling.

The special case for the "cancel" binding works, but it hardcodes the binding ID. Consider generalising this to check the dynamic flag instead:

-    // If this is the cancel binding, just update the settings and return
-    // It's managed dynamically, so we don't register/unregister here
-    if id == "cancel" {
+    // If this is a dynamic binding, just update the settings and return
+    // Dynamic bindings are managed at runtime, so we don't register/unregister here
+    if binding_to_modify.dynamic {
         if let Some(mut b) = settings.bindings.get(&id).cloned() {
             b.current_binding = binding;
             settings.bindings.insert(id.clone(), b.clone());

This makes the code more maintainable if additional dynamic bindings are added in the future.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 536f2c1 and a8a80af.

⛔ Files ignored due to path filters (1)
  • src-tauri/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • docs/fn-key-and-cancel-implementation.md (1 hunks)
  • src-tauri/Cargo.toml (1 hunks)
  • src-tauri/src/actions.rs (3 hunks)
  • src-tauri/src/settings.rs (2 hunks)
  • src-tauri/src/shortcut/mod.rs (6 hunks)
  • src-tauri/src/utils.rs (1 hunks)
  • src/bindings.ts (1 hunks)
  • src/components/settings/HandyShortcut.tsx (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/bindings.ts
  • src/components/settings/HandyShortcut.tsx
  • src-tauri/src/actions.rs
  • src-tauri/Cargo.toml
🧰 Additional context used
🧬 Code graph analysis (1)
src-tauri/src/utils.rs (3)
src-tauri/src/tray.rs (3)
  • app (59-59)
  • app (137-137)
  • change_tray_icon (58-75)
src-tauri/src/commands/audio.rs (2)
  • app (51-51)
  • app (102-102)
src-tauri/src/overlay.rs (1)
  • hide_recording_overlay (269-282)
🪛 GitHub Actions: prettier
docs/fn-key-and-cancel-implementation.md

[warning] 1-1: Prettier detected code style issues. Run 'prettier --write' to fix.

🪛 markdownlint-cli2 (0.18.1)
docs/fn-key-and-cancel-implementation.md

92-92: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (11)
docs/fn-key-and-cancel-implementation.md (1)

1-133: Excellent documentation.

The documentation is comprehensive and well-structured. It clearly explains the implementation approach, file structure, design decisions, and testing requirements. The references to relevant PRs and Linux limitations are particularly helpful.

Note: Please run Prettier to address the formatting issue flagged in the pipeline:

prettier --write docs/fn-key-and-cancel-implementation.md
src-tauri/src/utils.rs (1)

14-48: Well-structured cancellation flow.

The cancellation logic is correctly sequenced:

  1. Audio recording is cancelled first (preventing data loss)
  2. Mute is removed (restoring audio state)
  3. Toggle states are reset without triggering transcription
  4. UI state is cleaned up (overlay and tray icon)

The documentation clearly explains why action.stop() is not called, preventing unintended transcription. The explicit ordering comments are helpful for future maintainers.

src-tauri/src/settings.rs (2)

83-87: Clean addition of dynamic binding support.

The dynamic field is well-documented and uses #[serde(default)] to ensure backward compatibility with existing settings files. This allows for runtime registration/unregistration of bindings like the cancel shortcut.


447-457: The concern about case sensitivity is not substantiated by the codebase or available documentation.

The "Escape" key capitalization is consistent with the tauri-plugin-global-shortcut library's key naming conventions (Code::Escape enum variant). The validation function only normalizes modifier keys (ctrl, shift, alt, etc.) to lowercase; it does not validate or transform the key name itself. Web documentation for tauri-plugin-global-shortcut shows the library accepts multiple forms (both "escape" and "esc"), suggesting case-insensitive parsing. No errors, warnings, or compatibility issues related to this capitalization exist in the codebase. The dynamic binding is correctly marked and only registered during recording.

src-tauri/src/shortcut/mod.rs (7)

15-46: Clean routing for macOS fn key support.

The conditional compilation and routing logic for macOS fn key bindings is well-implemented:

  • Case-insensitive matching for "fn" bindings
  • Proper use of #[cfg(target_os = "macos")] guards
  • Clear separation between fn_monitor and regular shortcut paths

48-70: Correct handling of dynamic bindings during initialisation.

The updated init_shortcuts correctly skips dynamic bindings (like cancel), which are registered at runtime when needed. The debug logging provides good observability.


83-119: Well-designed idempotent registration for dynamic bindings.

The register_dynamic_binding function handles several important concerns:

  • Idempotent behavior (unregister before register) prevents deadlock when called from shortcut callbacks
  • Linux disabling is documented and intentional (PR cjpais#392)
  • Proper validation that the binding is marked as dynamic
  • Good error messages for troubleshooting

The design avoids the need for callers to explicitly unregister, simplifying the API and preventing common pitfalls.


125-145: LGTM on unregister_dynamic_binding.

The function mirrors register_dynamic_binding with consistent Linux disabling and error handling.


807-832: Correct validation for macOS-specific fn key.

The validate_shortcut_string function properly validates that the "fn" binding is only used on macOS, with a clear error message for other platforms.


861-919: Excellent deadlock avoidance in dispatch logic.

The dispatch_binding_event function handles a critical concurrency concern correctly:

In toggle mode (lines 886-903), the function:

  1. Acquires the toggle state lock
  2. Determines which action to perform
  3. Releases the lock before calling the action

This is essential because actions like cancel_current_operation need to acquire the same lock. The explicit comment explaining this design decision is valuable for maintainers.

The unified dispatch point for both global-shortcut and fn_monitor paths is clean architecture.


921-968: Clean refactoring to use centralised dispatch.

The refactoring of _register_shortcut to use the centralised dispatch_binding_event function eliminates code duplication and ensures consistent behavior across both shortcut systems.

Comment on lines +92 to +100
```
src-tauri/src/
├── shortcut/
│ ├── mod.rs # Shortcut logic, routing, dispatch_binding_event
│ └── fn_monitor.rs # macOS-only fn key monitoring
├── actions.rs # CancelAction, TranscribeAction with cancel registration
├── settings.rs # ShortcutBinding.dynamic field, cancel binding
└── utils.rs # cancel_current_operation()
```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add language identifier to fenced code block.

The fenced code block at line 92 is missing a language identifier, which reduces readability and prevents syntax highlighting.

Apply this diff to add the language identifier:

-```
+```text
 src-tauri/src/
 ├── shortcut/
 │   ├── mod.rs           # Shortcut logic, routing, dispatch_binding_event
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

92-92: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In docs/fn-key-and-cancel-implementation.md around lines 92 to 100, the fenced
code block lacks a language identifier; update the opening fence from ``` to
```text so the block becomes ```text ... ``` (i.e., add the language identifier
"text" to the existing fenced code block) to enable proper syntax highlighting
and readability.

@dannysmith dannysmith closed this Dec 1, 2025
@dannysmith dannysmith deleted the keyboard-shortcuts branch December 4, 2025 03:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants