feat: Add /dev/tty fallback for terminals with piped streams#1728
feat: Add /dev/tty fallback for terminals with piped streams#1728
Conversation
…1714) When a JLine application is launched by a process that redirects stdin/stdout through pipes (e.g., Maven exec-maven-plugin with exec:exec goal), JLine cannot detect a TTY and falls back to a dumb terminal, silently disabling interactive features like tab completion, syntax highlighting, and history. This adds an opt-in /dev/tty fallback that opens the controlling terminal directly when no system stream is a TTY. Enable it with the system property -Dorg.jline.terminal.devtty=true or the builder method devTty(true). The implementation saves the current terminal settings, sets raw mode on /dev/tty via stty, and uses TerminalProvider.newTerminal() with /dev/tty streams. FFM/JNI providers create a full PosixPtyTerminal with native PTY support; the exec provider creates an ExternalTerminal. Original terminal settings are restored when the terminal is closed.
Instead of manually running stty commands and opening /dev/tty streams in TerminalBuilder, add an overloaded sysTerminal() method to TerminalProvider (without systemStream parameter) for creating a terminal from the controlling terminal device. ExecTerminalProvider implements this by creating an ExecPty with "/dev/tty" as the device name, leveraging ExecPty's existing support for named devices (stty -F /dev/tty for attributes, FileInputStream/FileOutputStream for I/O). The resulting PosixSysTerminal handles raw mode save/restore through the Pty abstraction automatically. Other providers (FFM, JNI) inherit the default implementation which throws UnsupportedOperationException, allowing the builder to try the next provider.
|
Closing this in favor of a split approach:
|
…lback # Conflicts: # website/docs/terminal.md # website/docs/troubleshooting.md
📝 WalkthroughWalkthroughThis pull request introduces a Changes
Sequence Diagram(s)sequenceDiagram
participant App as Application
participant Builder as TerminalBuilder
participant Provider as TerminalProvider(s)
participant ExecProvider as ExecTerminalProvider
participant DevTTY as /dev/tty
App->>Builder: build()
Builder->>Builder: doBuild() - initial attempt
Builder->>Builder: terminal == null?
alt Initial terminal creation failed
Builder->>Builder: Check devTty config
Builder->>Builder: Iterate providers
loop For each TerminalProvider
Builder->>Provider: sysTerminal(...) [no SystemStream]
alt ExecTerminalProvider implementation
Provider->>ExecProvider: sysTerminal() override
ExecProvider->>ExecProvider: Non-Windows check
ExecProvider->>ExecProvider: Create ExecPty(/dev/tty)
ExecProvider->>DevTTY: Connect to controlling terminal
ExecProvider-->>Provider: Return PosixSysTerminal
else Provider not implemented
Provider-->>Builder: UnsupportedOperationException
end
Provider-->>Builder: Terminal or exception
end
alt Terminal acquired
Builder->>Builder: Use fallback terminal
end
end
Builder-->>App: Terminal instance
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@terminal/src/main/java/org/jline/terminal/impl/exec/ExecTerminalProvider.java`:
- Around line 134-152: The sysTerminal method in ExecTerminalProvider ignores
the passed paused parameter; after constructing the PosixSysTerminal instance in
sysTerminal(String name, ..., boolean paused) call terminal.pause() when paused
is true so the terminal starts paused as the TerminalProvider contract requires;
locate the sysTerminal method in ExecTerminalProvider, instantiate
PosixSysTerminal as currently done, then if (paused) invoke the terminal's
pause() method before returning the terminal.
🪄 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: e5144bf5-d41c-426c-9259-9a44d309f338
📒 Files selected for processing (5)
terminal/src/main/java/org/jline/terminal/TerminalBuilder.javaterminal/src/main/java/org/jline/terminal/impl/exec/ExecTerminalProvider.javaterminal/src/main/java/org/jline/terminal/spi/TerminalProvider.javawebsite/docs/terminal.mdwebsite/docs/troubleshooting.md
| @Override | ||
| public Terminal sysTerminal( | ||
| String name, | ||
| String type, | ||
| boolean ansiPassThrough, | ||
| Charset encoding, | ||
| Charset inputEncoding, | ||
| Charset outputEncoding, | ||
| boolean nativeSignals, | ||
| Terminal.SignalHandler signalHandler, | ||
| boolean paused) | ||
| throws IOException { | ||
| if (OSUtils.IS_WINDOWS) { | ||
| throw new UnsupportedOperationException("/dev/tty is not available on Windows"); | ||
| } | ||
| Pty pty = new ExecPty(this, null, "/dev/tty"); | ||
| return new PosixSysTerminal( | ||
| name, type, pty, encoding, inputEncoding, outputEncoding, nativeSignals, signalHandler); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for how PosixSysTerminal handles the paused parameter in other providers
ast-grep --pattern 'new PosixSysTerminal($$$)'Repository: jline/jline3
Length of output: 2364
🏁 Script executed:
#!/bin/bash
# Find PosixSysTerminal class definition
fd -t f 'PosixSysTerminal.java' | head -5Repository: jline/jline3
Length of output: 124
🏁 Script executed:
#!/bin/bash
# Check the PosixSysTerminal constructor signature
fd -t f 'PosixSysTerminal.java' -x cat {} \; | head -80Repository: jline/jline3
Length of output: 2792
🏁 Script executed:
#!/bin/bash
# Check how paused parameter is handled in other sysTerminal implementations in ExecTerminalProvider
rg -A 20 "public Terminal sysTerminal" terminal/src/main/java/org/jline/terminal/impl/exec/ExecTerminalProvider.java | head -100Repository: jline/jline3
Length of output: 1478
🏁 Script executed:
#!/bin/bash
# Get the full PosixSysTerminal class to see all constructor signatures
wc -l terminal/src/main/java/org/jline/terminal/impl/PosixSysTerminal.javaRepository: jline/jline3
Length of output: 128
🏁 Script executed:
#!/bin/bash
# Check PosixSysTerminal constructors
sed -n '60,150p' terminal/src/main/java/org/jline/terminal/impl/PosixSysTerminal.javaRepository: jline/jline3
Length of output: 3374
🏁 Script executed:
#!/bin/bash
# Search for pause() method in Terminal interface and related classes
rg -i "pause\(\)" --type java | head -20Repository: jline/jline3
Length of output: 1625
🏁 Script executed:
#!/bin/bash
# Check the sysTerminal interface definition to understand the paused parameter contract
rg -B 5 -A 15 "Terminal sysTerminal.*boolean paused" terminal/src/main/java/org/jline/terminal/spi/TerminalProvider.java | head -60Repository: jline/jline3
Length of output: 38
🏁 Script executed:
#!/bin/bash
# Check TerminalProvider.java for the interface definition of sysTerminal
fd -t f 'TerminalProvider.java'Repository: jline/jline3
Length of output: 434
🏁 Script executed:
#!/bin/bash
# Get the full TerminalProvider interface
cat terminal-spi/src/main/java/org/jline/terminal/spi/TerminalProvider.java | head -200Repository: jline/jline3
Length of output: 159
🏁 Script executed:
#!/bin/bash
# Check how JniTerminalProvider handles the paused parameter
rg -B 5 -A 25 "public Terminal sysTerminal" terminal-jni/src/main/java/org/jline/terminal/impl/jni/JniTerminalProvider.java | head -100Repository: jline/jline3
Length of output: 988
🏁 Script executed:
#!/bin/bash
# Find the TerminalProvider in the correct path
find . -name 'TerminalProvider.java' -type fRepository: jline/jline3
Length of output: 125
🏁 Script executed:
#!/bin/bash
# Check the content of the TerminalProvider interface
cat terminal/src/main/java/org/jline/terminal/spi/TerminalProvider.java | head -150Repository: jline/jline3
Length of output: 5850
🏁 Script executed:
#!/bin/bash
# Check how FfmTerminalProvider handles the paused parameter in sysTerminal
rg -B 3 -A 20 "public Terminal sysTerminal" terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/FfmTerminalProvider.javaRepository: jline/jline3
Length of output: 786
🏁 Script executed:
#!/bin/bash
# Check if PosixSysTerminal is supposed to receive paused parameter - look at AbstractPosixTerminal
rg -i "paused" terminal/src/main/java/org/jline/terminal/impl/AbstractPosixTerminal.javaRepository: jline/jline3
Length of output: 38
🏁 Script executed:
#!/bin/bash
# Look for where paused is actually used in sysTerminal implementations
rg -B 3 -A 10 "boolean paused" terminal/src/main/java/org/jline/terminal/impl/exec/ExecTerminalProvider.java | head -60Repository: jline/jline3
Length of output: 2209
🏁 Script executed:
#!/bin/bash
# Check if winSysTerminal or other terminal creation methods use paused
rg -B 2 -A 8 "if.*paused" terminal/src/main/java/org/jline/terminal/impl/exec/ExecTerminalProvider.javaRepository: jline/jline3
Length of output: 38
🏁 Script executed:
#!/bin/bash
# Check DumbTerminalProvider to see if it handles paused
rg -B 3 -A 15 "public Terminal sysTerminal" terminal/src/main/java/org/jline/terminal/impl/DumbTerminalProvider.javaRepository: jline/jline3
Length of output: 798
🏁 Script executed:
#!/bin/bash
# Check winSysTerminal method to see if it uses paused parameter
rg -B 5 -A 20 "private Terminal winSysTerminal" terminal/src/main/java/org/jline/terminal/impl/exec/ExecTerminalProvider.java | head -80Repository: jline/jline3
Length of output: 38
🏁 Script executed:
#!/bin/bash
# Check if any terminal implementation in ExecTerminalProvider actually calls pause() after creating terminal
rg -B 2 -A 8 "terminal.pause" terminal/src/main/java/org/jline/terminal/impl/exec/ExecTerminalProvider.javaRepository: jline/jline3
Length of output: 38
🏁 Script executed:
#!/bin/bash
# Look at how the paused parameter is used in JniTerminalProvider (if at all)
rg "boolean paused" terminal-jni/src/main/java/org/jline/terminal/impl/jni/JniTerminalProvider.java -A 15 | head -80Repository: jline/jline3
Length of output: 2470
🏁 Script executed:
#!/bin/bash
# Verify the 9-parameter sysTerminal is indeed the default method in TerminalProvider interface
rg -B 10 "default Terminal sysTerminal" terminal/src/main/java/org/jline/terminal/spi/TerminalProvider.javaRepository: jline/jline3
Length of output: 697
🏁 Script executed:
#!/bin/bash
# Check if there are any usages of this 9-parameter sysTerminal in callers to understand expectations
rg "\.sysTerminal\(" terminal/src/main/java/org/jline/terminal/TerminalBuilder.java | head -10Repository: jline/jline3
Length of output: 211
🏁 Script executed:
#!/bin/bash
# Final check: verify AbstractTerminal.pause() default implementation
rg -B 3 -A 5 "public void pause" terminal/src/main/java/org/jline/terminal/impl/AbstractTerminal.javaRepository: jline/jline3
Length of output: 243
🏁 Script executed:
#!/bin/bash
# Confirm PosixSysTerminal extends AbstractPosixTerminal which extends AbstractTerminal
head -5 terminal/src/main/java/org/jline/terminal/impl/PosixSysTerminal.java | tail -3Repository: jline/jline3
Length of output: 201
The paused parameter is accepted but not used, violating the TerminalProvider interface contract.
The paused parameter at line 144 is declared but never passed to or acted upon when creating the PosixSysTerminal. The TerminalProvider interface explicitly documents this parameter as determining whether the terminal should start in a paused state. While the 10-parameter sysTerminal overload correctly passes paused to native methods on Windows, this 9-parameter variant (used for the /dev/tty fallback on POSIX systems) ignores it entirely, creating an inconsistency.
Since PosixSysTerminal inherits the pause() method from AbstractTerminal, the fix is straightforward: call terminal.pause() after construction if the paused parameter is true.
🔧 Proposed fix
`@Override`
public Terminal sysTerminal(
String name,
String type,
boolean ansiPassThrough,
Charset encoding,
Charset inputEncoding,
Charset outputEncoding,
boolean nativeSignals,
Terminal.SignalHandler signalHandler,
boolean paused)
throws IOException {
if (OSUtils.IS_WINDOWS) {
throw new UnsupportedOperationException("/dev/tty is not available on Windows");
}
Pty pty = new ExecPty(this, null, "/dev/tty");
- return new PosixSysTerminal(
- name, type, pty, encoding, inputEncoding, outputEncoding, nativeSignals, signalHandler);
+ PosixSysTerminal terminal = new PosixSysTerminal(
+ name, type, pty, encoding, inputEncoding, outputEncoding, nativeSignals, signalHandler);
+ if (paused) {
+ terminal.pause();
+ }
+ return terminal;
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@terminal/src/main/java/org/jline/terminal/impl/exec/ExecTerminalProvider.java`
around lines 134 - 152, The sysTerminal method in ExecTerminalProvider ignores
the passed paused parameter; after constructing the PosixSysTerminal instance in
sysTerminal(String name, ..., boolean paused) call terminal.pause() when paused
is true so the terminal starts paused as the TerminalProvider contract requires;
locate the sysTerminal method in ExecTerminalProvider, instantiate
PosixSysTerminal as currently done, then if (paused) invoke the terminal's
pause() method before returning the terminal.



Summary
/dev/ttyfallback when no system stream is a TTY (e.g., when running viaexec-maven-plugin'sexec:execgoal)org.jline.terminal.devtty=trueand builder methoddevTty(true)to enable the fallbackTerminalProvider.newTerminal()with/dev/ttystreams — FFM/JNI providers create a fullPosixPtyTerminal, exec provider creates anExternalTerminalsttyon open/closeFixes #1714
Summary by CodeRabbit
Release Notes
New Features
/dev/ttyfallback support to maintain terminal functionality when input/output are piped (POSIX systems only).Documentation
/dev/ttyfallback option via system property or programmatic configuration.