recover pane
This commit is contained in:
@@ -1,35 +1,46 @@
|
||||
package com.gregor.jprototerm;
|
||||
|
||||
import dev.jlibghostty.DeviceAttributes;
|
||||
import dev.jlibghostty.Ghostty;
|
||||
import dev.jlibghostty.KittyGraphics;
|
||||
import dev.jlibghostty.MouseAction;
|
||||
import dev.jlibghostty.MouseEncoder;
|
||||
import dev.jlibghostty.MouseEncoderSize;
|
||||
import dev.jlibghostty.MouseInput;
|
||||
import dev.jlibghostty.RenderState;
|
||||
import dev.jlibghostty.RenderStateSnapshot;
|
||||
import dev.jlibghostty.ScrollViewport;
|
||||
import dev.jlibghostty.Terminal;
|
||||
import dev.jlibghostty.TerminalOptions;
|
||||
import dev.jlibghostty.DeviceAttributes;
|
||||
import javafx.scene.canvas.GraphicsContext;
|
||||
import javafx.scene.shape.Shape;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public final class TerminalPane implements AutoCloseable {
|
||||
// Monotonic across all panes, bumped on every content change. Lets the renderer detect
|
||||
// "nothing changed" in O(1) without scanning panes or building a render key.
|
||||
private static final AtomicLong RENDER_TICK = new AtomicLong();
|
||||
|
||||
public static long renderTick() {
|
||||
return RENDER_TICK.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* One terminal: owns its ghostty {@link Terminal}, the {@link ShellSession}/pty driving it,
|
||||
* and its on-screen geometry and grid. It does not draw itself — it is a {@link RenderTarget}
|
||||
* that a {@link TerminalRenderer} paints. {@link #paintFull}/{@link #paintIncremental} are the
|
||||
* only rendering API exposed to the {@link Compositor}, and they just delegate to that
|
||||
* renderer; the compositor decides z-order and which rect each pane occupies.
|
||||
*/
|
||||
public final class TerminalPane implements AutoCloseable, RenderTarget {
|
||||
private final Terminal terminal;
|
||||
private final TerminalMetrics metrics;
|
||||
private final boolean kittyEnabled;
|
||||
// Run on every content change so the owning tab can bump its content version — the
|
||||
// compositor's O(1) "did the current tab change?" gate.
|
||||
private final Runnable onContentChange;
|
||||
private final TerminalRenderer renderer;
|
||||
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 ShellSession session;
|
||||
private boolean floating;
|
||||
private boolean visible = true;
|
||||
// Clip region for rendering (rect minus the panes covering this one), set at layout time;
|
||||
// null means clip to the plain bounds. See RenderTarget#clip().
|
||||
private Shape clip;
|
||||
private double x;
|
||||
private double y;
|
||||
private double width;
|
||||
@@ -38,47 +49,40 @@ public final class TerminalPane implements AutoCloseable {
|
||||
private int rows;
|
||||
private int pixelWidth;
|
||||
private int pixelHeight;
|
||||
// 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 contentVersion;
|
||||
private long snapshotVersion = -1;
|
||||
private volatile boolean closed;
|
||||
|
||||
private TerminalPane(Terminal terminal, int columns, int rows) {
|
||||
private TerminalPane(Terminal terminal, TerminalMetrics metrics, boolean kittyEnabled,
|
||||
Runnable onContentChange, TerminalRenderer renderer, int columns, int rows) {
|
||||
this.terminal = terminal;
|
||||
this.metrics = metrics;
|
||||
this.kittyEnabled = kittyEnabled;
|
||||
this.onContentChange = onContentChange;
|
||||
this.renderer = renderer;
|
||||
this.columns = columns;
|
||||
this.rows = rows;
|
||||
}
|
||||
|
||||
public static TerminalPane create(int columns, int rows, long maxScrollback) {
|
||||
Terminal terminal = Ghostty.open(new TerminalOptions(columns, rows, maxScrollback));
|
||||
/**
|
||||
* Opens a pane sized to fit the given pixel rect: the shared cell metrics decide how many
|
||||
* columns and rows fit, and that grid is handed to ghostty and the shell at start-up. A
|
||||
* non-positive size falls back to the configured default grid (used before the first
|
||||
* layout, when no rect is known yet). The pane owns the shell session it starts and runs
|
||||
* {@code onContentChange} on every content change.
|
||||
*/
|
||||
public static TerminalPane create(AppConfig config, TerminalMetrics metrics, Runnable onContentChange, double widthPx, double heightPx) {
|
||||
int columns = widthPx > 0 ? metrics.columnsFor(widthPx) : config.columns();
|
||||
int rows = heightPx > 0 ? metrics.rowsFor(heightPx) : config.rows();
|
||||
Terminal terminal = Ghostty.open(new TerminalOptions(columns, rows, config.maxScrollback()));
|
||||
terminal.setDeviceAttributesProvider(DeviceAttributes::xtermCompatible);
|
||||
TerminalPane pane = new TerminalPane(terminal, columns, rows);
|
||||
TerminalPane pane = new TerminalPane(terminal, metrics, config.kittyGraphics(), onContentChange,
|
||||
new GhosttyTerminalRenderer(metrics), columns, rows);
|
||||
pane.refresh();
|
||||
pane.attach(ShellSession.start(config.shell(), config.envOverride(), pane, columns, rows));
|
||||
return pane;
|
||||
}
|
||||
|
||||
public void write(String text) {
|
||||
synchronized (terminal) {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
terminal.write(text);
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public void write(byte[] bytes) {
|
||||
synchronized (terminal) {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
terminal.write(bytes);
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public void attach(ShellSession session) {
|
||||
private void attach(ShellSession session) {
|
||||
this.session = session;
|
||||
terminal.setPtyWriter(bytes -> {
|
||||
ShellSession current = this.session;
|
||||
@@ -89,6 +93,20 @@ public final class TerminalPane implements AutoCloseable {
|
||||
session.startReading(this);
|
||||
}
|
||||
|
||||
public void write(String text) {
|
||||
synchronized (terminal) {
|
||||
terminal.write(text);
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public void write(byte[] bytes) {
|
||||
synchronized (terminal) {
|
||||
terminal.write(bytes);
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public void send(String text) {
|
||||
scrollViewportToBottom();
|
||||
if (session != null) {
|
||||
@@ -122,7 +140,7 @@ public final class TerminalPane implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
public void scrollViewportToBottom() {
|
||||
private void scrollViewportToBottom() {
|
||||
synchronized (terminal) {
|
||||
terminal.scrollViewport(ScrollViewport.bottom());
|
||||
refresh();
|
||||
@@ -130,16 +148,37 @@ public final class TerminalPane implements AutoCloseable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Full render snapshot of the current screen, memoised per content version (so a burst
|
||||
* of writes between two frames yields one snapshot). Uses a throwaway render state per
|
||||
* snapshot, which always returns the complete, correct screen — a persistent render
|
||||
* state's per-row dirty tracking proved unreliable across resizes and screen clears.
|
||||
* Incremental snapshot: cells are marshalled only for rows that changed since the last
|
||||
* frame (global dirty == PARTIAL), reused across calls for the same content version.
|
||||
* Snapshotting is deferred here rather than done in refresh(), so a burst of writes
|
||||
* between two frames collapses into a single snapshot.
|
||||
*/
|
||||
public RenderStateSnapshot renderSnapshot() {
|
||||
@Override
|
||||
public RenderStateSnapshot snapshot() {
|
||||
return takeSnapshot(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Full snapshot with every row's cells populated. Used where the whole pane is redrawn
|
||||
* regardless of dirty state (the kitty-graphics path).
|
||||
*/
|
||||
@Override
|
||||
public RenderStateSnapshot snapshotFull() {
|
||||
return takeSnapshot(true);
|
||||
}
|
||||
|
||||
private RenderStateSnapshot takeSnapshot(boolean full) {
|
||||
synchronized (terminal) {
|
||||
if (snapshotVersion != renderVersion) {
|
||||
cachedSnapshot = terminal.renderSnapshot();
|
||||
snapshotVersion = renderVersion;
|
||||
if (full) {
|
||||
renderState.update(terminal);
|
||||
cachedSnapshot = renderState.snapshot();
|
||||
renderState.resetDirty();
|
||||
snapshotVersion = contentVersion;
|
||||
} else if (snapshotVersion != contentVersion) {
|
||||
renderState.update(terminal);
|
||||
cachedSnapshot = renderState.snapshotIncremental();
|
||||
renderState.resetDirty();
|
||||
snapshotVersion = contentVersion;
|
||||
}
|
||||
return cachedSnapshot;
|
||||
}
|
||||
@@ -151,44 +190,39 @@ public final class TerminalPane implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
public long renderVersion() {
|
||||
return renderVersion;
|
||||
/** This pane's own content revision, bumped on every change (see {@link #refresh()}). */
|
||||
public long contentVersion() {
|
||||
return contentVersion;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean kittyEnabled() {
|
||||
return kittyEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<KittyGraphics> kittyGraphics() {
|
||||
synchronized (terminal) {
|
||||
return terminal.kittyGraphics();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean floating() {
|
||||
return floating;
|
||||
}
|
||||
|
||||
public void setFloating(boolean floating) {
|
||||
this.floating = floating;
|
||||
}
|
||||
|
||||
public boolean visible() {
|
||||
return visible;
|
||||
}
|
||||
|
||||
public void setVisible(boolean visible) {
|
||||
this.visible = visible;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double x() {
|
||||
return x;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double y() {
|
||||
return y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double width() {
|
||||
return width;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double height() {
|
||||
return height;
|
||||
}
|
||||
@@ -200,7 +234,24 @@ public final class TerminalPane implements AutoCloseable {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public void resize(int columns, int rows, int pixelWidth, int pixelHeight) {
|
||||
/** Set the clip region applied on the next paints (see {@link RenderTarget#clip()}). */
|
||||
public void setClip(Shape clip) {
|
||||
this.clip = clip;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Shape clip() {
|
||||
return clip;
|
||||
}
|
||||
|
||||
/** Recompute the ghostty grid from the current bounds and the shared cell metrics. */
|
||||
public void fitToBounds() {
|
||||
int columns = metrics.columnsFor(width);
|
||||
int rows = metrics.rowsFor(height);
|
||||
resize(columns, rows, (int) Math.round(metrics.cellWidth()), (int) Math.round(metrics.lineHeight()));
|
||||
}
|
||||
|
||||
private void resize(int columns, int rows, int pixelWidth, int pixelHeight) {
|
||||
if (columns <= 0 || rows <= 0 || pixelWidth <= 0 || pixelHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
@@ -222,26 +273,31 @@ public final class TerminalPane implements AutoCloseable {
|
||||
}
|
||||
|
||||
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.
|
||||
renderVersion++;
|
||||
RENDER_TICK.incrementAndGet();
|
||||
// Mark this pane's content dirty (the snapshot is computed lazily in the paint path,
|
||||
// so a burst of writes collapses into one snapshot per frame) and tell the owning tab
|
||||
// one of its panes changed.
|
||||
contentVersion++;
|
||||
onContentChange.run();
|
||||
}
|
||||
|
||||
/** Paint the whole pane; see {@link TerminalRenderer#paintFull}. */
|
||||
public void paintFull(GraphicsContext gc, boolean active) {
|
||||
renderer.paintFull(gc, this, active);
|
||||
}
|
||||
|
||||
/** Repaint what changed; see {@link TerminalRenderer#paintIncremental}. */
|
||||
public void paintIncremental(GraphicsContext gc, boolean active) {
|
||||
renderer.paintIncremental(gc, this, active);
|
||||
}
|
||||
|
||||
@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();
|
||||
synchronized (terminal) {
|
||||
terminal.close();
|
||||
}
|
||||
renderState.close();
|
||||
terminal.close();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user