Skip to content

Cleared the terminal and aborted the interactive form cleanly on Ctrl-C.#60

Merged
AlexSkrypnyk merged 2 commits into
mainfrom
feature/ctrlc-clear
Jul 16, 2026
Merged

Cleared the terminal and aborted the interactive form cleanly on Ctrl-C.#60
AlexSkrypnyk merged 2 commits into
mainfrom
feature/ctrlc-clear

Conversation

@AlexSkrypnyk

@AlexSkrypnyk AlexSkrypnyk commented Jul 16, 2026

Copy link
Copy Markdown
Member

Summary

Pressing Ctrl-C partway through the interactive panel TUI used to leave a broken terminal - the alternate screen stayed up, the cursor stayed hidden, the shell stayed in raw mode - and, once that was fixed, the collected answers were still printed as if the form had been submitted. This makes Ctrl-C a clean abort end to end: the screen is cleared and the terminal restored, and the abort is surfaced to the caller as a distinct InterruptException so partial answers are never mistaken for a completed form.

Previously Terminal::setup() left isig enabled, so Ctrl-C raised SIGINT and killed the process before the try/finally teardown could run. Now raw mode disables isig, Ctrl-C arrives as byte 0x03, and it is parsed as a first-class KeyName::Interrupt.

Changes

Interrupt handling (core)

  • src/Render/Terminal.php - setup() adds -isig so Ctrl-C is delivered as input rather than a signal; stty sane in restore() re-enables it on exit.
  • src/Input/KeyName.php, src/Input/KeyParser.php, src/Testing/KeyEncoder.php - a new KeyName::Interrupt case, with 0x03 parsed to it and the encoder round-trip kept complete.
  • src/Render/PanelController.php - a shared consumeInterrupt() catches the interrupt above the field/widget handlers (so it aborts from anywhere, including mid-widget and at the start banner), breaks the read loop, and always restores and clears the screen even when clearOnExit is off. Adds an isInterrupted() accessor.
  • src/Testing/TuiTester.php - exposes isInterrupted() for the harness.
  • src/Theme/DefaultTheme.php, translations/tui.php - the interrupt renders as ctrl-c in key hints, with its catalog entry.

Abort signalling (facade + demos)

  • src/InterruptException.php - a new exception representing a user abort.
  • src/Tui.php - run() and interact() throw InterruptException when the run was interrupted, instead of returning the partial answers; docblocks document both this and the existing EngineException.
  • playground/* - every interactive demo catches InterruptException inline and exits 130 (the conventional SIGINT code): a try/catch sibling for the numbered demos, a wrap around the echo for the widget one-liners. Each script stays fully self-contained - it requires the autoloader directly, with no shared bootstrap or helper. The headless demos are untouched.

Docs and conventions

  • docs/content/installation.mdx - an "Aborting with Ctrl-C" section showing the catch (InterruptException) pattern; docs/cspell.json gains Ctrl and SIGINT.
  • AGENTS.md - records that each playground script must be self-contained.

Tests

  • Parser, encoder round-trip, key-hint, controller (interrupt stops the loop before later keys, forces the clear when clearOnExit is off, and aborts at the banner), harness reporting, and a facade test asserting interact() throws on Ctrl-C. Changed classes stay fully covered.

Screenshots

N/A - headless library change, nothing rendered.

Before / After

Ctrl-C mid-capture

  BEFORE                                   AFTER
  ┌───────────────────────────┐           ┌───────────────────────────┐
  │ isig on → Ctrl-C = SIGINT │           │ -isig → Ctrl-C = 0x03 key │
  │            │              │           │            │              │
  │   default handler kills   │           │  parsed as Interrupt      │
  │            │              │           │            │              │
  │  try/finally NEVER runs   │           │  run loop breaks          │
  │            │              │           │            │              │
  │  alt screen + raw mode    │           │  finally: restore + clear │
  │  left on the terminal     │           │            │              │
  │            │              │           │  facade throws            │
  │  (once cleaned up:)       │           │  InterruptException       │
  │  answers PRINTED as a     │           │            │              │
  │  completed submission     │           │  caller aborts (exit 130) │
  └───────────────────────────┘           └───────────────────────────┘
        broken / misleading                      clean abort

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Ctrl-C is parsed as a named interrupt, handled across the panel loop and banner stage, surfaced through PanelController and TuiTester, and converted to InterruptException. Terminal setup, cleanup, playground bootstrapping, hints, translations, encoding, and tests were updated.

Changes

Ctrl-C interrupt flow

Layer / File(s) Summary
Interrupt input contract and hints
src/Input/*, src/Render/Terminal.php, src/Testing/KeyEncoder.php, src/Theme/DefaultTheme.php, translations/tui.php, tests/phpunit/Unit/Input/*, tests/phpunit/Unit/Theme/*
Adds KeyName::Interrupt, parses and encodes Ctrl-C as \x03, disables terminal signal handling, and adds translated hint coverage.
Panel interrupt lifecycle
src/Render/PanelController.php, tests/phpunit/Unit/Render/PanelControllerTest.php
Stops processing after Ctrl-C during banner or form input, exposes interruption state, restores the terminal, and clears the screen on interruption.
Facade interrupt exception
src/InterruptException.php, src/Tui.php, tests/phpunit/Unit/TuiTest.php
Raises InterruptException when interaction is interrupted and documents the new throwable behavior.
Tester interrupt reporting
src/Testing/TuiTester.php, tests/phpunit/Unit/Testing/TuiTesterTest.php
Propagates controller interruption state through TuiTester::isInterrupted() and validates access before and after execution.
Playground bootstrap
playground/bootstrap.php, playground/*/run.php, playground/3-widgets/*
Routes playground scripts through a shared bootstrap that handles interrupt exceptions with exit status 130.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Terminal
  participant KeyParser
  participant PanelController
  participant Tui
  participant PlaygroundBootstrap
  Terminal->>KeyParser: provide Ctrl-C byte
  KeyParser->>PanelController: return KeyName::Interrupt
  PanelController->>Tui: expose interrupted result
  Tui->>Tui: throw InterruptException
  PlaygroundBootstrap->>PlaygroundBootstrap: handle exception and exit 130
Loading

Possibly related PRs

  • drevops/tui#42: Both changes modify scripted interactive-loop termination and testing behavior.
  • drevops/tui#56: Both changes modify PanelController teardown and terminal-clearing behavior.

Suggested labels: A3

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main Ctrl-C interrupt handling and terminal cleanup changes.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/ctrlc-clear

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

@github-actions

This comment has been minimized.

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.49%. Comparing base (8e365ff) to head (1905d47).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #60      +/-   ##
==========================================
+ Coverage   97.48%   97.49%   +0.01%     
==========================================
  Files          97       97              
  Lines        2858     2876      +18     
==========================================
+ Hits         2786     2804      +18     
  Misses         72       72              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@github-actions

This comment has been minimized.

Raw mode now disables 'isig', so Ctrl-C is delivered as byte 0x03 and parsed as a new 'KeyName::Interrupt' rather than raising SIGINT, which previously killed the process before the teardown ran and left the alternate screen and raw mode behind. The panel loop catches it above the field handlers - so it aborts from anywhere, including mid-widget and at the start banner - through a shared 'consumeInterrupt()' helper, then always restores and clears the screen, even when 'clearOnExit' is off. 'isInterrupted()' is exposed on the controller and the 'TuiTester' harness, and the interrupt renders as 'ctrl-c' in key hints.
@AlexSkrypnyk
AlexSkrypnyk force-pushed the feature/ctrlc-clear branch from c59a3c9 to f729aa7 Compare July 16, 2026 06:27
@github-actions

This comment has been minimized.

…or a submit.

'Tui::run()' and 'interact()' now throw a new 'InterruptException' when the user aborts the interactive session with Ctrl-C, rather than returning the partial answers a caller would then print as a completed form. Each interactive playground demo catches it inline and exits 130, so every script stays self-contained with no shared bootstrap. The abort is documented in the installation guide, and the self-contained-scripts rule is recorded in 'AGENTS.md'.
@AlexSkrypnyk
AlexSkrypnyk force-pushed the feature/ctrlc-clear branch from f729aa7 to 1905d47 Compare July 16, 2026 06:50
@github-actions

Copy link
Copy Markdown
Code Coverage Report:
  2026-07-16 06:51:53

 Summary:
  Classes: 83.51% (81/97)
  Methods: 95.67% (685/716)
  Lines:   97.50% (2804/2876)

DrevOps\Tui\Answers\Answer
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  1/  1)
DrevOps\Tui\Answers\Answers
  Methods: 100.00% ( 9/ 9)   Lines: 100.00% ( 20/ 20)
DrevOps\Tui\Answers\Provenance
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  7/  7)
DrevOps\Tui\Answers\SummaryFormatter
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 24/ 24)
DrevOps\Tui\Builder\FieldBuilder
  Methods: 100.00% (38/38)   Lines: 100.00% (135/135)
DrevOps\Tui\Builder\Form
  Methods:  90.91% (10/11)   Lines:  98.18% ( 54/ 55)
DrevOps\Tui\Builder\PanelBuilder
  Methods: 100.00% (21/21)   Lines: 100.00% ( 33/ 33)
DrevOps\Tui\Condition\CompositeCondition
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 19/ 19)
DrevOps\Tui\Condition\Condition
  Methods: 100.00% (10/10)   Lines: 100.00% ( 38/ 38)
DrevOps\Tui\Derive\Derive
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 14/ 14)
DrevOps\Tui\Derive\Deriver
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% ( 13/ 13)
DrevOps\Tui\Derive\Transform
  Methods: 100.00% ( 3/ 3)   Lines: 100.00% (  7/  7)
DrevOps\Tui\Discovery\AbstractDiscover
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  1/  1)
DrevOps\Tui\Discovery\Dotenv
  Methods: 100.00% ( 3/ 3)   Lines: 100.00% ( 19/ 19)
DrevOps\Tui\Discovery\JsonValue
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 16/ 16)
DrevOps\Tui\Discovery\PathExists
  Methods: 100.00% ( 3/ 3)   Lines: 100.00% (  3/  3)
DrevOps\Tui\Discovery\Scan
  Methods: 100.00% ( 3/ 3)   Lines: 100.00% ( 20/ 20)
DrevOps\Tui\Engine\Engine
  Methods: 100.00% (15/15)   Lines: 100.00% (103/103)
DrevOps\Tui\Handler\HandlerRegistry
  Methods:  85.71% ( 6/ 7)   Lines:  95.45% ( 21/ 22)
DrevOps\Tui\Input\Binding
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  1/  1)
DrevOps\Tui\Input\DefaultKeyMap
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% ( 38/ 38)
DrevOps\Tui\Input\Hint
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  2/  2)
DrevOps\Tui\Input\Key
  Methods:  87.50% ( 7/ 8)   Lines:  63.64% (  7/ 11)
DrevOps\Tui\Input\KeyMap
  Methods: 100.00% (11/11)   Lines: 100.00% ( 61/ 61)
DrevOps\Tui\Input\KeyMapManager
  Methods: 100.00% ( 2/ 2)   Lines: 100.00% (  8/  8)
DrevOps\Tui\Input\KeyParser
  Methods:  85.71% ( 6/ 7)   Lines:  98.95% ( 94/ 95)
DrevOps\Tui\Input\Scope
  Methods: 100.00% ( 7/ 7)   Lines: 100.00% ( 11/ 11)
DrevOps\Tui\Input\ScopedKeyMap
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% (  4/  4)
DrevOps\Tui\Input\VimKeyMap
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% ( 12/ 12)
DrevOps\Tui\Model\DateBounds
  Methods: 100.00% ( 6/ 6)   Lines: 100.00% ( 28/ 28)
DrevOps\Tui\Model\Field
  Methods:  77.78% ( 7/ 9)   Lines:  87.69% ( 57/ 65)
DrevOps\Tui\Model\FieldType
  Methods:  50.00% ( 3/ 6)   Lines:  26.32% ( 10/ 38)
DrevOps\Tui\Model\FormDefinition
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 12/ 12)
DrevOps\Tui\Model\NumberBounds
  Methods:  83.33% ( 5/ 6)   Lines:  95.24% ( 20/ 21)
DrevOps\Tui\Model\Option
  Methods: 100.00% ( 3/ 3)   Lines: 100.00% (  9/  9)
DrevOps\Tui\Model\Panel
  Methods: 100.00% ( 2/ 2)   Lines: 100.00% (  2/  2)
DrevOps\Tui\Model\Weekday
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 15/ 15)
DrevOps\Tui\Render\Ansi
  Methods: 100.00% ( 5/ 5)   Lines: 100.00% ( 13/ 13)
DrevOps\Tui\Render\Box
  Methods: 100.00% ( 3/ 3)   Lines: 100.00% ( 15/ 15)
DrevOps\Tui\Render\ExternalEditor
  Methods: 100.00% ( 6/ 6)   Lines: 100.00% ( 27/ 27)
DrevOps\Tui\Render\HelpSection
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  1/  1)
DrevOps\Tui\Render\Navigator
  Methods: 100.00% ( 6/ 6)   Lines: 100.00% ( 15/ 15)
DrevOps\Tui\Render\PanelController
  Methods: 100.00% (27/27)   Lines: 100.00% (160/160)
DrevOps\Tui\Render\Scroller
  Methods: 100.00% ( 5/ 5)   Lines: 100.00% ( 15/ 15)
DrevOps\Tui\Render\Terminal
  Methods:  92.86% (13/14)   Lines:  95.74% ( 45/ 47)
DrevOps\Tui\Render\TerminalControl
  Methods: 100.00% ( 9/ 9)   Lines: 100.00% (  9/  9)
DrevOps\Tui\Render\Viewport
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  1/  1)
DrevOps\Tui\Resolver\InputResolver
  Methods: 100.00% ( 6/ 6)   Lines: 100.00% ( 29/ 29)
DrevOps\Tui\Schema\AgentHelp
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 31/ 31)
DrevOps\Tui\Schema\SchemaGenerator
  Methods: 100.00% ( 3/ 3)   Lines: 100.00% ( 33/ 33)
DrevOps\Tui\Schema\SchemaValidator
  Methods: 100.00% ( 7/ 7)   Lines: 100.00% ( 32/ 32)
DrevOps\Tui\Testing\ArrayKeyStream
  Methods: 100.00% ( 3/ 3)   Lines: 100.00% ( 12/ 12)
DrevOps\Tui\Testing\BufferedTerminal
  Methods: 100.00% ( 6/ 6)   Lines: 100.00% ( 11/ 11)
DrevOps\Tui\Testing\KeyEncoder
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% ( 22/ 22)
DrevOps\Tui\Testing\TuiTester
  Methods: 100.00% (12/12)   Lines: 100.00% ( 29/ 29)
DrevOps\Tui\Testing\WidgetRunner
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  5/  5)
DrevOps\Tui\Theme\DefaultTheme
  Methods:  95.89% (70/73)   Lines:  96.69% (263/272)
DrevOps\Tui\Theme\DosTheme
  Methods:  72.73% ( 8/11)   Lines:  76.92% ( 10/ 13)
DrevOps\Tui\Theme\EmberTheme
  Methods:  66.67% ( 6/ 9)   Lines:  66.67% (  6/  9)
DrevOps\Tui\Theme\FrostTheme
  Methods:  66.67% ( 6/ 9)   Lines:  66.67% (  6/  9)
DrevOps\Tui\Theme\MidnightTheme
  Methods:  66.67% ( 6/ 9)   Lines:  66.67% (  6/  9)
DrevOps\Tui\Theme\MonoTheme
  Methods:  66.67% ( 6/ 9)   Lines:  66.67% (  6/  9)
DrevOps\Tui\Theme\Sgr
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  1/  1)
DrevOps\Tui\Theme\ThemeManager
  Methods: 100.00% ( 2/ 2)   Lines: 100.00% (  8/  8)
DrevOps\Tui\Translation\Translator
  Methods: 100.00% (11/11)   Lines: 100.00% ( 45/ 45)
DrevOps\Tui\Tui
  Methods: 100.00% (21/21)   Lines: 100.00% ( 61/ 61)
DrevOps\Tui\Widget\AbstractWidget
  Methods: 100.00% (17/17)   Lines: 100.00% ( 43/ 43)
DrevOps\Tui\Widget\CalendarWidget
  Methods: 100.00% (13/13)   Lines: 100.00% ( 54/ 54)
DrevOps\Tui\Widget\Capability\CompletionCapableTrait
  Methods: 100.00% ( 3/ 3)   Lines: 100.00% ( 14/ 14)
DrevOps\Tui\Widget\Capability\FilterCapableTrait
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 15/ 15)
DrevOps\Tui\Widget\Capability\MultiSelectionCapableTrait
  Methods: 100.00% (10/10)   Lines: 100.00% ( 63/ 63)
DrevOps\Tui\Widget\Capability\OptionsCapableTrait
  Methods: 100.00% ( 9/ 9)   Lines: 100.00% ( 34/ 34)
DrevOps\Tui\Widget\Capability\PagingCapableTrait
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 16/ 16)
DrevOps\Tui\Widget\Capability\SearchCapableTrait
  Methods: 100.00% ( 3/ 3)   Lines: 100.00% (  3/  3)
DrevOps\Tui\Widget\Capability\SelectionCapableTrait
  Methods: 100.00% ( 6/ 6)   Lines: 100.00% ( 19/ 19)
DrevOps\Tui\Widget\Capability\TextEditCapableTrait
  Methods: 100.00% ( 9/ 9)   Lines: 100.00% ( 34/ 34)
DrevOps\Tui\Widget\ConfirmWidget
  Methods: 100.00% ( 7/ 7)   Lines: 100.00% ( 23/ 23)
DrevOps\Tui\Widget\FilePickerWidget
  Methods: 100.00% (31/31)   Lines: 100.00% (181/181)
DrevOps\Tui\Widget\MatchResult
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  1/  1)
DrevOps\Tui\Widget\MatchTier
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  6/  6)
DrevOps\Tui\Widget\Matcher
  Methods: 100.00% ( 9/ 9)   Lines: 100.00% ( 73/ 73)
DrevOps\Tui\Widget\MultiSearchWidget
  Methods: 100.00% ( 2/ 2)   Lines: 100.00% (  4/  4)
DrevOps\Tui\Widget\MultiSelectWidget
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% (  7/  7)
DrevOps\Tui\Widget\NumberWidget
  Methods: 100.00% ( 9/ 9)   Lines: 100.00% ( 38/ 38)
DrevOps\Tui\Widget\PasswordDisplay
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  5/  5)
DrevOps\Tui\Widget\PasswordWidget
  Methods: 100.00% ( 9/ 9)   Lines: 100.00% ( 47/ 47)
DrevOps\Tui\Widget\PauseWidget
  Methods: 100.00% ( 5/ 5)   Lines: 100.00% ( 11/ 11)
DrevOps\Tui\Widget\ReorderWidget
  Methods: 100.00% (10/10)   Lines: 100.00% ( 55/ 55)
DrevOps\Tui\Widget\SearchWidget
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 14/ 14)
DrevOps\Tui\Widget\SelectWidget
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% (  6/  6)
DrevOps\Tui\Widget\SuggestWidget
  Methods: 100.00% (12/12)   Lines: 100.00% ( 46/ 46)
DrevOps\Tui\Widget\TextWidget
  Methods: 100.00% ( 5/ 5)   Lines: 100.00% ( 18/ 18)
DrevOps\Tui\Widget\TextareaWidget
  Methods: 100.00% ( 8/ 8)   Lines: 100.00% ( 51/ 51)
DrevOps\Tui\Widget\ToggleWidget
  Methods: 100.00% ( 8/ 8)   Lines: 100.00% ( 32/ 32)
DrevOps\Tui\Widget\WidgetFactory
  Methods: 100.00% ( 5/ 5)   Lines: 100.00% ( 35/ 35)

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