non fix of clearing issue

This commit is contained in:
Gregor Lohaus
2026-05-30 01:13:53 +02:00
parent e2850f067e
commit 7dbbf89b27
4 changed files with 110 additions and 41 deletions

View File

@@ -1,7 +1,5 @@
package com.gregor.jprototerm; package com.gregor.jprototerm;
import javafx.application.Platform;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@@ -94,23 +92,25 @@ public final class ShellSession implements AutoCloseable {
} }
private void readOutput(TerminalPane pane) { private void readOutput(TerminalPane pane) {
byte[] buffer = new byte[8192]; byte[] buffer = new byte[65536];
try { try {
int read; int read;
while ((read = pty.read(buffer)) != -1) { while ((read = pty.read(buffer)) != -1) {
if (!closed) { if (closed) {
break;
}
byte[] bytes = new byte[read]; byte[] bytes = new byte[read];
System.arraycopy(buffer, 0, bytes, 0, read); System.arraycopy(buffer, 0, bytes, 0, read);
Platform.runLater(() -> { // Feed the terminal model straight from the reader thread. terminal access is
if (!closed) { // guarded by the per-terminal lock, and the render loop picks the change up on
// the next pulse. Avoiding a Platform.runLater hop per chunk removes a frame of
// latency and stops write tasks from contending with rendering on the FX thread
// when a TUI repaints heavily (the input-lag culprit).
pane.write(bytes); pane.write(bytes);
} }
});
}
}
} catch (RuntimeException ex) { } catch (RuntimeException ex) {
if (!closed) { if (!closed) {
Platform.runLater(() -> pane.write("\r\nshell output stopped: " + ex.getMessage() + "\r\n")); pane.write("\r\nshell output stopped: " + ex.getMessage() + "\r\n");
} }
} }
} }

View File

@@ -35,16 +35,27 @@ final class Tab implements AutoCloseable {
return List.of(); return List.of();
} }
List<TerminalPane> visible = panes.stream().filter(TerminalPane::visible).toList(); List<TerminalPane> visible = panes.stream().filter(TerminalPane::visible).toList();
TerminalPane active = activePane(); if (visible.isEmpty()) {
if (!active.visible() || !active.floating()) { return List.of();
return visible;
} }
// Draw order = z-order: all tiled panes first (they never overlap), then floating
// panes on top, with the active floating pane last (topmost). This holds regardless
// of creation order, so a tiled pane created after a floating one still sits behind.
TerminalPane active = activePane();
List<TerminalPane> ordered = new ArrayList<>(visible.size()); List<TerminalPane> ordered = new ArrayList<>(visible.size());
visible.stream() for (TerminalPane pane : visible) {
.filter(pane -> pane != active) if (!pane.floating()) {
.forEach(ordered::add); ordered.add(pane);
}
}
for (TerminalPane pane : visible) {
if (pane.floating() && pane != active) {
ordered.add(pane);
}
}
if (active.visible() && active.floating()) {
ordered.add(active); ordered.add(active);
}
return List.copyOf(ordered); return List.copyOf(ordered);
} }
@@ -155,7 +166,12 @@ final class Tab implements AutoCloseable {
void closeActivePane() { void closeActivePane() {
TerminalPane active = activePane(); TerminalPane active = activePane();
int removed = activeIndex; int removed = activeIndex;
int previous = previousVisibleIndex(removed); // When closing a floating pane, focus the next visible floating pane if there is one
// (don't jump to a tiled pane); otherwise fall back to the nearest visible pane.
int target = active.floating() ? nearestVisibleFloatingIndex(removed) : -1;
if (target < 0) {
target = previousVisibleIndex(removed);
}
panes.remove(removed); panes.remove(removed);
if (active == lastFocusedFloating) { if (active == lastFocusedFloating) {
lastFocusedFloating = null; lastFocusedFloating = null;
@@ -165,7 +181,7 @@ final class Tab implements AutoCloseable {
activeIndex = 0; activeIndex = 0;
return; return;
} }
activeIndex = adjustIndexAfterRemoval(previous, removed); activeIndex = adjustIndexAfterRemoval(target, removed);
hiddenFloatingFocusIndex = adjustHiddenFocusAfterRemoval(hiddenFloatingFocusIndex, removed); hiddenFloatingFocusIndex = adjustHiddenFocusAfterRemoval(hiddenFloatingFocusIndex, removed);
// If the last tiled (main) pane was closed, promote a floating pane to be the new // If the last tiled (main) pane was closed, promote a floating pane to be the new
@@ -273,6 +289,20 @@ final class Tab implements AutoCloseable {
return 0; return 0;
} }
private int nearestVisibleFloatingIndex(int index) {
for (int i = index + 1; i < panes.size(); i++) {
if (panes.get(i).visible() && panes.get(i).floating()) {
return i;
}
}
for (int i = index - 1; i >= 0; i--) {
if (panes.get(i).visible() && panes.get(i).floating()) {
return i;
}
}
return -1;
}
private int previousVisibleIndex(int index) { private int previousVisibleIndex(int index) {
for (int i = index - 1; i >= 0; i--) { for (int i = index - 1; i >= 0; i--) {
if (panes.get(i).visible()) { if (panes.get(i).visible()) {

View File

@@ -54,6 +54,9 @@ public final class TerminalCanvasView {
// Last content version drawn to the canvas per pane, so a content frame repaints only // Last content version drawn to the canvas per pane, so a content frame repaints only
// the panes that actually changed. // the panes that actually changed.
private final Map<TerminalPane, Long> paneContentVersion = new HashMap<>(); private final Map<TerminalPane, Long> paneContentVersion = new HashMap<>();
// Pane list from the last layout pass; reused on content-only frames so typing doesn't
// re-run layout()/panes()/resize each frame.
private List<TerminalPane> cachedPanes = List.of();
private String fontFamily; private String fontFamily;
private double fontSize; private double fontSize;
private Font cachedFont; private Font cachedFont;
@@ -126,24 +129,21 @@ public final class TerminalCanvasView {
lastWorkspaceVersion = workspaceVersion; lastWorkspaceVersion = workspaceVersion;
lastRenderTick = renderTick; lastRenderTick = renderTick;
double topInset = workspace.tabCount() > 1 ? TAB_BAR_HEIGHT : 0.0;
workspace.layout(width, height, topInset);
Font font = currentFont(); Font font = currentFont();
FontMetrics metrics = currentFontMetrics(); FontMetrics metrics = currentFontMetrics();
List<TerminalPane> panes = workspace.panes();
// Apply terminal resizes up front so snapshots reflect current geometry (a no-op
// when the grid is unchanged).
for (TerminalPane pane : panes) {
applyResize(pane, metrics);
}
GraphicsContext gc = canvas.getGraphicsContext2D(); GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setFontSmoothingType(FontSmoothingType.LCD); gc.setFontSmoothingType(FontSmoothingType.LCD);
if (layoutChanged) { if (layoutChanged) {
// Recomposite everything onto the retained canvas: clear, then paint panes // Geometry/pane-set changed: relayout, resize terminals, and recomposite the
// bottom-to-top (workspace.panes() puts the active floating pane last == on top). // whole canvas, painting panes bottom-to-top (active floating pane last == top).
double topInset = workspace.tabCount() > 1 ? TAB_BAR_HEIGHT : 0.0;
workspace.layout(width, height, topInset);
List<TerminalPane> panes = workspace.panes();
for (TerminalPane pane : panes) {
applyResize(pane, metrics);
}
cachedPanes = panes;
paneContentVersion.keySet().retainAll(panes); paneContentVersion.keySet().retainAll(panes);
gc.setFill(GAP_BACKGROUND); gc.setFill(GAP_BACKGROUND);
gc.fillRect(0, 0, width, height); gc.fillRect(0, 0, width, height);
@@ -157,8 +157,9 @@ public final class TerminalCanvasView {
return; return;
} }
// Content-only frame: repaint just the panes whose content changed, directly on the // Content-only frame: geometry is unchanged, so skip layout/panes/resize entirely and
// retained canvas, then restore any panes stacked above where they overlap. // reuse the cached pane list — repaint just the panes whose content changed.
List<TerminalPane> panes = cachedPanes;
for (int i = 0; i < panes.size(); i++) { for (int i = 0; i < panes.size(); i++) {
TerminalPane pane = panes.get(i); TerminalPane pane = panes.get(i);
Long drawn = paneContentVersion.get(pane); Long drawn = paneContentVersion.get(pane);
@@ -197,12 +198,16 @@ public final class TerminalCanvasView {
double ph = pane.height(); double ph = pane.height();
boolean kitty = config.kittyGraphics() && paneHasKittyGraphics(pane); boolean kitty = config.kittyGraphics() && paneHasKittyGraphics(pane);
// A pane just resized (e.g. from a split) can't be trusted to report its dirty rows
// for the app's post-SIGWINCH redraw, so force a full snapshot once.
boolean forceFull = pane.consumeFullRender();
double regionY0; double regionY0;
double regionY1; double regionY1;
gc.save(); gc.save();
clipRect(gc, px, py, pw, ph); clipRect(gc, px, py, pw, ph);
if (kitty) { if (kitty || forceFull) {
drawPaneContent(gc, pane, font, metrics, pane.renderSnapshotFull(), px, py, pw, ph, true); drawPaneContent(gc, pane, font, metrics, pane.renderSnapshotFull(), px, py, pw, ph, kitty);
regionY0 = py; regionY0 = py;
regionY1 = py + ph; regionY1 = py + ph;
} else { } else {

View File

@@ -29,7 +29,7 @@ public final class TerminalPane implements AutoCloseable {
private final MouseEncoder mouseEncoder = new MouseEncoder(); private final MouseEncoder mouseEncoder = new MouseEncoder();
// A persistent render state (reused across frames) is what makes ghostty's per-row // A persistent render state (reused across frames) is what makes ghostty's per-row
// dirty tracking meaningful: update() accumulates dirty since the last resetDirty(). // dirty tracking meaningful: update() accumulates dirty since the last resetDirty().
private final RenderState renderState = new RenderState(); private RenderState renderState = new RenderState();
private RenderStateSnapshot cachedSnapshot; private RenderStateSnapshot cachedSnapshot;
private ShellSession session; private ShellSession session;
private boolean floating; private boolean floating;
@@ -42,8 +42,12 @@ public final class TerminalPane implements AutoCloseable {
private int rows; private int rows;
private int pixelWidth; private int pixelWidth;
private int pixelHeight; private int pixelHeight;
private long renderVersion; // Bumped on the reader thread (terminal writes) and read on the FX thread (render loop),
// so it must be volatile.
private volatile long renderVersion;
private long snapshotVersion = -1; private long snapshotVersion = -1;
private volatile boolean closed;
private boolean needsFullRender;
private TerminalPane(Terminal terminal, int columns, int rows) { private TerminalPane(Terminal terminal, int columns, int rows) {
this.terminal = terminal; this.terminal = terminal;
@@ -61,6 +65,9 @@ public final class TerminalPane implements AutoCloseable {
public void write(String text) { public void write(String text) {
synchronized (terminal) { synchronized (terminal) {
if (closed) {
return;
}
terminal.write(text); terminal.write(text);
refresh(); refresh();
} }
@@ -68,6 +75,9 @@ public final class TerminalPane implements AutoCloseable {
public void write(byte[] bytes) { public void write(byte[] bytes) {
synchronized (terminal) { synchronized (terminal) {
if (closed) {
return;
}
terminal.write(bytes); terminal.write(bytes);
refresh(); refresh();
} }
@@ -231,10 +241,27 @@ public final class TerminalPane implements AutoCloseable {
this.rows = rows; this.rows = rows;
this.pixelWidth = pixelWidth; this.pixelWidth = pixelWidth;
this.pixelHeight = pixelHeight; this.pixelHeight = pixelHeight;
// A persistent render state gets corrupted by a terminal resize: its next snapshot
// comes back blank (a throwaway render state, as ghostty's API was originally used,
// never had this). Recreate it so the next snapshot is a clean full render of the
// resized grid, even for an idle pane that won't redraw on its own.
renderState.close();
renderState = new RenderState();
snapshotVersion = -1;
// The app (e.g. a TUI) also redraws a moment later via SIGWINCH; force the next
// content repaint to use a full snapshot so we don't rely on dirty across the resize.
needsFullRender = true;
refresh(); refresh();
} }
} }
/** Returns and clears the "force a full repaint" flag set by {@link #resize}. */
public boolean consumeFullRender() {
boolean pending = needsFullRender;
needsFullRender = false;
return pending;
}
private void refresh() { private void refresh() {
// Only mark the pane dirty; the snapshot itself is computed lazily in // Only mark the pane dirty; the snapshot itself is computed lazily in
// renderSnapshot() so a burst of writes collapses into a single snapshot per frame. // renderSnapshot() so a burst of writes collapses into a single snapshot per frame.
@@ -244,12 +271,19 @@ public final class TerminalPane implements AutoCloseable {
@Override @Override
public void close() { public void close() {
// Stop accepting reader-thread writes first, then shut the session (which unblocks
// and ends the reader), so terminal.close() can't race a write from that thread.
synchronized (terminal) {
closed = true;
}
if (session != null) { if (session != null) {
session.close(); session.close();
session = null; session = null;
} }
mouseEncoder.close(); mouseEncoder.close();
renderState.close(); renderState.close();
synchronized (terminal) {
terminal.close(); terminal.close();
} }
}
} }