-
Notifications
You must be signed in to change notification settings - Fork 238
Open
Description
Summary
When using JLINE3's Status class for a persistent status bar at the terminal bottom, vertical terminal resizing causes multiple rendering issues including:
- Status bar flickering/flinching
- Text content scrolling upward unexpectedly
- Status bar appearing duplicated
- Scroll region corruption
- Cursor positioning errors
These issues occur because LineReaderImpl.handleSignal() and Status.update() are not properly coordinated during WINCH (window change) signal handling.
Environment
- JLINE Version: 3.30.4 (also affects earlier 3.x versions)
- Operating System: macOS (reproducible on Linux)
- Terminal Emulators: Terminal.app, iTerm2, and others supporting SIGWINCH
- Java Version: Java 8+
Steps to Reproduce
- Create a JLINE3
LineReaderwith aStatusbar enabled - Display status bar content using
Status.update(lines) - Vertically resize the terminal window (drag to increase/decrease rows)
- Observe the status bar behavior during and after resize
Expected Behavior
When the terminal is resized:
- Status bar should remain at the bottom of the new terminal size
- No flickering or duplicate rendering
- Main content area (readline buffer) should remain visible and properly positioned
- Scroll region should be cleanly updated to account for new terminal dimensions
- Cursor should remain at the correct position in the readline buffer
Actual Behavior
When terminal is resized:
- ✗ Status bar flickers and may briefly disappear
- ✗ Status bar sometimes appears duplicated (shown at both old and new positions)
- ✗ Text from the readline buffer scrolls upward unexpectedly
- ✗ Scroll region becomes corrupted, causing content to render in wrong areas
- ✗ Cursor may jump to incorrect positions
Root Cause Analysis
The issue stems from lack of coordination between two components during WINCH signal handling:
1. LineReaderImpl.handleSignal() - Resize Handler
File: reader/src/main/java/org/jline/reader/impl/LineReaderImpl.java
Lines: 1259-1283
code used to roproduce the issue :
import java.io.IOException;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.*;
import org.jline.reader.*;
import org.jline.reader.impl.LineReaderImpl;
import org.jline.terminal.*;
import org.jline.utils.*;
/**
* Simple JLine Terminal Demo with Status Bar
*
* This demonstrates:
* - Basic terminal setup
* - LineReader with custom prompt
* - Status bar with multiple components (time, memory, line count)
* - WINCH signal handling
* - Cursor and display management
*
* To run: java -cp jline-3.x.x.jar SimpleTerminalDemo.java
*/
public class SimpleTerminalDemo {
public static void main(String[] args) throws IOException {
System.err.println("===== STDERR TEST - If you see this, stderr is working! =====");
System.out.println("System.out.println working ? ");
// Create terminal
Terminal terminal = TerminalBuilder.builder()
.system(true)
.build();
System.err.println("===== AFTER TERMINAL CREATED - Can you see this? =====");
// Create line reader
LineReader reader = LineReaderBuilder.builder()
.terminal(terminal)
.build();
// Create and setup status bar
SimpleStatusBar statusBar = new SimpleStatusBar(terminal, (LineReaderImpl) reader);
statusBar.enable();
// Print welcome message
terminal.writer().println("=== JLine Terminal Demo ===");
terminal.writer().println("Type 'help' for commands, 'exit' to quit");
terminal.writer().println();
terminal.writer().flush();
// Main input loop
int lineCount = 0;
while (true) {
try {
// Read line with prompt
String line = reader.readLine("demo> ");
lineCount++;
// Update status bar
statusBar.setLineCount(lineCount);
statusBar.refresh();
// Process commands
if (line == null || line.trim().equalsIgnoreCase("exit") || line.trim().equalsIgnoreCase("quit")) {
break;
}
if (line.trim().equalsIgnoreCase("help")) {
terminal.writer().println("Commands:");
terminal.writer().println(" help - Show this help");
terminal.writer().println(" clear - Clear screen");
terminal.writer().println(" status - Toggle status bar");
terminal.writer().println(" echo - Echo your input");
terminal.writer().println(" exit - Exit demo");
} else if (line.trim().equalsIgnoreCase("clear")) {
terminal.puts(InfoCmp.Capability.clear_screen);
terminal.flush();
} else if (line.trim().equalsIgnoreCase("status")) {
statusBar.toggle();
} else if (line.trim().startsWith("echo ")) {
terminal.writer().println("You said: " + line.substring(5));
} else if (!line.trim().isEmpty()) {
terminal.writer().println("Unknown command: " + line);
terminal.writer().println("Type 'help' for available commands");
}
terminal.writer().flush();
} catch (UserInterruptException e) {
// Ctrl+C pressed
terminal.writer().println("^C");
terminal.writer().flush();
} catch (EndOfFileException e) {
// Ctrl+D pressed
break;
}
}
// Cleanup
statusBar.shutdown();
statusBar.disable();
terminal.writer().println("\nGoodbye!");
terminal.writer().flush();
terminal.close();
}
/**
* Simple Status Bar with Time, Memory, and Line Count components
*/
static class SimpleStatusBar {
private final Terminal terminal;
private final LineReaderImpl reader;
private final Status status;
private boolean enabled = false;
private int lineCount = 0;
// Debounce mechanism for resize
private final ScheduledExecutorService resizeExecutor;
private volatile ScheduledFuture<?> pendingRestore = null;
private static final int RESIZE_SETTLE_MS = 150; // Wait 150ms after last WINCH
// Components
private final TimeComponent timeComponent = new TimeComponent();
private final MemoryComponent memoryComponent = new MemoryComponent();
private final LineCountComponent lineCountComponent = new LineCountComponent();
public SimpleStatusBar(Terminal terminal, LineReaderImpl reader) {
this.terminal = terminal;
this.reader = reader;
this.status = Status.getStatus(terminal, true);
// Create executor for debouncing
this.resizeExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "StatusBar-Resize");
t.setDaemon(true);
return t;
});
// Install WINCH handler with debouncing
installWinchHandler();
}
public void enable() {
if (!enabled) {
enabled = true;
terminal.writer().write('\n');
terminal.writer().flush();
refresh();
}
}
public void disable() {
if (enabled) {
enabled = false;
status.update(Collections.emptyList());
}
}
public void toggle() {
if (enabled) {
disable();
terminal.writer().println("Status bar disabled");
} else {
enable();
terminal.writer().println("Status bar enabled");
}
terminal.writer().flush();
}
public void setLineCount(int count) {
this.lineCount = count;
}
public void refresh() {
if (!enabled) {
return;
}
// Build status line
AttributedStringBuilder builder = new AttributedStringBuilder();
builder.append(timeComponent.render());
builder.append(" ¦ ");
builder.append(memoryComponent.render());
builder.append(" ¦ ");
lineCountComponent.setLineCount(lineCount);
builder.append(lineCountComponent.render());
// Update status
status.update(Collections.singletonList(builder.toAttributedString()));
}
private void installWinchHandler() {
// Get JLine's WINCH handler
Terminal.SignalHandler jlineWinch = terminal.handle(Terminal.Signal.WINCH, s -> {});
// Install our debounced handler
terminal.handle(Terminal.Signal.WINCH, signal -> {
// Step 1: Let JLine do its resize first
if (jlineWinch != null) {
jlineWinch.handle(signal);
}
if (!enabled) {
return; // Nothing to do if disabled
}
// Step 2: Hide status bar immediately (prevents scroll region issues)
status.update(Collections.emptyList());
// Step 3: Cancel any pending restore
if (pendingRestore != null && !pendingRestore.isDone()) {
pendingRestore.cancel(false);
}
// Step 4: Schedule restore after resize settles (150ms of quiet)
pendingRestore = resizeExecutor.schedule(() -> {
try {
// Resize status to new terminal size
status.resize();
// Wait a bit more for terminal to settle
Thread.sleep(50);
// Restore status bar
refresh();
} catch (Exception e) {
// Ignore errors during restore
}
}, RESIZE_SETTLE_MS, TimeUnit.MILLISECONDS);
});
}
public void shutdown() {
// Cancel pending tasks
if (pendingRestore != null && !pendingRestore.isDone()) {
pendingRestore.cancel(false);
}
// Shutdown executor
resizeExecutor.shutdown();
try {
if (!resizeExecutor.awaitTermination(1, TimeUnit.SECONDS)) {
resizeExecutor.shutdownNow();
}
} catch (InterruptedException e) {
resizeExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
// ========== Status Bar Components ==========
/**
* Shows current time
*/
static class TimeComponent {
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");
public AttributedString render() {
String time = LocalTime.now().format(formatter);
return new AttributedStringBuilder()
.style(AttributedStyle.DEFAULT.foreground(AttributedStyle.CYAN))
.append("⏰ ")
.style(AttributedStyle.DEFAULT)
.append(time)
.toAttributedString();
}
}
/**
* Shows memory usage
*/
static class MemoryComponent {
public AttributedString render() {
Runtime runtime = Runtime.getRuntime();
long usedMemory = (runtime.totalMemory() - runtime.freeMemory()) / (1024 * 1024);
long totalMemory = runtime.maxMemory() / (1024 * 1024);
String memoryText = String.format("%dMB / %dMB", usedMemory, totalMemory);
return new AttributedStringBuilder()
.style(AttributedStyle.DEFAULT.foreground(AttributedStyle.GREEN))
.append("💾 ")
.style(AttributedStyle.DEFAULT)
.append(memoryText)
.toAttributedString();
}
}
/**
* Shows line count
*/
static class LineCountComponent {
private int lineCount = 0;
public void setLineCount(int count) {
this.lineCount = count;
}
public AttributedString render() {
String text = String.format("Lines: %d", lineCount);
return new AttributedStringBuilder()
.style(AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW))
.append("📝 ")
.style(AttributedStyle.DEFAULT)
.append(text)
.toAttributedString();
}
}
}
}
```
run-bash.sh:
#!/bin/bash
# Static JLine jar path
JLINE_JAR="put your path to jline.jar here"
if [ ! -f "$JLINE_JAR" ]; then
echo "Error: JLine jar not found at: $JLINE_JAR"
exit 1
fi
echo "Using JLine jar: $JLINE_JAR"
# Compile
javac -cp "$JLINE_JAR" SimpleTerminalDemo.java
if [ $? -ne 0 ]; then
echo "Compilation failed!"
exit 1
fi
echo "Running demo..."
echo ""
# Run
java -cp ".:$JLINE_JAR" SimpleTerminalDemo
# Cleanup
rm -f SimpleTerminalDemo.class SimpleTerminalDemo\$SimpleStatusBar*.class
Metadata
Metadata
Assignees
Labels
No labels