Skip to content

fix: add VT100 response feedback to SwingTerminal and WebTerminal#1824

Merged
gnodet merged 2 commits into
masterfrom
fix/vt100-response-feedback
Apr 24, 2026
Merged

fix: add VT100 response feedback to SwingTerminal and WebTerminal#1824
gnodet merged 2 commits into
masterfrom
fix/vt100-response-feedback

Conversation

@gnodet
Copy link
Copy Markdown
Member

@gnodet gnodet commented Apr 24, 2026

Summary

Fixes #1797 (partial — bug fix only, refactoring in a follow-up PR).

SwingTerminal and WebTerminal never fed VT100 responses back to the LineDisciplineTerminal after writing to the ScreenTerminal. This means terminal query-response sequences like cursor position reports (DSR) and device attributes (DA) were silently lost.

DisplayTest and Tmux already do this correctly: after ScreenTerminal.write(), they call read() to collect any VT100 output and feed it back via processInputBytes().

Changes

  • SwingTerminal: Add read() to TerminalComponent, add feedbackVt100Response() to SwingTerminalOutputStream called after each component.write()
  • WebTerminal: Add webTerminal reference to WebTerminalOutputStream, add feedbackVt100Response() called after each component.write()

Test plan

  • ./mvx mvn test -pl builtins — 1071 tests pass
  • ./mvx mvn test -pl terminal -Dtest=DisplayTest — 2 tests pass
  • ./mvx mvn spotless:apply -pl builtins — no formatting changes

Summary by CodeRabbit

  • Improvements

    • Enhanced terminal input/output feedback so graphical terminals reflect immediate echo and VT100 responses.
    • Improved web-based terminal response handling for more accurate command feedback and interaction.
    • Output operations now process and incorporate feedback data promptly for consistent terminal behavior.
  • Tests

    • Added automated tests validating VT100 feedback and echo behavior across terminal implementations.

 #1797)

SwingTerminal and WebTerminal never fed VT100 responses (cursor
position reports, device attributes) back to the LineDisciplineTerminal
after writing to the ScreenTerminal. DisplayTest and Tmux do this
correctly via ScreenTerminal.read() + processInputBytes().

Add feedbackVt100Response() to both output streams so that VT100
query-response sequences (e.g. DSR, DA) work correctly.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 2026

📝 Walkthrough

Walkthrough

Output streams for SwingTerminal and WebTerminal were changed to perform an immediate read/feedback loop after writing decoded output: they call a feedback hook that reads pending bytes from the terminal component and routes any non-empty bytes back into the terminal's input processor for immediate processing.

Changes

Cohort / File(s) Summary
SwingTerminal Feedback Loop
builtins/src/main/java/org/jline/builtins/SwingTerminal.java
Added public String read() to TerminalComponent. SwingTerminalOutputStream now calls feedbackVt100Response() after writing decoded output and routes any pending component bytes back into terminal.processInputBytes(...).
WebTerminal Feedback Loop
builtins/src/main/java/org/jline/builtins/WebTerminal.java
Constructor now supplies WebTerminal reference to WebTerminalOutputStream. During UTF‑8 decoding, the stream invokes feedbackVt100Response() to read component.read() and immediately pass non-empty bytes to webTerminal.processInputBytes(...).
Tests
builtins/src/test/.../Vt100ResponseFeedbackTest.java
New JUnit test verifying VT100 feedback: sends DSR/DA sequences and asserts expected VT100 responses; Swing tests are disabled in headless environments.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant OutputStream
    participant TerminalComponent
    participant Terminal

    Client->>OutputStream: write(bytes)
    OutputStream->>OutputStream: decode UTF-8
    OutputStream->>TerminalComponent: write(decoded output)
    OutputStream->>OutputStream: feedbackVt100Response()
    OutputStream->>TerminalComponent: read()
    TerminalComponent-->>OutputStream: pending bytes
    alt pending bytes non-empty
        OutputStream->>Terminal: processInputBytes(pending bytes)
        Terminal->>Terminal: process VT100 response
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

bug, java

Poem

🐰 A nibble, a write, then a curious peek,

bytes bounce back quick as a hop of a week.
VT100 whispers caught mid-play,
echoed right back without delay,
rabbits cheer — terminals brighten the day! 🥕

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive While the PR focuses on the critical feedback mechanism, issue #1797 also requests simplification and consolidation of the complex output stream implementations, which are only partially addressed. Clarify whether the comprehensive simplification of SwingTerminal's complex output stream and WebTerminal's manual UTF-8 decoding refactoring are deferred to future PRs or out of scope for this change.
✅ Passed checks (3 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 VT100 response feedback to SwingTerminal and WebTerminal, which directly matches the primary objective of the changeset.
Linked Issues check ✅ Passed The PR implements the core coding requirement from issue #1797: adding feedbackVt100Response() hooks to feed VT100 output back through processInputBytes() in both terminals [#1797].

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 fix/vt100-response-feedback

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
builtins/src/main/java/org/jline/builtins/WebTerminal.java (1)

500-508: ⚠️ Potential issue | 🟠 Major

Same concurrency concern as SwingTerminalOutputStream.feedbackVt100Response.

TerminalAjaxHandler.handle (line 330) calls processInputBytes on the HTTP handler thread to deliver user keystrokes. Output generated by another thread (e.g., a running command writing to masterOutput) now also calls processInputBytes via this feedback hook. Since LineDisciplineTerminal.processInputBytes is unsynchronized, a VT100 response and a concurrent keystroke can interleave byte-for-byte in slaveInputPipe. Please verify whether serialization is needed here; the same remediation would apply to SwingTerminal.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builtins/src/main/java/org/jline/builtins/WebTerminal.java` around lines 500
- 508, feedbackVt100Response can race with TerminalAjaxHandler.handle because
both call LineDisciplineTerminal.processInputBytes (via
webTerminal.processInputBytes), which is unsynchronized and can interleave bytes
into slaveInputPipe; serialize calls to processInputBytes by guarding them with
the same lock used elsewhere (or add a dedicated lock) so both
TerminalAjaxHandler.handle and WebTerminal.feedbackVt100Response synchronize on
that lock before invoking webTerminal.processInputBytes; update
WebTerminal.feedbackVt100Response to acquire the lock (or call through a
single-threaded executor) prior to calling processInputBytes, and apply the same
pattern to SwingTerminalOutputStream.feedbackVt100Response to ensure consistent
serialization.
🧹 Nitpick comments (2)
builtins/src/main/java/org/jline/builtins/SwingTerminal.java (2)

872-874: LGTM — consider a brief Javadoc for consistency.

Other public methods in TerminalComponent (write, dump, isDirty, pollInput, takeInput) are documented; adding a one-liner describing that this drains pending VT100 output from the underlying ScreenTerminal would keep the surface consistent.

📝 Proposed Javadoc
+        /**
+         * Drains any pending VT100 response output produced by the underlying {`@link` ScreenTerminal}.
+         *
+         * `@return` the accumulated VT100 output, or an empty string if none
+         */
         public String read() {
             return screenTerminal.read();
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builtins/src/main/java/org/jline/builtins/SwingTerminal.java` around lines
872 - 874, Add a one-line Javadoc to the public method read() in SwingTerminal
describing its behavior and consistency with other TerminalComponent methods:
state that read() drains and returns pending VT100 output from the underlying
ScreenTerminal (screenTerminal.read()), keeping the surface consistent with
write, dump, isDirty, pollInput, and takeInput documentation; place the Javadoc
immediately above the public String read() method in class SwingTerminal.

310-314: Minor: feedback is skipped on the buffer-full decode path.

decodeAndOutput(false) on line 312 (and line 450 inside it) can write decoded characters to component when the 16-byte inputBuffer fills up, but does not call feedbackVt100Response(). In practice the VT100 response stays queued in vt100_out and is picked up on the next write, so it's not a correctness bug — just an inconsistency worth a follow-up if you want uniform drain behavior.

Also applies to: 449-452

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builtins/src/main/java/org/jline/builtins/SwingTerminal.java` around lines
310 - 314, When inputBuffer fills and you call decodeAndOutput(false) (in the
buffer-full branches around inputBuffer.hasRemaining() and the similar path at
lines ~449-452), the code writes decoded chars to component but does not call
feedbackVt100Response(), leaving vt100_out queued; update those buffer-full
paths to invoke feedbackVt100Response() after decodeAndOutput(false) so VT100
feedback is drained immediately and behavior matches the normal decode path.
Ensure you reference the same flow used elsewhere (where decodeAndOutput(true)
triggers feedbackVt100Response()) and call feedbackVt100Response() only after
decodeAndOutput(false) completes to preserve ordering.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@builtins/src/main/java/org/jline/builtins/SwingTerminal.java`:
- Around line 270-278: feedbackVt100Response currently calls
terminal.processInputBytes(...) from arbitrary threads (EDT/HTTP handlers) while
inputThread and other handlers also call processInputBytes, allowing byte-level
interleaving; fix by serializing all calls to
LineDisciplineTerminal.processInputBytes: introduce a dedicated lock or marshal
VT100 responses onto the same input thread used by SwingTerminal.inputThread (or
WebTerminal's input dispatcher) and use that lock when invoking
terminal.processInputBytes in SwingTerminal.feedbackVt100Response and
WebTerminal.WebTerminalOutputStream.feedbackVt100Response so writes to the
LineDisciplineTerminal/slaveInputPipe are single-threaded and ordered.

---

Duplicate comments:
In `@builtins/src/main/java/org/jline/builtins/WebTerminal.java`:
- Around line 500-508: feedbackVt100Response can race with
TerminalAjaxHandler.handle because both call
LineDisciplineTerminal.processInputBytes (via webTerminal.processInputBytes),
which is unsynchronized and can interleave bytes into slaveInputPipe; serialize
calls to processInputBytes by guarding them with the same lock used elsewhere
(or add a dedicated lock) so both TerminalAjaxHandler.handle and
WebTerminal.feedbackVt100Response synchronize on that lock before invoking
webTerminal.processInputBytes; update WebTerminal.feedbackVt100Response to
acquire the lock (or call through a single-threaded executor) prior to calling
processInputBytes, and apply the same pattern to
SwingTerminalOutputStream.feedbackVt100Response to ensure consistent
serialization.

---

Nitpick comments:
In `@builtins/src/main/java/org/jline/builtins/SwingTerminal.java`:
- Around line 872-874: Add a one-line Javadoc to the public method read() in
SwingTerminal describing its behavior and consistency with other
TerminalComponent methods: state that read() drains and returns pending VT100
output from the underlying ScreenTerminal (screenTerminal.read()), keeping the
surface consistent with write, dump, isDirty, pollInput, and takeInput
documentation; place the Javadoc immediately above the public String read()
method in class SwingTerminal.
- Around line 310-314: When inputBuffer fills and you call
decodeAndOutput(false) (in the buffer-full branches around
inputBuffer.hasRemaining() and the similar path at lines ~449-452), the code
writes decoded chars to component but does not call feedbackVt100Response(),
leaving vt100_out queued; update those buffer-full paths to invoke
feedbackVt100Response() after decodeAndOutput(false) so VT100 feedback is
drained immediately and behavior matches the normal decode path. Ensure you
reference the same flow used elsewhere (where decodeAndOutput(true) triggers
feedbackVt100Response()) and call feedbackVt100Response() only after
decodeAndOutput(false) completes to preserve ordering.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: b4d09a9e-70d4-42cd-8145-1f9f432aeb0b

📥 Commits

Reviewing files that changed from the base of the PR and between 53ac43e and 3aa5c46.

📒 Files selected for processing (2)
  • builtins/src/main/java/org/jline/builtins/SwingTerminal.java
  • builtins/src/main/java/org/jline/builtins/WebTerminal.java

Comment on lines +270 to +278
private void feedbackVt100Response() throws IOException {
if (component == null || terminal == null) {
return;
}
String response = component.read();
if (!response.isEmpty()) {
terminal.processInputBytes(response.getBytes(StandardCharsets.UTF_8));
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm LineDisciplineTerminal.processInputBytes has no synchronization and inspect slaveInputPipe writes.
fd -e java LineDisciplineTerminal.java --exec cat {}

Repository: jline/jline3

Length of output: 14331


🏁 Script executed:

fd -e java SwingTerminal.java --exec cat {} | head -300

Repository: jline/jline3

Length of output: 10183


🏁 Script executed:

fd -e java WebTerminal.java --exec cat {}

Repository: jline/jline3

Length of output: 27247


Concurrent calls to processInputBytes can cause byte-level interleaving of VT100 responses with keystrokes.

feedbackVt100Response() invokes terminal.processInputBytes(...) from whichever thread invokes the output stream's write methods (typically EDT in SwingTerminal, or HTTP handler threads in WebTerminal). Meanwhile, SwingTerminal's inputThread (line 96) and WebTerminal's HTTP handlers also call processInputBytes for user input. Per LineDisciplineTerminal.processInputBytes, byte iteration writes to slaveInputPipe without synchronization, allowing a VT100 response (e.g. \e[5;10R) to interleave at the byte level with concurrent keystrokes, producing malformed sequences downstream.

Serialize feedback on the input thread or guard processInputBytes with a lock. The same concern applies to WebTerminal.WebTerminalOutputStream.feedbackVt100Response.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builtins/src/main/java/org/jline/builtins/SwingTerminal.java` around lines
270 - 278, feedbackVt100Response currently calls terminal.processInputBytes(...)
from arbitrary threads (EDT/HTTP handlers) while inputThread and other handlers
also call processInputBytes, allowing byte-level interleaving; fix by
serializing all calls to LineDisciplineTerminal.processInputBytes: introduce a
dedicated lock or marshal VT100 responses onto the same input thread used by
SwingTerminal.inputThread (or WebTerminal's input dispatcher) and use that lock
when invoking terminal.processInputBytes in SwingTerminal.feedbackVt100Response
and WebTerminal.WebTerminalOutputStream.feedbackVt100Response so writes to the
LineDisciplineTerminal/slaveInputPipe are single-threaded and ordered.

…inal

Verify that DSR (cursor position report) and DA (device attributes)
responses from ScreenTerminal are correctly fed back as terminal input
via processInputBytes(). Also tests that cursor position reports
reflect actual cursor movement.
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
builtins/src/test/java/org/jline/builtins/Vt100ResponseFeedbackTest.java (2)

42-122: Optional: extract common setup to reduce duplication across the 5 tests.

Each test repeats: construct terminal → fetch attributes → disable ECHO → set attributes → write query → assert response. A small helper parameterized on a Terminal supplier and the query/expected response would keep the test bodies focused on what they verify. Not required — purely a readability nit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builtins/src/test/java/org/jline/builtins/Vt100ResponseFeedbackTest.java`
around lines 42 - 122, Multiple tests in Vt100ResponseFeedbackTest duplicate the
same setup steps (creating a Terminal, getting Attributes, disabling
Attributes.LocalFlag.ECHO, setting attributes, writing a query and asserting the
response); extract a small helper method (e.g., runTerminalQuery or
performTerminalQuery) that accepts a Supplier<Terminal> (or Terminal factory),
the query bytes/String, and the expected response, and call this helper from
webTerminalFeedsDsrResponse, webTerminalFeedsDeviceAttributesResponse,
webTerminalDsrReflectsCursorPosition, swingTerminalFeedsDsrResponse, and
swingTerminalFeedsDeviceAttributesResponse to centralize creating the
WebTerminal/SwingTerminal, applying attrs.setLocalFlag(..., false) via
terminal.getAttributes()/terminal.setAttributes(...), writing the query
(terminal.output().write(...); terminal.output().flush()), draining input with
readAvailable when needed, and asserting the response with
readAvailable(terminal.input(), timeout); keep test-specific differences (query
and expected response) as parameters so tests remain concise.

27-40: readAvailable may drop late-arriving bytes and artificially extends drain duration.

The second loop exits as soon as available() transiently returns 0, so any bytes delivered after the initial burst (even within the timeout) will be missed. This is tolerable because the new feedback path is inline with write()/flush(), but it makes the helper brittle if feedback ever becomes asynchronous.

Also, the drain at line 82 (readAvailable(..., 100)) will always wait the full 100 ms because writing "Hello" is not expected to produce any response, so available() stays 0 for the whole deadline. Consider an early-return “no data expected” drain, or keep reading until the deadline (not until the first empty poll).

♻️ Proposed refactor
 private String readAvailable(InputStream in, long timeoutMs) throws Exception {
     long deadline = System.currentTimeMillis() + timeoutMs;
-    while (in.available() == 0 && System.currentTimeMillis() < deadline) {
-        Thread.sleep(10);
-    }
     byte[] buf = new byte[256];
     int total = 0;
-    while (in.available() > 0 && total < buf.length) {
-        int n = in.read(buf, total, buf.length - total);
-        if (n <= 0) break;
-        total += n;
+    while (System.currentTimeMillis() < deadline && total < buf.length) {
+        if (in.available() > 0) {
+            int n = in.read(buf, total, buf.length - total);
+            if (n <= 0) break;
+            total += n;
+        } else if (total > 0) {
+            // We already got some bytes and nothing more is pending; assume response is complete.
+            break;
+        } else {
+            Thread.sleep(10);
+        }
     }
     return new String(buf, 0, total, StandardCharsets.UTF_8);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builtins/src/test/java/org/jline/builtins/Vt100ResponseFeedbackTest.java`
around lines 27 - 40, The helper readAvailable currently stops reading when
available() transiently returns 0 and the drain call readAvailable(..., 100)
will always block the full timeout; change readAvailable to keep polling until
the deadline and accumulate bytes as they arrive (do not break the read loop on
a single available()==0), and add an optional flag/overload (e.g.
readAvailable(InputStream in, long timeoutMs, boolean returnImmediatelyIfNoData)
or readAvailableNoWait) so callers that expect no response (the drain call) can
return immediately if no bytes were ever observed; update callers to use the new
flag where an early-return is desired and ensure the reading loop only exits on
deadline or when buffer is full.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@builtins/src/test/java/org/jline/builtins/Vt100ResponseFeedbackTest.java`:
- Around line 42-122: Multiple tests in Vt100ResponseFeedbackTest duplicate the
same setup steps (creating a Terminal, getting Attributes, disabling
Attributes.LocalFlag.ECHO, setting attributes, writing a query and asserting the
response); extract a small helper method (e.g., runTerminalQuery or
performTerminalQuery) that accepts a Supplier<Terminal> (or Terminal factory),
the query bytes/String, and the expected response, and call this helper from
webTerminalFeedsDsrResponse, webTerminalFeedsDeviceAttributesResponse,
webTerminalDsrReflectsCursorPosition, swingTerminalFeedsDsrResponse, and
swingTerminalFeedsDeviceAttributesResponse to centralize creating the
WebTerminal/SwingTerminal, applying attrs.setLocalFlag(..., false) via
terminal.getAttributes()/terminal.setAttributes(...), writing the query
(terminal.output().write(...); terminal.output().flush()), draining input with
readAvailable when needed, and asserting the response with
readAvailable(terminal.input(), timeout); keep test-specific differences (query
and expected response) as parameters so tests remain concise.
- Around line 27-40: The helper readAvailable currently stops reading when
available() transiently returns 0 and the drain call readAvailable(..., 100)
will always block the full timeout; change readAvailable to keep polling until
the deadline and accumulate bytes as they arrive (do not break the read loop on
a single available()==0), and add an optional flag/overload (e.g.
readAvailable(InputStream in, long timeoutMs, boolean returnImmediatelyIfNoData)
or readAvailableNoWait) so callers that expect no response (the drain call) can
return immediately if no bytes were ever observed; update callers to use the new
flag where an early-return is desired and ensure the reading loop only exits on
deadline or when buffer is full.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8c1fe9aa-2b56-4ae3-823c-0b16509f90a6

📥 Commits

Reviewing files that changed from the base of the PR and between 3aa5c46 and fc5aa63.

📒 Files selected for processing (1)
  • builtins/src/test/java/org/jline/builtins/Vt100ResponseFeedbackTest.java

@gnodet gnodet merged commit 9150044 into master Apr 24, 2026
15 checks passed
@gnodet gnodet added the fix Bug fix label May 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unified piped output stream for LineDescipleTerminal

1 participant