Skip to content

Show the first key of a chord after a timeout - #44

Merged
doggy8088 merged 1 commit into
keys/keymap-parserfrom
keys/chord-timeout
Oct 9, 2026
Merged

doggy8088 merged 1 commit into
keys/keymap-parserfrom
keys/chord-timeout

Conversation

@doggy8088

Copy link
Copy Markdown
Owner

Closes #9

Summary

A key that could start a multi-key shortcut was held back until the next key arrived, so a single Up arrow was never shown (konami.yml applies to every process and starts with Up) and the first key of real chords (Ctrl+M,B, Ctrl+K,Ctrl+C) stayed invisible until the next press. ShortcutAccumulator.Flush() now completes a pending accumulator through the existing no-match path, and MessageProvider flushes it ChordTimeout (1 s) after the last key, driven by IConcurrencyService.MainThreadScheduler so it is testable with TestScheduler. konami.yml is no longer shipped active; it is an opt-in sample.

Changes

  • src/Carnac.Logic/ShortcutAccumulator.cs: new Flush() (no-op when nothing is buffered or already completed) and IsPendingForOtherProcess(KeyPress).
  • src/Carnac.Logic/MessageProvider.cs: new ctor parameter IConcurrencyService (all ctor arguments are null-checked), public ChordTimeout (1 s). The Scan over the accumulator was replaced by an Observable.Create that keeps the accumulator per subscription (the old Scan seed was shared by all subscribers), schedules the flush after a key leaves the accumulator pending, restarts/cancels it on the next key, forwards OnError/OnCompleted and disposes the timer. State changes and OnNext calls are under a lock (key hook and timer normally share the UI thread; the lock plus a key counter make a late timer callback harmless).
  • A key from a different process while keys are pending now flushes the held keys and is then processed by a new accumulator. Before, ShortcutAccumulator.Add flushed the held keys and silently dropped the new key.
  • src/Carnac/App.xaml.cs: ConcurrencyService, KeyProvider, ShortcutProvider and MessageProvider are created in OnStartup (after the single-instance check) so that one ConcurrencyService is shared by the MessageProvider and the KeysController.
  • src/Carnac.Logic/Keymaps/konami.yml -> Keymaps/samples/konami.yml.sample (git rename, header explains how to opt in); src/Carnac.Logic/Carnac.Logic.csproj lists it as a plain None item (not copied to the output, so neither the build output, the Inno installer's Keymaps\*.yml glob nor Directory.GetFiles("*.yml") see it).
  • src/Carnac.Tests: MessageProviderFacts (new constructor argument), new ChordTimeoutFacts, ShortcutAccumulatorFacts, KeyPresses (test helper), ShortcutProviderFacts (2 konami tests, uses the helper), csproj.

Testing

  • Check-Build.ps1: BUILD OK / TESTS OK (103 tests, 27 new)
  • New tests: ChordTimeoutFacts (single Up shown exactly at the timeout, not before; Up,Up,Up still "up x 3" and nothing left for the timer; chord completed in time resolves to the shortcut and the timer does not fire later; stalled chord shown as the pressed keys; late second key does not complete the chord; each key restarts the timeout; ordinary keys are immediate; chord broken by another key shows all keys immediately; key from another process flushes and is not lost; another-process key can start its own chord; dispose cancels the flush; completion/error forwarded; per-subscription state; constructor null checks), ShortcutAccumulatorFacts (Flush variants, next key after Flush, IsPendingForOtherProcess), ShortcutProviderFacts.konami_keymap_is_not_active_by_default and konami_sample_works_when_it_is_copied_into_the_keymaps_folder.

Not verified / deviations

  • The real UI thread / hook wiring was not run (rules forbid starting Carnac). The reasoning: the low-level hook is installed by KeysController.Start() from OnStartup on the UI thread, so key events and MainThreadScheduler callbacks share a thread; a lock protects the accumulator anyway.
  • The sample is intentionally not copied to the output, so a user of the installer/zip cannot find it locally; the README section of the process-matching issue (Keymap "process:" matching (single, case-sensitive name) and Process Filter documentation/examples #25) points to Keymaps\samples in the repository.
  • Fix beyond the issue text: the dropped key of a different process (see Changes). Behaviour when the key stream completes with keys still pending is unchanged (they are not flushed; the existing does_not_show_key_press_on_partial_match test relies on it).
  • The website (gh-pages) still documents konami.yml; not touched per the rules.
  • Confidence: the implementing agent rated this 88; after an independent build/test run and a read of the production diff the orchestrator rates it 95.

Stack keys - layer 2 of 5. Base: keys/keymap-parser. Author-assessed confidence: 95%.

Copilot AI balanced review requested due to automatic review settings September 28, 2026 23:07
@doggy8088
doggy8088 added this pull request to stack #48 September 28, 2026 23:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟢 Approval recommended

The timeout, cancellation, cross-process behavior, packaging change, and relevant edge cases are consistently implemented and tested.

Review effort: Balanced
Findings: None

What changed in this PR

Adds timeout-based flushing for incomplete keyboard chords so pending keys become visible without breaking completed shortcuts.

Changes:

  • Adds per-subscription chord timeout and cross-process handling.
  • Moves Konami keymap to an opt-in sample.
  • Adds comprehensive accumulator and timeout tests.
File Description
src/​Carnac/​App.xaml.cs Shares the UI scheduler service.
src/​Carnac.Logic/​MessageProvider.cs Implements chord timeout handling.
src/​Carnac.Logic/​ShortcutAccumulator.cs Adds pending-state detection and flushing.
src/​Carnac.Logic/​Carnac.Logic.csproj Stops deploying the Konami keymap.
src/​Carnac.Logic/​Keymaps/​konami.yml Removes the active keymap.
src/​Carnac.Logic/​Keymaps/​samples/​konami.yml.sample Adds an opt-in sample.
src/​Carnac.Tests/​ChordTimeoutFacts.cs Tests timeout and lifecycle behavior.
src/​Carnac.Tests/​ShortcutAccumulatorFacts.cs Tests accumulator flushing.
src/​Carnac.Tests/​ShortcutProviderFacts.cs Tests Konami keymap packaging.
src/​Carnac.Tests/​MessageProviderFacts.cs Supplies the test scheduler dependency.
src/​Carnac.Tests/​KeyPresses.cs Adds a shared key-press fixture helper.
src/​Carnac.Tests/​Carnac.Tests.csproj Includes the new test sources.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@doggy8088
doggy8088 force-pushed the keys/chord-timeout branch 10 times, most recently from 68e00c8 to b5e57de Compare September 29, 2026 00:24
A key that could start a multi-key shortcut was held back until the next
key arrived, so a single Up arrow was never shown (konami.yml applies to
every process and starts with Up) and the first key of real chords such as
Ctrl+M,B or Ctrl+K,Ctrl+C was delayed indefinitely.

Add ShortcutAccumulator.Flush(), which completes a pending accumulator
through the existing no-match path, and let MessageProvider schedule a
flush ChordTimeout (1 s) after a key leaves the accumulator pending. The
timeout runs on IConcurrencyService.MainThreadScheduler (the same thread
the keyboard hook delivers on) and each key restarts it. A key from
another process now flushes the held keys and is processed by a new
accumulator instead of being dropped. The Scan seed shared by all
subscribers is gone: pending state is per subscription.

Stop shipping konami.yml as an active keymap; it becomes the opt-in
Keymaps\samples\konami.yml.sample, which the installer's Keymaps\*.yml
glob does not pick up.

Refs #9

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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.

A single Up-arrow press is never shown: pending multi-key shortcuts are held back forever

2 participants