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;
import javafx.application.Platform;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
@@ -94,23 +92,25 @@ public final class ShellSession implements AutoCloseable {
}
private void readOutput(TerminalPane pane) {
byte[] buffer = new byte[8192];
byte[] buffer = new byte[65536];
try {
int read;
while ((read = pty.read(buffer)) != -1) {
if (!closed) {
byte[] bytes = new byte[read];
System.arraycopy(buffer, 0, bytes, 0, read);
Platform.runLater(() -> {
if (!closed) {
pane.write(bytes);
}
});
if (closed) {
break;
}
byte[] bytes = new byte[read];
System.arraycopy(buffer, 0, bytes, 0, read);
// Feed the terminal model straight from the reader thread. terminal access is
// 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);
}
} catch (RuntimeException ex) {
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();
}
List<TerminalPane> visible = panes.stream().filter(TerminalPane::visible).toList();
TerminalPane active = activePane();
if (!active.visible() || !active.floating()) {
return visible;
if (visible.isEmpty()) {
return List.of();
}
// 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());
visible.stream()
.filter(pane -> pane != active)
.forEach(ordered::add);
ordered.add(active);
for (TerminalPane pane : visible) {
if (!pane.floating()) {
ordered.add(pane);
}
}
for (TerminalPane pane : visible) {
if (pane.floating() && pane != active) {
ordered.add(pane);
}
}
if (active.visible() && active.floating()) {
ordered.add(active);
}
return List.copyOf(ordered);
}
@@ -155,7 +166,12 @@ final class Tab implements AutoCloseable {
void closeActivePane() {
TerminalPane active = activePane();
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);
if (active == lastFocusedFloating) {
lastFocusedFloating = null;
@@ -165,7 +181,7 @@ final class Tab implements AutoCloseable {
activeIndex = 0;
return;
}
activeIndex = adjustIndexAfterRemoval(previous, removed);
activeIndex = adjustIndexAfterRemoval(target, removed);
hiddenFloatingFocusIndex = adjustHiddenFocusAfterRemoval(hiddenFloatingFocusIndex, removed);
// 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;
}
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) {
for (int i = index - 1; i >= 0; i--) {
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
// the panes that actually changed.
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 double fontSize;
private Font cachedFont;
@@ -126,24 +129,21 @@ public final class TerminalCanvasView {
lastWorkspaceVersion = workspaceVersion;
lastRenderTick = renderTick;
double topInset = workspace.tabCount() > 1 ? TAB_BAR_HEIGHT : 0.0;
workspace.layout(width, height, topInset);
Font font = currentFont();
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();
gc.setFontSmoothingType(FontSmoothingType.LCD);
if (layoutChanged) {
// Recomposite everything onto the retained canvas: clear, then paint panes
// bottom-to-top (workspace.panes() puts the active floating pane last == on top).
// Geometry/pane-set changed: relayout, resize terminals, and recomposite the
// 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);
gc.setFill(GAP_BACKGROUND);
gc.fillRect(0, 0, width, height);
@@ -157,8 +157,9 @@ public final class TerminalCanvasView {
return;
}
// Content-only frame: repaint just the panes whose content changed, directly on the
// retained canvas, then restore any panes stacked above where they overlap.
// Content-only frame: geometry is unchanged, so skip layout/panes/resize entirely and
// reuse the cached pane list — repaint just the panes whose content changed.
List<TerminalPane> panes = cachedPanes;
for (int i = 0; i < panes.size(); i++) {
TerminalPane pane = panes.get(i);
Long drawn = paneContentVersion.get(pane);
@@ -197,12 +198,16 @@ public final class TerminalCanvasView {
double ph = pane.height();
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 regionY1;
gc.save();
clipRect(gc, px, py, pw, ph);
if (kitty) {
drawPaneContent(gc, pane, font, metrics, pane.renderSnapshotFull(), px, py, pw, ph, true);
if (kitty || forceFull) {
drawPaneContent(gc, pane, font, metrics, pane.renderSnapshotFull(), px, py, pw, ph, kitty);
regionY0 = py;
regionY1 = py + ph;
} else {

View File

@@ -29,7 +29,7 @@ public final class TerminalPane implements AutoCloseable {
private final MouseEncoder mouseEncoder = new MouseEncoder();
// A persistent render state (reused across frames) is what makes ghostty's per-row
// 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 ShellSession session;
private boolean floating;
@@ -42,8 +42,12 @@ public final class TerminalPane implements AutoCloseable {
private int rows;
private int pixelWidth;
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 volatile boolean closed;
private boolean needsFullRender;
private TerminalPane(Terminal terminal, int columns, int rows) {
this.terminal = terminal;
@@ -61,6 +65,9 @@ public final class TerminalPane implements AutoCloseable {
public void write(String text) {
synchronized (terminal) {
if (closed) {
return;
}
terminal.write(text);
refresh();
}
@@ -68,6 +75,9 @@ public final class TerminalPane implements AutoCloseable {
public void write(byte[] bytes) {
synchronized (terminal) {
if (closed) {
return;
}
terminal.write(bytes);
refresh();
}
@@ -231,10 +241,27 @@ public final class TerminalPane implements AutoCloseable {
this.rows = rows;
this.pixelWidth = pixelWidth;
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();
}
}
/** 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() {
// 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.
@@ -244,12 +271,19 @@ public final class TerminalPane implements AutoCloseable {
@Override
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) {
session.close();
session = null;
}
mouseEncoder.close();
renderState.close();
terminal.close();
synchronized (terminal) {
terminal.close();
}
}
}