Skip to content

Make Escape cancel active recordings globally - #5

Open
amrshawqy wants to merge 1 commit into
bedriyan:mainfrom
amrshawqy:fix/global-escape-cancel
Open

Make Escape cancel active recordings globally#5
amrshawqy wants to merge 1 commit into
bedriyan:mainfrom
amrshawqy:fix/global-escape-cancel

Conversation

@amrshawqy

Copy link
Copy Markdown

Why

Speaky's recording shortcut already works globally, but Escape cancellation did not follow the same model. Once focus moved to another application, Escape could be missed because the existing implementation used:

  • a HID-level event tap, which ordinary non-root applications cannot install reliably; and
  • a listen-only tap, which can observe events but cannot consume them.

That made cancellation dependent on which application was focused. It also required a second Escape press, which is slower than the conventional “Escape cancels the active operation” behavior.

This PR establishes a simple contract:

Speaky state Escape behavior
Recording Cancel immediately, from any focused application, and consume the key
Idle, transcribing, or error Leave Escape completely untouched

What changed

Reliable global Escape handling

  • Uses an active .cgSessionEventTap with .defaultTap.
  • Enables the tap only while RecordingState.recording is active.
  • Returns nil for Escape key-down so the focused application does not also act on it.
  • Disables capture immediately after accepting Escape, ensuring one cancellation callback.
  • Re-enables a tap disabled by timeout or user input only when recording is still active.
  • Retries tap creation when recording starts, covering Accessibility permission granted after launch.
  • Keeps a consuming local monitor for when Speaky itself is focused.
  • Treats the passive global monitor as best-effort only and surfaces an actionable warning when exclusive global cancellation is unavailable.

Correct shortcut lifecycle

Escape now clears hands-free and active-key state, cancels pending Fn debounce work, and suppresses the matching physical key release. This prevents a held push-to-talk shortcut from restarting recording immediately after Escape cancels it.

Cancellation-safe start feedback

Rapid start → Escape → restart testing exposed an existing lifecycle race: the first recording's two-second start-sound task could outlive cancellation and mute the second recording.

The coordinator now owns exactly one start-feedback task:

  • previous feedback is cancelled before each start;
  • normal stop and Escape cancellation both cancel feedback;
  • obsolete start audio is stopped safely;
  • cancellation is no longer swallowed while waiting for playback;
  • the system is muted only if the same recording is still active.

This keeps the Escape workflow deterministic and prevents stale tasks from changing a later recording's audio state.

Clear permission guidance

Settings, onboarding, launch warnings, and the README now explain that Accessibility permission supports both auto-paste and reliable global Escape cancellation. The README also includes a focused manual verification checklist because CI cannot grant macOS Accessibility permission.

Tests and validation

  • Full macOS arm64 integration suite: 44 passed, 0 failed, 0 skipped.
  • Added dedicated tests for:
    • Escape interception only during recording;
    • unrelated key events passing through;
    • one-shot cancellation;
    • held-shortcut release suppression;
    • cancellation and immediate restart without stale muting;
    • sound/mute behavior across all background-audio modes.
  • Async test checkpoints fail deterministically instead of timing out silently.
  • Optimized arm64 Release build completed successfully.
  • Swift syntax validation and git diff --check passed.
  • The implementation received an independent read-only review, was updated for every finding, and passed a second independent verification with no remaining correctness, concurrency, cleanup, scope, or minimality findings.

Validation was also performed with the pending Dock and stable-build work from #3 and #4 to catch integration conflicts. This PR remains independently scoped and does not include either feature. The pinned dependency configuration from #4 was used because current main follows FluidAudio's moving branch; this PR does not change dependencies.

Manual verification

  1. Grant Accessibility permission to the exact Speaky build.
  2. Start recording and focus another application.
  3. Press Escape once and confirm recording is cancelled.
  4. Confirm the focused application does not also receive Escape.
  5. Confirm Escape behaves normally while Speaky is not recording.
  6. Hold the recording shortcut, press Escape, release the shortcut, and confirm recording does not restart.
  7. Start, cancel with Escape, and immediately start again; confirm the start sound plays and the new recording is muted only after its own sound completes.

Scope

The PR does not change transcription, paste behavior, recording shortcuts, model handling, or release packaging. It is limited to Escape cancellation, the directly related recording-feedback lifecycle, permission messaging, and regression coverage.

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.

1 participant