Skip to content

Consume InputReader in component flows when there is no tty - #1388

Open
czpilar wants to merge 1 commit into
spring-projects:mainfrom
czpilar:patches/1383-component-flow-input-reader
Open

czpilar wants to merge 1 commit into
spring-projects:mainfrom
czpilar:patches/1383-component-flow-input-reader

Conversation

@czpilar

@czpilar czpilar commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Interactive components read keystrokes from a JLine Terminal and ignore the InputReader of the CommandContext, so inputs queued through ShellInputProvider never reach a ComponentFlow in a @ShellTest. Without a tty the components skip their read loop and the flow context ends up without their keys. This is option 3 from the discussion in the issue.

Changes

  • ComponentFlow.Builder#inputReader(InputReader) lets a command hand over the reader of its CommandContext. Kept across clone() and reset() like the other builder infrastructure.
  • Without a tty and with a reader set, StringInput, NumberInput, PathInput and ConfirmationInput read one line from it and feed it through their existing read() logic, so parsing, default values and validation are unchanged. With a real terminal the reader is ignored.
  • PathSearch opts out and item selectors are not covered; key driven components cannot be expressed as a single line of text.
  • spring-shell-test is untouched and still depends only on spring-shell-core.

Notes

  • The command has to declare CommandContext as a parameter and call .inputReader(commandContext.inputReader()). The reproducer from the issue needs those two lines to pass.
  • ConfirmationInput accepts y, yes, 1 and n, no, 0; the value true from the report has never been recognised.
  • A Terminal bean is still required. The @ShellTest slice does not provide one; the reproducer gets it from full auto-configuration.

Unit tests cover the four components and the flow, and ComponentFlowShellTests runs the scenario from the issue end to end. A new section in testing.adoc documents the usage.

Resolves #1383

Signed-off-by: David Pilar <david@czpilar.net>
@fmbenhassine

Copy link
Copy Markdown
Contributor

Thank you for your PR! The direction is right (bridging InputReader into ComponentFlow's single-value components), but I need to wrap up my head around this, as I don't fully understand the approach here (especially the isInputReaderFallbackSupported flag and the headlessLoop method).

I will get back to you once I review this in detail. #1388 might be re-scheduled for a later iteration than 4.0.4.

@czpilar

czpilar commented Sep 25, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @fmbenhassine, no rush, and a later iteration is fine with me. Let me explain the two pieces you mention, since both exist for a rather mechanical reason.

headlessLoop. The existing loop() in AbstractComponent is tied to a live terminal: it creates a Display, enters raw mode, emits keypad_xmit / cursor_invisible, re-renders the component on every iteration and reads from the BindingReader built over terminal.reader(). None of that is usable without a tty, and the input has to come from somewhere else. headlessLoop is the same read() dispatch with all of that stripped away: it takes a BindingReader as a parameter and loops until read() returns true. The caller builds that reader over the line from the InputReader plus a carriage return, so the component goes through exactly the OPERATION_CHAR / OPERATION_EXIT path it uses interactively. That is what keeps parsing, default values and required() handling identical without duplicating them per component.

isInputReaderFallbackSupported. PathSearch extends AbstractTextComponent, so putting the fallback into AbstractTextComponent.runInternal would make PathSearch pick it up as well, and it is a key driven component (it navigates a scored list of candidates), not a single value input. The hook only lets that one class switch the fallback off. If you would rather not have a hook, two alternatives come to mind: override runInternal in PathSearch directly, or move the fallback out of the base class into the four concrete components. Happy to go either way.

The rest is plumbing: Builder#inputReader on ComponentFlow, carried through clone() / reset() and handed to the four single value components.

This branch has not been deployed

No deployments
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.

ShellInputProvider inputs are not consumed by ComponentFlow-based interactive components (e.g. ConfirmationInput, StringInput)

2 participants