recover pane
This commit is contained in:
@@ -1,35 +1,46 @@
|
|||||||
package com.gregor.jprototerm;
|
package com.gregor.jprototerm;
|
||||||
|
|
||||||
|
import dev.jlibghostty.DeviceAttributes;
|
||||||
import dev.jlibghostty.Ghostty;
|
import dev.jlibghostty.Ghostty;
|
||||||
import dev.jlibghostty.KittyGraphics;
|
import dev.jlibghostty.KittyGraphics;
|
||||||
import dev.jlibghostty.MouseAction;
|
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;
|
||||||
import dev.jlibghostty.TerminalOptions;
|
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.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 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();
|
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;
|
// Clip region for rendering (rect minus the panes covering this one), set at layout time;
|
||||||
private boolean visible = true;
|
// null means clip to the plain bounds. See RenderTarget#clip().
|
||||||
|
private Shape clip;
|
||||||
private double x;
|
private double x;
|
||||||
private double y;
|
private double y;
|
||||||
private double width;
|
private double width;
|
||||||
@@ -38,47 +49,40 @@ public final class TerminalPane implements AutoCloseable {
|
|||||||
private int rows;
|
private int rows;
|
||||||
private int pixelWidth;
|
private int pixelWidth;
|
||||||
private int pixelHeight;
|
private int pixelHeight;
|
||||||
// Bumped on the reader thread (terminal writes) and read on the FX thread (render loop),
|
private long contentVersion;
|
||||||
// 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, TerminalMetrics metrics, boolean kittyEnabled,
|
||||||
|
Runnable onContentChange, TerminalRenderer renderer, int columns, int rows) {
|
||||||
this.terminal = terminal;
|
this.terminal = terminal;
|
||||||
|
this.metrics = metrics;
|
||||||
|
this.kittyEnabled = kittyEnabled;
|
||||||
|
this.onContentChange = onContentChange;
|
||||||
|
this.renderer = renderer;
|
||||||
this.columns = columns;
|
this.columns = columns;
|
||||||
this.rows = rows;
|
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);
|
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.refresh();
|
||||||
|
pane.attach(ShellSession.start(config.shell(), config.envOverride(), pane, columns, rows));
|
||||||
return pane;
|
return pane;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void write(String text) {
|
private void attach(ShellSession session) {
|
||||||
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) {
|
|
||||||
this.session = session;
|
this.session = session;
|
||||||
terminal.setPtyWriter(bytes -> {
|
terminal.setPtyWriter(bytes -> {
|
||||||
ShellSession current = this.session;
|
ShellSession current = this.session;
|
||||||
@@ -89,6 +93,20 @@ public final class TerminalPane implements AutoCloseable {
|
|||||||
session.startReading(this);
|
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) {
|
public void send(String text) {
|
||||||
scrollViewportToBottom();
|
scrollViewportToBottom();
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
@@ -122,7 +140,7 @@ public final class TerminalPane implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void scrollViewportToBottom() {
|
private void scrollViewportToBottom() {
|
||||||
synchronized (terminal) {
|
synchronized (terminal) {
|
||||||
terminal.scrollViewport(ScrollViewport.bottom());
|
terminal.scrollViewport(ScrollViewport.bottom());
|
||||||
refresh();
|
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
|
* Incremental snapshot: cells are marshalled only for rows that changed since the last
|
||||||
* of writes between two frames yields one snapshot). Uses a throwaway render state per
|
* frame (global dirty == PARTIAL), reused across calls for the same content version.
|
||||||
* snapshot, which always returns the complete, correct screen — a persistent render
|
* Snapshotting is deferred here rather than done in refresh(), so a burst of writes
|
||||||
* state's per-row dirty tracking proved unreliable across resizes and screen clears.
|
* 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) {
|
synchronized (terminal) {
|
||||||
if (snapshotVersion != renderVersion) {
|
if (full) {
|
||||||
cachedSnapshot = terminal.renderSnapshot();
|
renderState.update(terminal);
|
||||||
snapshotVersion = renderVersion;
|
cachedSnapshot = renderState.snapshot();
|
||||||
|
renderState.resetDirty();
|
||||||
|
snapshotVersion = contentVersion;
|
||||||
|
} else if (snapshotVersion != contentVersion) {
|
||||||
|
renderState.update(terminal);
|
||||||
|
cachedSnapshot = renderState.snapshotIncremental();
|
||||||
|
renderState.resetDirty();
|
||||||
|
snapshotVersion = contentVersion;
|
||||||
}
|
}
|
||||||
return cachedSnapshot;
|
return cachedSnapshot;
|
||||||
}
|
}
|
||||||
@@ -151,44 +190,39 @@ public final class TerminalPane implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public long renderVersion() {
|
/** This pane's own content revision, bumped on every change (see {@link #refresh()}). */
|
||||||
return renderVersion;
|
public long contentVersion() {
|
||||||
|
return contentVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean kittyEnabled() {
|
||||||
|
return kittyEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public Optional<KittyGraphics> kittyGraphics() {
|
public Optional<KittyGraphics> kittyGraphics() {
|
||||||
synchronized (terminal) {
|
synchronized (terminal) {
|
||||||
return terminal.kittyGraphics();
|
return terminal.kittyGraphics();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean floating() {
|
@Override
|
||||||
return floating;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFloating(boolean floating) {
|
|
||||||
this.floating = floating;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean visible() {
|
|
||||||
return visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setVisible(boolean visible) {
|
|
||||||
this.visible = visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
public double x() {
|
public double x() {
|
||||||
return x;
|
return x;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public double y() {
|
public double y() {
|
||||||
return y;
|
return y;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public double width() {
|
public double width() {
|
||||||
return width;
|
return width;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public double height() {
|
public double height() {
|
||||||
return height;
|
return height;
|
||||||
}
|
}
|
||||||
@@ -200,7 +234,24 @@ public final class TerminalPane implements AutoCloseable {
|
|||||||
this.height = height;
|
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) {
|
if (columns <= 0 || rows <= 0 || pixelWidth <= 0 || pixelHeight <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -222,26 +273,31 @@ public final class TerminalPane implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void refresh() {
|
private void refresh() {
|
||||||
// Only mark the pane dirty; the snapshot itself is computed lazily in
|
// Mark this pane's content dirty (the snapshot is computed lazily in the paint path,
|
||||||
// renderSnapshot() so a burst of writes collapses into a single snapshot per frame.
|
// so a burst of writes collapses into one snapshot per frame) and tell the owning tab
|
||||||
renderVersion++;
|
// one of its panes changed.
|
||||||
RENDER_TICK.incrementAndGet();
|
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
|
@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();
|
||||||
synchronized (terminal) {
|
renderState.close();
|
||||||
terminal.close();
|
terminal.close();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user