Compare commits
3 Commits
e2850f067e
...
76f539d34a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76f539d34a | ||
|
|
ba884cd0a2 | ||
|
|
7dbbf89b27 |
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user