Skip to content

[#11] Added external editor handoff to the textarea widget.#26

Merged
AlexSkrypnyk merged 5 commits into
mainfrom
feature/11-external-editor
Jul 10, 2026
Merged

[#11] Added external editor handoff to the textarea widget.#26
AlexSkrypnyk merged 5 commits into
mainfrom
feature/11-external-editor

Conversation

@AlexSkrypnyk

@AlexSkrypnyk AlexSkrypnyk commented Jul 10, 2026

Copy link
Copy Markdown
Member

Closes #11

Summary

Textarea fields can now opt into an external-editor handoff for composing long or structured text.
Calling ->externalEditor() on a textarea field lets the user press Ctrl-E while editing to suspend the TUI and open their $VISUAL/$EDITOR, seeded with the current buffer.
Saving and exiting the editor commits the result as the field's value; an aborted edit (a non-zero editor exit) or the absence of a configured editor falls back to the existing inline editing behaviour.

Changes

New service

  • Added DrevOps\Tui\Render\ExternalEditor, which resolves the editor command from $VISUAL/$EDITOR ($VISUAL wins, matching the long-standing Unix convention), reports availability via isAvailable(), and drives the handoff in edit(): writes the buffer to a temp file, suspends the given Terminal (restore()), spawns the editor through proc_open(), restores the terminal (setup()) in a finally block so a failed spawn never strands the TUI in raw mode, then reads back and normalizes (drops one trailing newline) the saved file.
  • edit() returns NULL - signalling an aborted edit - when no editor is configured, the temp file or seed write fails, the editor exits non-zero, or the editor deletes the file instead of saving it.

Widget + controller wiring

  • TextareaWidget takes a new externalEdit constructor flag, listens for Ctrl-E (\x05) in handle(), and exposes wantsExternalEdit() / applyExternalEdit() so a driver can perform the handoff between key events; the control byte is swallowed (never inserted into the buffer) when the flag is off.
  • view() appends a ctrl-e editor footer hint only when the handoff is enabled.
  • WidgetFactory takes an externalEditorAvailable constructor flag and only offers the handoff on a Textarea widget when both the field opted in ($field->externalEditor) and an editor is actually available.
  • PanelController now owns an injectable ExternalEditor instance, passes its availability into the WidgetFactory it constructs, tracks the active Terminal for the duration of run(), and after each key is handled on a TextareaWidget, checks wantsExternalEdit() and, if requested, runs the handoff and applies the captured buffer (or NULL on abort).

Config/builder opt-in

  • Field gains a readonly bool $externalEditor = FALSE property, honoured only by the textarea widget.
  • FieldBuilder gains externalEditor(bool $enabled = TRUE): self, wired through to the built Field.

Docs + playground

  • README.md documents ->externalEditor() and the Ctrl-E behaviour.
  • playground/3-widgets/widget-textarea.php constructs an ExternalEditor, passes its availability into the TextareaWidget, and performs the handoff inline in its key loop.

Tests

  • New ExternalEditorTest covering command resolution precedence and trimming, availability, and every edit() branch: no editor, temp file failure, seed write failure, captured value with terminal suspend/restore, no terminal, non-zero exit, deleted file, and trailing-newline normalization.
  • TextareaWidgetTest, WidgetFactoryTest, PanelControllerTest, and FormTest gain coverage for the Ctrl-E request/swallow behaviour, the opt-in x availability matrix, commit/abort through the controller, and footer hint visibility.

Before / After

BEFORE - inline-only editing
+--------------------------------------+
| TextareaWidget                        |
|                                        |
|   Enter    -> insert newline          |
|   Up/Down  -> move between lines      |
|   Tab      -> accept value            |
+--------------------------------------+

AFTER - Ctrl-E suspends the TUI for $VISUAL/$EDITOR
+--------------------------------------+
| TextareaWidget                        |
|                                        |
|   Enter    -> insert newline          |
|   Up/Down  -> move between lines      |
|   Tab      -> accept value            |
|   Ctrl-E   -> wantsExternalEdit() ----+---+
+--------------------------------------+   |
                                            | PanelController sees the flag
                                            v
                       +--------------------------------------+
                       | ExternalEditor::edit()                |
                       |                                        |
                       |  1. write buffer to a temp file       |
                       |  2. terminal->restore()   (suspend)   |
                       |  3. spawn $VISUAL / $EDITOR           |
                       |  4. terminal->setup()     (resume)    |
                       |  5. read back + normalize the file    |
                       +--------------------------------------+
                                            |
                        applyExternalEdit(buffer | NULL)
                                            v
                    +----------------------------------------+
                    |  buffer -> commit as field value        |
                    |  NULL   -> keep inline value, stay open |
                    +----------------------------------------+

Summary by CodeRabbit

  • New Features

    • Textarea fields can now open the configured $VISUAL or $EDITOR with Ctrl-E.
    • Edited content is returned to the form, while cancelled or failed edits leave the original value unchanged.
    • Added a fluent option to enable external editing for supported textarea fields.
    • The textarea displays an editor hint when the feature is available.
  • Documentation

    • Added usage instructions and a PHP example to the Textarea documentation.

A textarea field can opt in with '->externalEditor()'. Pressing Ctrl-E while editing suspends the TUI, opens the user's $VISUAL/$EDITOR seeded with the current buffer, and captures the saved result on return; saving commits the value, an aborted edit or absent editor falls back to inline editing.
Renamed test methods to camel caps, matched the data provider to its test, wrapped over-length comments, and extracted the editor and terminal test doubles into reusable typed fixtures.
@AlexSkrypnyk AlexSkrypnyk added the A3 Board worker 3 label Jul 10, 2026
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds opt-in external-editor support for textarea fields. It resolves $VISUAL/$EDITOR, suspends and restores the terminal, captures saved content, handles aborts and unavailable editors, wires configuration through widgets and controllers, and documents the feature.

Changes

External editor handoff

Layer / File(s) Summary
External editor service
src/Render/ExternalEditor.php, tests/phpunit/Unit/Render/*
Adds editor resolution, temporary-file lifecycle, terminal restoration, saved-buffer normalization, failure handling, and unit coverage.
Field and widget capability wiring
src/Builder/FieldBuilder.php, src/Config/Field.php, src/Widget/WidgetFactory.php, tests/phpunit/Unit/Builder/FormTest.php, tests/phpunit/Unit/Widget/WidgetFactoryTest.php
Adds the externalEditor field option and enables textarea handoff only when the field and available editor both allow it.
Textarea handoff and controller flow
src/Widget/TextareaWidget.php, src/Render/PanelController.php, playground/3-widgets/widget-textarea.php, tests/phpunit/Unit/Widget/TextareaWidgetTest.php, tests/phpunit/Unit/Render/PanelControllerTest.php
Handles Ctrl-E requests, invokes the external editor, applies returned content, updates hints, and covers commit, abort, validation, and fallback behavior.
Usage documentation
README.md
Documents configuration, editor handoff, abort behavior, and fallback to inline editing.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • drevops/tui#3: Updates textarea footer and hint rendering used by the new editor hint.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding external editor handoff to the textarea widget.
Linked Issues check ✅ Passed The changes satisfy #11 by adding opt-in editor handoff, restore/fallback behavior, tests, and README documentation.
Out of Scope Changes check ✅ Passed The added playground updates, tests, and docs all directly support the external editor handoff feature.
✨ 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/11-external-editor

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 PHPStan (2.2.2)

Composer install failed: the lock file is not up to date with the latest changes in composer.json. Run composer update and commit the updated composer.lock.


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

@github-actions

Copy link
Copy Markdown
Code Coverage Report:
  2026-07-10 10:47:33

 Summary:
  Classes: 89.66% (52/58)
  Methods: 96.98% (353/364)
  Lines:   98.96% (1430/1445)

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\SummaryFormatter
  Methods: 100.00% ( 3/ 3)   Lines: 100.00% ( 20/ 20)
DrevOps\Tui\Builder\FieldBuilder
  Methods:  88.24% (15/17)   Lines:  92.59% ( 50/ 54)
DrevOps\Tui\Builder\Form
  Methods: 100.00% (13/13)   Lines: 100.00% ( 49/ 49)
DrevOps\Tui\Builder\PanelBuilder
  Methods: 100.00% (16/16)   Lines: 100.00% ( 28/ 28)
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\Config\Config
  Methods: 100.00% ( 5/ 5)   Lines: 100.00% ( 17/ 17)
DrevOps\Tui\Config\Field
  Methods: 100.00% ( 2/ 2)   Lines: 100.00% (  2/  2)
DrevOps\Tui\Config\Option
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  1/  1)
DrevOps\Tui\Config\Panel
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  1/  1)
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% (12/12)   Lines: 100.00% ( 89/ 89)
DrevOps\Tui\Handler\HandlerRegistry
  Methods:  85.71% ( 6/ 7)   Lines:  95.45% ( 21/ 22)
DrevOps\Tui\Input\ArrayKeyStream
  Methods: 100.00% ( 3/ 3)   Lines: 100.00% ( 12/ 12)
DrevOps\Tui\Input\Key
  Methods: 100.00% ( 5/ 5)   Lines: 100.00% (  5/  5)
DrevOps\Tui\Input\KeyParser
  Methods: 100.00% ( 6/ 6)   Lines: 100.00% ( 68/ 68)
DrevOps\Tui\Render\Ansi
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% (  5/  5)
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\Navigator
  Methods: 100.00% ( 6/ 6)   Lines: 100.00% ( 15/ 15)
DrevOps\Tui\Render\PanelController
  Methods: 100.00% (16/16)   Lines: 100.00% ( 92/ 92)
DrevOps\Tui\Render\Scroller
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 12/ 12)
DrevOps\Tui\Render\Terminal
  Methods: 100.00% (12/12)   Lines: 100.00% ( 35/ 35)
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% ( 27/ 27)
DrevOps\Tui\Schema\AgentHelp
  Methods: 100.00% ( 2/ 2)   Lines: 100.00% ( 16/ 16)
DrevOps\Tui\Schema\SchemaGenerator
  Methods: 100.00% ( 3/ 3)   Lines: 100.00% ( 25/ 25)
DrevOps\Tui\Schema\SchemaValidator
  Methods: 100.00% ( 7/ 7)   Lines: 100.00% ( 40/ 40)
DrevOps\Tui\Theme\DefaultTheme
  Methods:  91.80% (56/61)   Lines:  96.77% (180/186)
DrevOps\Tui\Theme\ThemeManager
  Methods: 100.00% ( 2/ 2)   Lines: 100.00% (  8/  8)
DrevOps\Tui\Tui
  Methods: 100.00% (12/12)   Lines: 100.00% ( 26/ 26)
DrevOps\Tui\Widget\AbstractWidget
  Methods: 100.00% (10/10)   Lines: 100.00% ( 20/ 20)
DrevOps\Tui\Widget\ConfirmWidget
  Methods: 100.00% ( 6/ 6)   Lines: 100.00% ( 23/ 23)
DrevOps\Tui\Widget\MultiSearchWidget
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  1/  1)
DrevOps\Tui\Widget\MultiSelectWidget
  Methods: 100.00% ( 9/ 9)   Lines: 100.00% ( 64/ 64)
DrevOps\Tui\Widget\NumberWidget
  Methods: 100.00% ( 2/ 2)   Lines: 100.00% (  7/  7)
DrevOps\Tui\Widget\PasswordDisplay
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  5/  5)
DrevOps\Tui\Widget\PasswordWidget
  Methods: 100.00% ( 7/ 7)   Lines: 100.00% ( 38/ 38)
DrevOps\Tui\Widget\PauseWidget
  Methods: 100.00% ( 3/ 3)   Lines: 100.00% (  6/  6)
DrevOps\Tui\Widget\SearchWidget
  Methods:  80.00% ( 4/ 5)   Lines:  94.59% ( 35/ 37)
DrevOps\Tui\Widget\SelectWidget
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 19/ 19)
DrevOps\Tui\Widget\SuggestWidget
  Methods: 100.00% ( 5/ 5)   Lines: 100.00% ( 37/ 37)
DrevOps\Tui\Widget\TextWidget
  Methods: 100.00% ( 6/ 6)   Lines: 100.00% ( 29/ 29)
DrevOps\Tui\Widget\TextareaWidget
  Methods: 100.00% ( 7/ 7)   Lines: 100.00% ( 48/ 48)
DrevOps\Tui\Widget\WidgetFactory
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 26/ 26)
DrevOps\Tui\Widget\WidgetRunner
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  5/  5)

@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.96%. Comparing base (861bb75) to head (f65e0a0).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #26      +/-   ##
==========================================
+ Coverage   98.92%   98.96%   +0.03%     
==========================================
  Files          57       58       +1     
  Lines        1395     1445      +50     
==========================================
+ Hits         1380     1430      +50     
  Misses         15       15              

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

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/phpunit/Unit/Render/SpawnStubEditor.php`:
- Around line 34-39: Rename the promoted constructor properties and all
references in SpawnStubEditor::__construct and its methods from
writeBack/deleteFile to write_back/delete_file, preserving their existing types,
defaults, and behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c2cd1432-f121-4a6c-8182-5384c2d3c860

📥 Commits

Reviewing files that changed from the base of the PR and between 861bb75 and f65e0a0.

📒 Files selected for processing (15)
  • README.md
  • playground/3-widgets/widget-textarea.php
  • src/Builder/FieldBuilder.php
  • src/Config/Field.php
  • src/Render/ExternalEditor.php
  • src/Render/PanelController.php
  • src/Widget/TextareaWidget.php
  • src/Widget/WidgetFactory.php
  • tests/phpunit/Unit/Builder/FormTest.php
  • tests/phpunit/Unit/Render/ExternalEditorTest.php
  • tests/phpunit/Unit/Render/PanelControllerTest.php
  • tests/phpunit/Unit/Render/RecordingTerminal.php
  • tests/phpunit/Unit/Render/SpawnStubEditor.php
  • tests/phpunit/Unit/Widget/TextareaWidgetTest.php
  • tests/phpunit/Unit/Widget/WidgetFactoryTest.php

Comment on lines +34 to +39
public function __construct(
protected ?string $writeBack,
protected int $code,
protected bool $deleteFile = FALSE,
) {
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Promoted constructor properties use camelCase instead of snake_case.

$writeBack and $deleteFile are constructor arguments (promoted to properties). The guideline calls for snake_case on method arguments; the codebase's own convention for multi-word constructor parameters (e.g. PanelController's $external_editor) follows snake_case.

♻️ Proposed rename
   public function __construct(
-    protected ?string $writeBack,
+    protected ?string $write_back,
     protected int $code,
-    protected bool $deleteFile = FALSE,
+    protected bool $delete_file = FALSE,
   ) {
   }

Update the reference at line 52 ($this->writeBack$this->write_back) and line 49 ($this->deleteFile$this->delete_file) accordingly.

As per coding guidelines, "Use snake_case for local variables and method arguments."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function __construct(
protected ?string $writeBack,
protected int $code,
protected bool $deleteFile = FALSE,
) {
}
public function __construct(
protected ?string $write_back,
protected int $code,
protected bool $delete_file = FALSE,
) {
}
🧰 Tools
🪛 PHPMD (2.15.0)

[error] 37-37: The method __construct has a boolean flag argument $deleteFile, which is a certain sign of a Single Responsibility Principle violation. (undefined)

(BooleanArgumentFlag)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/phpunit/Unit/Render/SpawnStubEditor.php` around lines 34 - 39, Rename
the promoted constructor properties and all references in
SpawnStubEditor::__construct and its methods from writeBack/deleteFile to
write_back/delete_file, preserving their existing types, defaults, and behavior.

Source: Coding guidelines

@AlexSkrypnyk AlexSkrypnyk merged commit 6486a4c into main Jul 10, 2026
8 of 9 checks passed
@AlexSkrypnyk AlexSkrypnyk deleted the feature/11-external-editor branch July 10, 2026 11:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A3 Board worker 3

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add external editor ($EDITOR) handoff for long text

1 participant