scrollback
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,3 +11,4 @@ devenv.local.yaml
|
||||
# pre-commit
|
||||
.pre-commit-config.yaml
|
||||
build
|
||||
build
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,7 @@
|
||||
[terminal]
|
||||
columns = 100
|
||||
rows = 30
|
||||
max_scrollback = 100000
|
||||
shell = "/bin/bash"
|
||||
font_family = "JetBrainsMono Nerd Font"
|
||||
font_size = 15
|
||||
|
||||
@@ -18,6 +18,7 @@ import java.util.Map;
|
||||
public record AppConfig(
|
||||
int columns,
|
||||
int rows,
|
||||
long maxScrollback,
|
||||
String shell,
|
||||
String fontFamily,
|
||||
double fontSize,
|
||||
@@ -51,6 +52,7 @@ public record AppConfig(
|
||||
return new AppConfig(
|
||||
intValue(document, "terminal.columns", defaults.columns),
|
||||
intValue(document, "terminal.rows", defaults.rows),
|
||||
longValue(document, "terminal.max_scrollback", defaults.maxScrollback),
|
||||
stringValue(document, "terminal.shell", defaults.shell),
|
||||
stringValue(document, "terminal.font_family", defaults.fontFamily),
|
||||
doubleValue(document, "terminal.font_size", defaults.fontSize),
|
||||
@@ -69,6 +71,7 @@ public record AppConfig(
|
||||
return new AppConfig(
|
||||
100,
|
||||
30,
|
||||
100_000,
|
||||
defaultShell(),
|
||||
"JetBrainsMono Nerd Font",
|
||||
15.0,
|
||||
@@ -93,6 +96,7 @@ public record AppConfig(
|
||||
return new AppConfig(
|
||||
columns,
|
||||
rows,
|
||||
maxScrollback,
|
||||
shell,
|
||||
family,
|
||||
size,
|
||||
@@ -154,6 +158,7 @@ public record AppConfig(
|
||||
builder.append("[terminal]\n");
|
||||
builder.append("columns = ").append(columns).append('\n');
|
||||
builder.append("rows = ").append(rows).append('\n');
|
||||
builder.append("max_scrollback = ").append(maxScrollback).append('\n');
|
||||
builder.append("shell = ").append(quoted(shell)).append('\n');
|
||||
builder.append("font_family = ").append(quoted(fontFamily)).append('\n');
|
||||
builder.append("font_size = ").append(trimDouble(fontSize)).append("\n\n");
|
||||
@@ -218,6 +223,18 @@ public record AppConfig(
|
||||
}
|
||||
}
|
||||
|
||||
private static long longValue(TomlTable table, String key, long fallback) {
|
||||
TomlPrimitive primitive = primitive(table, key);
|
||||
if (primitive == null) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
return primitive.asInteger();
|
||||
} catch (RuntimeException ex) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private static double doubleValue(TomlTable table, String key, double fallback) {
|
||||
TomlPrimitive primitive = primitive(table, key);
|
||||
if (primitive == null) {
|
||||
|
||||
@@ -30,7 +30,6 @@ public final class Main extends Application {
|
||||
StackPane root = new StackPane(terminalView.canvas());
|
||||
terminalView.canvas().widthProperty().bind(root.widthProperty());
|
||||
terminalView.canvas().heightProperty().bind(root.heightProperty());
|
||||
terminalView.canvas().setOnMousePressed(event -> terminalView.canvas().requestFocus());
|
||||
|
||||
Scene scene = new Scene(root, config.windowWidth(), config.windowHeight());
|
||||
scene.addEventFilter(KeyEvent.KEY_PRESSED, this::handlePressed);
|
||||
|
||||
@@ -5,6 +5,10 @@ import dev.jlibghostty.KittyImageFormat;
|
||||
import dev.jlibghostty.KittyImageSnapshot;
|
||||
import dev.jlibghostty.KittyPlacement;
|
||||
import dev.jlibghostty.KittyRenderInfo;
|
||||
import dev.jlibghostty.KeyModifiers;
|
||||
import dev.jlibghostty.MouseButton;
|
||||
import dev.jlibghostty.MouseEncoderSize;
|
||||
import dev.jlibghostty.MouseInput;
|
||||
import dev.jlibghostty.RenderCell;
|
||||
import dev.jlibghostty.RenderColor;
|
||||
import dev.jlibghostty.RenderCursorStyle;
|
||||
@@ -15,6 +19,10 @@ import javafx.scene.canvas.GraphicsContext;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.PixelFormat;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.input.InputEvent;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.input.ScrollEvent;
|
||||
import javafx.scene.input.ScrollEvent.VerticalTextScrollUnits;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.scene.text.FontSmoothingType;
|
||||
@@ -34,6 +42,8 @@ public final class TerminalCanvasView {
|
||||
private final Map<Long, Image> kittyImageCache = new HashMap<>();
|
||||
private String fontFamily;
|
||||
private double fontSize;
|
||||
private boolean mouseButtonPressed;
|
||||
private MouseButton pressedButton = MouseButton.UNKNOWN;
|
||||
|
||||
public TerminalCanvasView(TerminalWorkspace workspace, AppConfig config) {
|
||||
this.workspace = workspace;
|
||||
@@ -41,6 +51,11 @@ public final class TerminalCanvasView {
|
||||
this.fontFamily = config.fontFamily();
|
||||
this.fontSize = config.fontSize();
|
||||
canvas.setFocusTraversable(true);
|
||||
canvas.setOnMousePressed(this::handleMousePressed);
|
||||
canvas.setOnMouseReleased(this::handleMouseReleased);
|
||||
canvas.setOnMouseDragged(this::handleMouseDragged);
|
||||
canvas.setOnMouseMoved(this::handleMouseMoved);
|
||||
canvas.setOnScroll(this::handleScroll);
|
||||
}
|
||||
|
||||
public Canvas canvas() {
|
||||
@@ -119,12 +134,187 @@ public final class TerminalCanvasView {
|
||||
double lineHeight = Math.max(1.0, text.getLayoutBounds().getHeight());
|
||||
double baselineOffset = -text.getLayoutBounds().getMinY();
|
||||
|
||||
Text cell = new Text("M");
|
||||
String sample = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
Text cell = new Text(sample);
|
||||
cell.setFont(font);
|
||||
double cellWidth = Math.max(1.0, cell.getLayoutBounds().getWidth());
|
||||
double cellWidth = Math.max(1.0, cell.getLayoutBounds().getWidth() / sample.length());
|
||||
return new FontMetrics(cellWidth, lineHeight, baselineOffset);
|
||||
}
|
||||
|
||||
private void handleMousePressed(MouseEvent event) {
|
||||
canvas.requestFocus();
|
||||
TerminalPane pane = paneAt(event.getX(), event.getY());
|
||||
if (pane == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
workspace.focus(pane);
|
||||
pressedButton = mouseButton(event);
|
||||
mouseButtonPressed = true;
|
||||
sendMouse(pane, MouseInput.press(pressedButton, eventX(pane, event.getX()), eventY(pane, event.getY()), modifiers(event)), true, event);
|
||||
}
|
||||
|
||||
private void handleMouseReleased(MouseEvent event) {
|
||||
TerminalPane pane = paneAt(event.getX(), event.getY());
|
||||
if (pane == null) {
|
||||
pane = workspace.activePane();
|
||||
}
|
||||
|
||||
MouseButton button = pressedButton == MouseButton.UNKNOWN ? mouseButton(event) : pressedButton;
|
||||
sendMouse(pane, MouseInput.release(button, eventX(pane, event.getX()), eventY(pane, event.getY()), modifiers(event)), false, event);
|
||||
mouseButtonPressed = false;
|
||||
pressedButton = MouseButton.UNKNOWN;
|
||||
}
|
||||
|
||||
private void handleMouseDragged(MouseEvent event) {
|
||||
TerminalPane pane = paneAt(event.getX(), event.getY());
|
||||
if (pane == null) {
|
||||
pane = workspace.activePane();
|
||||
}
|
||||
|
||||
MouseButton button = pressedButton == MouseButton.UNKNOWN ? mouseButton(event) : pressedButton;
|
||||
sendMouse(pane, MouseInput.drag(button, eventX(pane, event.getX()), eventY(pane, event.getY()), modifiers(event)), true, event);
|
||||
}
|
||||
|
||||
private void handleMouseMoved(MouseEvent event) {
|
||||
TerminalPane pane = paneAt(event.getX(), event.getY());
|
||||
if (pane == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendMouse(pane, MouseInput.motion(eventX(pane, event.getX()), eventY(pane, event.getY()), modifiers(event)), mouseButtonPressed, event);
|
||||
}
|
||||
|
||||
private void handleScroll(ScrollEvent event) {
|
||||
TerminalPane pane = paneAt(event.getX(), event.getY());
|
||||
if (pane == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.requestFocus();
|
||||
workspace.focus(pane);
|
||||
int direction = scrollDirection(event);
|
||||
if (direction == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
MouseButton wheelButton = direction > 0 ? MouseButton.FOUR : MouseButton.FIVE;
|
||||
int rows = scrollRows(event);
|
||||
boolean sent = false;
|
||||
for (int i = 0; i < rows; i++) {
|
||||
sent |= sendMouse(
|
||||
pane,
|
||||
MouseInput.press(wheelButton, eventX(pane, event.getX()), eventY(pane, event.getY()), modifiers(event)),
|
||||
mouseButtonPressed,
|
||||
event
|
||||
);
|
||||
}
|
||||
if (!sent) {
|
||||
pane.scrollViewport(direction > 0 ? -rows : rows);
|
||||
event.consume();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean sendMouse(TerminalPane pane, MouseInput input, boolean anyButtonPressed, InputEvent event) {
|
||||
MouseTarget target = mouseTarget(pane);
|
||||
if (target == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean sent = pane.sendMouse(input, target.size(), anyButtonPressed);
|
||||
if (sent) {
|
||||
event.consume();
|
||||
}
|
||||
return sent;
|
||||
}
|
||||
|
||||
private TerminalPane paneAt(double x, double y) {
|
||||
java.util.List<TerminalPane> panes = workspace.panes();
|
||||
for (int i = panes.size() - 1; i >= 0; i--) {
|
||||
TerminalPane pane = panes.get(i);
|
||||
if (x >= pane.x() && x < pane.x() + pane.width() && y >= pane.y() && y < pane.y() + pane.height()) {
|
||||
return pane;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private MouseTarget mouseTarget(TerminalPane pane) {
|
||||
if (pane.width() <= 24.0 || pane.height() <= 24.0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
FontMetrics metrics = measureFontMetrics(Font.font(fontFamily, fontSize));
|
||||
int columns = Math.max(1, (int) ((pane.width() - 24.0) / metrics.cellWidth));
|
||||
int rows = Math.max(1, (int) ((pane.height() - 24.0) / metrics.lineHeight));
|
||||
long cellWidth = Math.max(1L, Math.round(metrics.cellWidth));
|
||||
long cellHeight = Math.max(1L, Math.round(metrics.lineHeight));
|
||||
long screenWidth = Math.max(1L, Math.round(columns * metrics.cellWidth));
|
||||
long screenHeight = Math.max(1L, Math.round(rows * metrics.lineHeight));
|
||||
return new MouseTarget(MouseEncoderSize.of(screenWidth, screenHeight, cellWidth, cellHeight), screenWidth, screenHeight);
|
||||
}
|
||||
|
||||
private double eventX(TerminalPane pane, double canvasX) {
|
||||
MouseTarget target = mouseTarget(pane);
|
||||
if (target == null) {
|
||||
return 0.0;
|
||||
}
|
||||
return clamp(canvasX - pane.x() - 12.0, 0.0, target.screenWidth() - 1.0);
|
||||
}
|
||||
|
||||
private double eventY(TerminalPane pane, double canvasY) {
|
||||
MouseTarget target = mouseTarget(pane);
|
||||
if (target == null) {
|
||||
return 0.0;
|
||||
}
|
||||
return clamp(canvasY - pane.y() - 12.0, 0.0, target.screenHeight() - 1.0);
|
||||
}
|
||||
|
||||
private static double clamp(double value, double min, double max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
private static KeyModifiers modifiers(MouseEvent event) {
|
||||
return KeyModifiers.of(event.isShiftDown(), event.isControlDown(), event.isAltDown(), event.isMetaDown());
|
||||
}
|
||||
|
||||
private static KeyModifiers modifiers(ScrollEvent event) {
|
||||
return KeyModifiers.of(event.isShiftDown(), event.isControlDown(), event.isAltDown(), event.isMetaDown());
|
||||
}
|
||||
|
||||
private static int scrollRows(ScrollEvent event) {
|
||||
double rows;
|
||||
if (event.getTextDeltaYUnits() == VerticalTextScrollUnits.LINES && event.getTextDeltaY() != 0.0) {
|
||||
rows = Math.abs(event.getTextDeltaY());
|
||||
} else if (event.getTextDeltaYUnits() == VerticalTextScrollUnits.PAGES && event.getTextDeltaY() != 0.0) {
|
||||
rows = Math.abs(event.getTextDeltaY()) * 24.0;
|
||||
} else if (event.getMultiplierY() > 0.0) {
|
||||
rows = Math.abs(event.getDeltaY()) / event.getMultiplierY();
|
||||
} else {
|
||||
rows = Math.abs(event.getDeltaY()) / 40.0;
|
||||
}
|
||||
return Math.max(1, Math.min(64, (int) Math.ceil(rows)));
|
||||
}
|
||||
|
||||
private static int scrollDirection(ScrollEvent event) {
|
||||
if (event.getDeltaY() != 0.0) {
|
||||
return event.getDeltaY() > 0.0 ? 1 : -1;
|
||||
}
|
||||
if (event.getTextDeltaYUnits() != VerticalTextScrollUnits.NONE && event.getTextDeltaY() != 0.0) {
|
||||
return event.getTextDeltaY() > 0.0 ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static MouseButton mouseButton(MouseEvent event) {
|
||||
return switch (event.getButton()) {
|
||||
case PRIMARY -> MouseButton.LEFT;
|
||||
case SECONDARY -> MouseButton.RIGHT;
|
||||
case MIDDLE -> MouseButton.MIDDLE;
|
||||
default -> MouseButton.UNKNOWN;
|
||||
};
|
||||
}
|
||||
|
||||
private static void drawRow(
|
||||
GraphicsContext gc,
|
||||
RenderRow row,
|
||||
@@ -139,11 +329,11 @@ public final class TerminalCanvasView {
|
||||
double cellTop = top + (row.row() * lineHeight);
|
||||
cell.background().ifPresent(background -> {
|
||||
gc.setFill(toFxColor(background));
|
||||
gc.fillRect(x, cellTop, cellWidth, lineHeight);
|
||||
fillCellRect(gc, x, cellTop, cellWidth, lineHeight);
|
||||
});
|
||||
if (cell.selected()) {
|
||||
gc.setFill(SELECTED_BACKGROUND);
|
||||
gc.fillRect(x, cellTop, cellWidth, lineHeight);
|
||||
fillCellRect(gc, x, cellTop, cellWidth, lineHeight);
|
||||
}
|
||||
if (cell.codepoints().length == 0) {
|
||||
continue;
|
||||
@@ -156,6 +346,14 @@ public final class TerminalCanvasView {
|
||||
}
|
||||
}
|
||||
|
||||
private static void fillCellRect(GraphicsContext gc, double x, double y, double width, double height) {
|
||||
double x1 = Math.floor(x);
|
||||
double y1 = Math.floor(y);
|
||||
double x2 = Math.ceil(x + width);
|
||||
double y2 = Math.ceil(y + height);
|
||||
gc.fillRect(x1, y1, Math.max(1.0, x2 - x1), Math.max(1.0, y2 - y1));
|
||||
}
|
||||
|
||||
private static Color toFxColor(RenderColor color) {
|
||||
return Color.rgb(color.red(), color.green(), color.blue());
|
||||
}
|
||||
@@ -253,4 +451,7 @@ public final class TerminalCanvasView {
|
||||
|
||||
private record FontMetrics(double cellWidth, double lineHeight, double baselineOffset) {
|
||||
}
|
||||
|
||||
private record MouseTarget(MouseEncoderSize size, long screenWidth, long screenHeight) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@ package com.gregor.jprototerm;
|
||||
|
||||
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.RenderStateSnapshot;
|
||||
import dev.jlibghostty.ScrollViewport;
|
||||
import dev.jlibghostty.Terminal;
|
||||
import dev.jlibghostty.TerminalOptions;
|
||||
import dev.jlibghostty.DeviceAttributes;
|
||||
@@ -12,6 +17,7 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public final class TerminalPane implements AutoCloseable {
|
||||
private final Terminal terminal;
|
||||
private final MouseEncoder mouseEncoder = new MouseEncoder();
|
||||
private final AtomicReference<RenderStateSnapshot> renderSnapshot = new AtomicReference<>();
|
||||
private ShellSession session;
|
||||
private boolean floating;
|
||||
@@ -31,8 +37,8 @@ public final class TerminalPane implements AutoCloseable {
|
||||
this.rows = rows;
|
||||
}
|
||||
|
||||
public static TerminalPane create(int columns, int rows) {
|
||||
Terminal terminal = Ghostty.open(TerminalOptions.of(columns, rows));
|
||||
public static TerminalPane create(int columns, int rows, long maxScrollback) {
|
||||
Terminal terminal = Ghostty.open(new TerminalOptions(columns, rows, maxScrollback));
|
||||
terminal.setDeviceAttributesProvider(DeviceAttributes::xtermCompatible);
|
||||
TerminalPane pane = new TerminalPane(terminal, columns, rows);
|
||||
pane.refresh();
|
||||
@@ -65,11 +71,45 @@ public final class TerminalPane implements AutoCloseable {
|
||||
}
|
||||
|
||||
public void send(String text) {
|
||||
scrollViewportToBottom();
|
||||
if (session != null) {
|
||||
session.send(text);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean sendMouse(MouseInput input, MouseEncoderSize size, boolean anyButtonPressed) {
|
||||
synchronized (terminal) {
|
||||
mouseEncoder.syncFromTerminal(terminal);
|
||||
mouseEncoder.setSize(size);
|
||||
mouseEncoder.setAnyButtonPressed(anyButtonPressed);
|
||||
mouseEncoder.setTrackLastCell(input.action() == MouseAction.MOTION && input.button().isEmpty());
|
||||
|
||||
byte[] encoded = mouseEncoder.encode(input);
|
||||
if (encoded.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (session != null) {
|
||||
session.send(encoded);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void scrollViewport(long rows) {
|
||||
synchronized (terminal) {
|
||||
terminal.scrollViewport(ScrollViewport.delta(rows));
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public void scrollViewportToBottom() {
|
||||
synchronized (terminal) {
|
||||
terminal.scrollViewport(ScrollViewport.bottom());
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public RenderStateSnapshot renderSnapshot() {
|
||||
return renderSnapshot.get();
|
||||
}
|
||||
@@ -150,6 +190,7 @@ public final class TerminalPane implements AutoCloseable {
|
||||
session.close();
|
||||
session = null;
|
||||
}
|
||||
mouseEncoder.close();
|
||||
terminal.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,13 @@ public final class TerminalWorkspace implements AutoCloseable {
|
||||
return activePane() == pane;
|
||||
}
|
||||
|
||||
public void focus(TerminalPane pane) {
|
||||
int index = panes.indexOf(pane);
|
||||
if (index >= 0 && pane.visible()) {
|
||||
activeIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
public void layout(double width, double height) {
|
||||
List<TerminalPane> tiled = panes.stream()
|
||||
.filter(TerminalPane::visible)
|
||||
@@ -235,7 +242,7 @@ public final class TerminalWorkspace implements AutoCloseable {
|
||||
}
|
||||
|
||||
private TerminalPane openPane(boolean floating) {
|
||||
TerminalPane pane = TerminalPane.create(config.columns(), config.rows());
|
||||
TerminalPane pane = TerminalPane.create(config.columns(), config.rows(), config.maxScrollback());
|
||||
pane.setFloating(floating);
|
||||
pane.attach(ShellSession.start(config.shell(), pane, config.columns(), config.rows()));
|
||||
return pane;
|
||||
|
||||
Reference in New Issue
Block a user