Skip to content

fix: wire the interactive demo's advertised keys to its handler [patch] - #121

Merged
matt-edmondson merged 2 commits into
mainfrom
fix/115-interactive-demo-input
Sep 16, 2026
Merged

matt-edmondson merged 2 commits into
mainfrom
fix/115-interactive-demo-input

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #115

The problem

InteractiveDemo's instructions panel advertised ↑/↓, SPACE, R and T, and every one of them did nothing. UIApplication.ProcessInputAsync offers each key to the root element and nothing else, and no shipped element type overrides UIElementBase.OnHandleInput — it returns false — so InteractiveDemo.HandleInput had exactly one reference in the assembly, its own definition.

Which of the two options this takes

The issue and its triage left a choice open: remove the false claims, or wire input through properly — with the second flagged as exposing "a genuine gap … no way for application code to receive a key at all".

That gap turns out not to exist. UIContainerBase.HandleInput already walks the children and then offers the key to the container itself, and OnHandleInput is protected virtual on a public base class, so an application that wants keys only has to supply an element that handles them. This PR does exactly that and changes nothing in TUI.Core — the real fix, at no library-design cost, so the honest-but-lesser stopgap was not needed.

The demo's root is now an InputRoutingPanel : StackPanel whose OnHandleInput forwards to the demo.

The rest of the change

  • HandleInput takes an InputResult. It switched on a key name string ("UP", "SPACE") that nothing produced; SpectreConsoleProvider.ReadInputAsync emits InputResult.FromKey, so it now switches on ConsoleKey. It returns whether it acted, so an unrecognised key is reported on screen but still counted unhandled.
  • Toggling the theme never worked either. TextStyle.Foreground round-trips through System.Drawing.Color and its getter returns the canonical name, so a style built from "cyan" reads back as "Cyan" and currentStyle.Foreground == "cyan" never matched — T left the counter on one colour however many times it was pressed. This is inside the acceptance criterion ("visibly updates the counter/status/theme"), so it is fixed here rather than deferred; the comparison is now case-insensitive.
  • The instructions panel is rendered from the list the handler is tested against (Controls), so a control cannot be advertised on screen without naming a key something has to act on. That is the recurrence guard for this exact bug.
  • Demo state moves from static fields to instance state, so key handling can be exercised without one test inheriting the counter another left behind. RunAsync is unchanged from Program.cs's point of view.

Tests

TUI.Test now references TUI.App. TUI.App/AssemblyInfo.cs already carried InternalsVisibleTo("ktsu.TUI.Test"); the project reference is what makes that reachable.

TUI.Test/InteractiveDemoInputTests.cs presses keys through demo.RootElement.HandleInput(...) rather than calling the handler directly — the bug was never in the handler, it was that nothing in the element tree reached it, so a test that called HandleInput itself would have passed before and after.

Verified the tests fail without the fix. With the routing reverted (OnHandleInput back to false), 9 of the 11 new tests fail:

failed UpArrowIncrementsTheCounter
failed DownArrowDecrementsTheCounter
failed SpaceAddsTenToTheCounter
failed RResetsTheCounter
failed TTogglesTheCounterColourBackAndForth
failed AnUnadvertisedKeyIsReportedAndLeavesTheCounterAlone
failed RepeatedPressesAccumulate
failed EachDemoInstanceStartsFromAFreshCounter
failed EveryControlTheInstructionsPanelAdvertisesIsHandled
  total: 165   failed: 9   succeeded: 156

The two that still pass are correct to: NonKeyboardInputIsIgnored asserts false either way, and TKeepsTheCounterBold holds trivially when nothing changes.

The theme half was reverted separately (routing restored, comparison back to == CounterColour) to confirm it is covered on its own: total: 165 failed: 1TTogglesTheCounterColourBackAndForth.

Full suite: 165/165 pass (154 before this PR). dotnet build TUI.sln is clean — 0 warnings, 0 errors — across net10.0;net9.0;net8.0.

Not verified here

Driving TUI.App under a pseudo-terminal, for the same reason recorded in #117: SetCursorVisibility issues a cursor-position query (ESC[6n) that nothing in a headless container answers, so the app never gets past hiding the cursor. The acceptance criterion is covered by the tests above, which exercise the same IUIElement.HandleInput path ProcessInputAsync calls.

🤖 Generated with Claude Code

https://claude.ai/code/session_018q8Pwj87WCcqL2HSesWSsg


Generated by Claude Code

matt-edmondson and others added 2 commits September 16, 2026 09:28
The instructions panel advertised five controls, and every one of them did
nothing. UIApplication.ProcessInputAsync offers each key to the root element
only, and no shipped element type overrides UIElementBase.OnHandleInput — it
returns false — so InteractiveDemo.HandleInput was never reached from anywhere
in the assembly.

The demo now supplies a root that does override it. No library change was
needed: UIContainerBase.HandleInput already walks the children before offering
the key to the container, so an application that wants keys only has to provide
an element that handles them.

HandleInput takes an InputResult and switches on ConsoleKey rather than a key
name, which is what the console provider actually produces, and returns whether
it acted so an unrecognised key is still reported as unhandled.

Two supporting changes fell out of it:

- Toggling the theme never worked, because TextStyle.Foreground round-trips
  through System.Drawing.Color and reads back canonicalised, so the `== "cyan"`
  comparison never matched and the counter stayed one colour. The comparison is
  now case-insensitive.
- The instructions panel is rendered from the same control list the handler is
  tested against, so a control cannot be advertised without a key behind it.

Demo state moves from static fields to instance state so the key handling can be
exercised without one test inheriting another's counter.

Fixes #115

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018q8Pwj87WCcqL2HSesWSsg
SonarCloud reported nine MSTEST0046/MSTEST0037 findings on the new test file:
StringAssert.Contains and Assert.AreNotEqual(0, …Count) in place of
Assert.Contains and Assert.IsNotEmpty. The rest of the suite already uses the
newer forms, so this is the file catching up rather than a new convention.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018q8Pwj87WCcqL2HSesWSsg
@sonarqubecloud

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit 85acf99 into main Sep 16, 2026
12 checks passed
@matt-edmondson
matt-edmondson deleted the fix/115-interactive-demo-input branch September 16, 2026 10:45
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.

TUI.App --interactive demo's advertised keyboard controls (arrows/Space/R/T) do nothing — InteractiveDemo.HandleInput is unreachable

1 participant