3 Commits

Author SHA1 Message Date
Gregor Lohaus
76f539d34a fix build error 2026-05-30 01:27:10 +02:00
Gregor Lohaus
ba884cd0a2 probably wrong fix 2026-05-30 01:23:16 +02:00
Gregor Lohaus
7dbbf89b27 non fix of clearing issue 2026-05-30 01:13:53 +02:00
4 changed files with 98 additions and 104 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) {
byte[] bytes = new byte[read]; break;
System.arraycopy(buffer, 0, bytes, 0, read);
Platform.runLater(() -> {
if (!closed) {
pane.write(bytes);
}
});
} }
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) { } 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);
ordered.add(active); }
}
for (TerminalPane pane : visible) {
if (pane.floating() && pane != active) {
ordered.add(pane);
}
}
if (active.visible() && active.floating()) {
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;
@@ -97,9 +100,6 @@ public final class TerminalCanvasView {
} }
// GhosttyRenderStateDirty values (stable C ABI; see ghostty/vt/render.h). // GhosttyRenderStateDirty values (stable C ABI; see ghostty/vt/render.h).
private static final int DIRTY_PARTIAL = 1;
private static final int DIRTY_FULL = 2;
// Thin tab strip shown at the top when more than one tab is open. // Thin tab strip shown at the top when more than one tab is open.
private static final double TAB_BAR_HEIGHT = 22.0; private static final double TAB_BAR_HEIGHT = 22.0;
@@ -126,24 +126,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);
@@ -151,14 +148,15 @@ public final class TerminalCanvasView {
drawTabBar(gc, width, topInset); drawTabBar(gc, width, topInset);
} }
for (TerminalPane pane : panes) { for (TerminalPane pane : panes) {
paintPane(gc, pane, font, metrics, pane.renderSnapshotFull()); paintPane(gc, pane, font, metrics, pane.renderSnapshot());
paneContentVersion.put(pane, pane.renderVersion()); paneContentVersion.put(pane, pane.renderVersion());
} }
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);
@@ -187,46 +185,20 @@ public final class TerminalCanvasView {
gc.restore(); gc.restore();
} }
// Repaint one pane whose content changed, then restore the (opaque) panes stacked above // Repaint one pane whose content changed (a full, reliable repaint of the pane), then
// it wherever they overlap the repainted region, so the z-order stays correct. // restore the (opaque) panes stacked above it where they overlap, keeping z-order.
private void repaintPaneContent(GraphicsContext gc, List<TerminalPane> panes, int index, Font font, FontMetrics metrics) { private void repaintPaneContent(GraphicsContext gc, List<TerminalPane> panes, int index, Font font, FontMetrics metrics) {
TerminalPane pane = panes.get(index); TerminalPane pane = panes.get(index);
double px = Math.round(pane.x()); double px = Math.round(pane.x());
double py = Math.round(pane.y()); double py = Math.round(pane.y());
double pw = pane.width(); double pw = pane.width();
double ph = pane.height(); double ph = pane.height();
boolean kitty = config.kittyGraphics() && paneHasKittyGraphics(pane);
double regionY0;
double regionY1;
gc.save(); gc.save();
clipRect(gc, px, py, pw, ph); clipRect(gc, px, py, pw, ph);
if (kitty) { drawPaneContent(gc, pane, font, metrics, pane.renderSnapshot(), px, py, pw, ph,
drawPaneContent(gc, pane, font, metrics, pane.renderSnapshotFull(), px, py, pw, ph, true); config.kittyGraphics() && paneHasKittyGraphics(pane));
regionY0 = py;
regionY1 = py + ph;
} else {
RenderStateSnapshot snapshot = pane.renderSnapshot();
int dirty = snapshot == null ? DIRTY_FULL : snapshot.dirty();
if (dirty == DIRTY_FULL) {
drawPaneContent(gc, pane, font, metrics, snapshot, px, py, pw, ph, false);
regionY0 = py;
regionY1 = py + ph;
} else if (dirty == DIRTY_PARTIAL) {
double[] band = drawDirtyRows(gc, pane, font, metrics, snapshot, px, py, pw, ph);
gc.restore();
if (band == null) {
return;
}
restoreStackedAbove(gc, panes, index, font, metrics, px, band[0], pw, band[1] - band[0]);
return;
} else {
gc.restore();
return; // dirty == FALSE: nothing visible changed.
}
}
gc.restore(); gc.restore();
restoreStackedAbove(gc, panes, index, font, metrics, px, regionY0, pw, regionY1 - regionY0); restoreStackedAbove(gc, panes, index, font, metrics, px, py, pw, ph);
} }
// Redraw any panes above `index` in z-order that intersect the given screen rect, so a // Redraw any panes above `index` in z-order that intersect the given screen rect, so a
@@ -246,7 +218,7 @@ public final class TerminalCanvasView {
} }
gc.save(); gc.save();
clipRect(gc, ox0, oy0, ox1 - ox0, oy1 - oy0); clipRect(gc, ox0, oy0, ox1 - ox0, oy1 - oy0);
drawPaneContent(gc, above, font, metrics, above.renderSnapshotFull(), ax, ay, above.width(), above.height(), drawPaneContent(gc, above, font, metrics, above.renderSnapshot(), ax, ay, above.width(), above.height(),
config.kittyGraphics() && paneHasKittyGraphics(above)); config.kittyGraphics() && paneHasKittyGraphics(above));
gc.restore(); gc.restore();
} }

View File

@@ -6,7 +6,6 @@ import dev.jlibghostty.MouseAction;
import dev.jlibghostty.MouseEncoder; import dev.jlibghostty.MouseEncoder;
import dev.jlibghostty.MouseEncoderSize; import dev.jlibghostty.MouseEncoderSize;
import dev.jlibghostty.MouseInput; import dev.jlibghostty.MouseInput;
import dev.jlibghostty.RenderState;
import dev.jlibghostty.RenderStateSnapshot; import dev.jlibghostty.RenderStateSnapshot;
import dev.jlibghostty.ScrollViewport; import dev.jlibghostty.ScrollViewport;
import dev.jlibghostty.Terminal; import dev.jlibghostty.Terminal;
@@ -27,9 +26,6 @@ public final class TerminalPane implements AutoCloseable {
private final Terminal terminal; private final Terminal terminal;
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
// dirty tracking meaningful: update() accumulates dirty since the last resetDirty().
private final RenderState renderState = new RenderState();
private RenderStateSnapshot cachedSnapshot; private RenderStateSnapshot cachedSnapshot;
private ShellSession session; private ShellSession session;
private boolean floating; private boolean floating;
@@ -42,8 +38,11 @@ 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 TerminalPane(Terminal terminal, int columns, int rows) { private TerminalPane(Terminal terminal, int columns, int rows) {
this.terminal = terminal; this.terminal = terminal;
@@ -61,6 +60,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 +70,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();
} }
@@ -125,34 +130,15 @@ public final class TerminalPane implements AutoCloseable {
} }
/** /**
* Incremental snapshot: cells are marshalled only for rows that changed since the last * Full render snapshot of the current screen, memoised per content version (so a burst
* frame (global dirty == PARTIAL), reused across calls for the same content version. * of writes between two frames yields one snapshot). Uses a throwaway render state per
* Snapshotting is deferred here rather than done in refresh(), so a burst of writes * snapshot, which always returns the complete, correct screen — a persistent render
* between two frames collapses into a single snapshot. * state's per-row dirty tracking proved unreliable across resizes and screen clears.
*/ */
public RenderStateSnapshot renderSnapshot() { public RenderStateSnapshot renderSnapshot() {
return snapshot(false);
}
/**
* Full snapshot with every row's cells populated. Used where the whole pane is redrawn
* regardless of dirty state (the kitty-graphics path).
*/
public RenderStateSnapshot renderSnapshotFull() {
return snapshot(true);
}
private RenderStateSnapshot snapshot(boolean full) {
synchronized (terminal) { synchronized (terminal) {
if (full) { if (snapshotVersion != renderVersion) {
renderState.update(terminal); cachedSnapshot = terminal.renderSnapshot();
cachedSnapshot = renderState.snapshot();
renderState.resetDirty();
snapshotVersion = renderVersion;
} else if (snapshotVersion != renderVersion) {
renderState.update(terminal);
cachedSnapshot = renderState.snapshotIncremental();
renderState.resetDirty();
snapshotVersion = renderVersion; snapshotVersion = renderVersion;
} }
return cachedSnapshot; return cachedSnapshot;
@@ -244,12 +230,18 @@ 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(); synchronized (terminal) {
terminal.close(); terminal.close();
}
} }
} }