pretty good
This commit is contained in:
@@ -8,7 +8,11 @@ import io.github.wasabithumb.jtoml.value.primitive.TomlPrimitive;
|
||||
import io.github.wasabithumb.jtoml.value.table.TomlTable;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public record AppConfig(
|
||||
@@ -22,10 +26,23 @@ public record AppConfig(
|
||||
boolean kittyGraphics,
|
||||
Map<String, KeyBinding> keybindings
|
||||
) {
|
||||
private static final List<String> KEYBINDING_KEYS = List.of(
|
||||
"navigate_left",
|
||||
"navigate_down",
|
||||
"navigate_up",
|
||||
"navigate_right",
|
||||
"toggle_floating",
|
||||
"new_floating",
|
||||
"next_floating",
|
||||
"close_pane",
|
||||
"open_font_selector"
|
||||
);
|
||||
|
||||
public static AppConfig load() {
|
||||
AppConfig defaults = defaults();
|
||||
Path path = configPath();
|
||||
if (!Files.isRegularFile(path)) {
|
||||
writeDefaultConfig(path, defaults);
|
||||
return defaults;
|
||||
}
|
||||
|
||||
@@ -40,16 +57,7 @@ public record AppConfig(
|
||||
doubleValue(document, "window.width", defaults.windowWidth),
|
||||
doubleValue(document, "window.height", defaults.windowHeight),
|
||||
booleanValue(document, "kitty_graphics.enabled", defaults.kittyGraphics),
|
||||
Map.of(
|
||||
"navigate_left", binding(document, "keybindings.navigate_left", defaults.keybindings.get("navigate_left")),
|
||||
"navigate_down", binding(document, "keybindings.navigate_down", defaults.keybindings.get("navigate_down")),
|
||||
"navigate_up", binding(document, "keybindings.navigate_up", defaults.keybindings.get("navigate_up")),
|
||||
"navigate_right", binding(document, "keybindings.navigate_right", defaults.keybindings.get("navigate_right")),
|
||||
"toggle_floating", binding(document, "keybindings.toggle_floating", defaults.keybindings.get("toggle_floating")),
|
||||
"new_floating", binding(document, "keybindings.new_floating", defaults.keybindings.get("new_floating")),
|
||||
"next_floating", binding(document, "keybindings.next_floating", defaults.keybindings.get("next_floating")),
|
||||
"close_pane", binding(document, "keybindings.close_pane", defaults.keybindings.get("close_pane"))
|
||||
)
|
||||
keybindings(document, defaults)
|
||||
);
|
||||
} catch (TomlException ex) {
|
||||
System.err.println("Could not parse " + path + ": " + ex.getMessage());
|
||||
@@ -75,11 +83,30 @@ public record AppConfig(
|
||||
"toggle_floating", KeyBinding.parse("ALT+F"),
|
||||
"new_floating", KeyBinding.parse("ALT+SHIFT+F"),
|
||||
"next_floating", KeyBinding.parse("ALT+F12"),
|
||||
"close_pane", KeyBinding.parse("ALT+X")
|
||||
"close_pane", KeyBinding.parse("ALT+X"),
|
||||
"open_font_selector", KeyBinding.parse("ALT+T")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public AppConfig withFont(String family, double size) {
|
||||
return new AppConfig(
|
||||
columns,
|
||||
rows,
|
||||
shell,
|
||||
family,
|
||||
size,
|
||||
windowWidth,
|
||||
windowHeight,
|
||||
kittyGraphics,
|
||||
keybindings
|
||||
);
|
||||
}
|
||||
|
||||
public void save() {
|
||||
save(configPath(), this);
|
||||
}
|
||||
|
||||
public static Path configPath() {
|
||||
String configHome = System.getenv("XDG_CONFIG_HOME");
|
||||
if (configHome != null && !configHome.isBlank()) {
|
||||
@@ -92,6 +119,76 @@ public record AppConfig(
|
||||
return "/bin/bash";
|
||||
}
|
||||
|
||||
private static Map<String, KeyBinding> keybindings(TomlTable table, AppConfig defaults) {
|
||||
Map<String, KeyBinding> parsed = new LinkedHashMap<>();
|
||||
for (String key : KEYBINDING_KEYS) {
|
||||
parsed.put(key, binding(table, "keybindings." + key, defaults.keybindings.get(key)));
|
||||
}
|
||||
return Map.copyOf(parsed);
|
||||
}
|
||||
|
||||
private static void writeDefaultConfig(Path path, AppConfig defaults) {
|
||||
save(path, defaults);
|
||||
}
|
||||
|
||||
private static void save(Path path, AppConfig config) {
|
||||
try {
|
||||
Path parent = path.getParent();
|
||||
if (parent != null) {
|
||||
Files.createDirectories(parent);
|
||||
}
|
||||
Files.writeString(
|
||||
path,
|
||||
config.toToml(),
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.TRUNCATE_EXISTING,
|
||||
StandardOpenOption.WRITE
|
||||
);
|
||||
} catch (IOException ex) {
|
||||
System.err.println("Could not write " + path + ": " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String toToml() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("[terminal]\n");
|
||||
builder.append("columns = ").append(columns).append('\n');
|
||||
builder.append("rows = ").append(rows).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");
|
||||
builder.append("[window]\n");
|
||||
builder.append("width = ").append(trimDouble(windowWidth)).append('\n');
|
||||
builder.append("height = ").append(trimDouble(windowHeight)).append("\n\n");
|
||||
builder.append("[kitty_graphics]\n");
|
||||
builder.append("enabled = ").append(kittyGraphics).append("\n\n");
|
||||
builder.append("[keybindings]\n");
|
||||
for (String key : KEYBINDING_KEYS) {
|
||||
KeyBinding binding = keybindings.get(key);
|
||||
if (binding != null) {
|
||||
builder.append(key).append(" = ").append(quoted(binding.toString())).append('\n');
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static String quoted(String value) {
|
||||
return "\"" + value
|
||||
.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\t", "\\t")
|
||||
+ "\"";
|
||||
}
|
||||
|
||||
private static String trimDouble(double value) {
|
||||
if (value == Math.rint(value)) {
|
||||
return Long.toString((long) value);
|
||||
}
|
||||
return Double.toString(value);
|
||||
}
|
||||
|
||||
private static KeyBinding binding(TomlTable table, String key, KeyBinding fallback) {
|
||||
String value = stringValue(table, key, null);
|
||||
if (value == null) {
|
||||
|
||||
@@ -35,6 +35,22 @@ public record KeyBinding(boolean alt, boolean control, boolean shift, KeyCode co
|
||||
&& event.getCode() == code;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (control) {
|
||||
builder.append("CTRL+");
|
||||
}
|
||||
if (alt) {
|
||||
builder.append("ALT+");
|
||||
}
|
||||
if (shift) {
|
||||
builder.append("SHIFT+");
|
||||
}
|
||||
builder.append(code.getName().toUpperCase(Locale.ROOT).replace(' ', '_'));
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static KeyCode keyCode(String token) {
|
||||
KeyCode alias = switch (token) {
|
||||
case "GRAVE", "BACKTICK", "BACK_QUOTE", "`" -> KeyCode.BACK_QUOTE;
|
||||
|
||||
@@ -3,19 +3,29 @@ package com.gregor.jprototerm;
|
||||
import javafx.animation.AnimationTimer;
|
||||
import javafx.application.Application;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Dialog;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Spinner;
|
||||
import javafx.scene.control.SpinnerValueFactory;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
public final class Main extends Application {
|
||||
private TerminalWorkspace workspace;
|
||||
private TerminalCanvasView terminalView;
|
||||
private AppConfig config;
|
||||
|
||||
@Override
|
||||
public void start(Stage stage) {
|
||||
AppConfig config = AppConfig.load();
|
||||
config = AppConfig.load();
|
||||
|
||||
workspace = new TerminalWorkspace(config);
|
||||
TerminalCanvasView terminalView = new TerminalCanvasView(workspace, config);
|
||||
terminalView = new TerminalCanvasView(workspace, config);
|
||||
|
||||
StackPane root = new StackPane(terminalView.canvas());
|
||||
terminalView.canvas().widthProperty().bind(root.widthProperty());
|
||||
@@ -23,7 +33,7 @@ public final class Main extends Application {
|
||||
terminalView.canvas().setOnMousePressed(event -> terminalView.canvas().requestFocus());
|
||||
|
||||
Scene scene = new Scene(root, config.windowWidth(), config.windowHeight());
|
||||
scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> handlePressed(config, event));
|
||||
scene.addEventFilter(KeyEvent.KEY_PRESSED, this::handlePressed);
|
||||
scene.addEventFilter(KeyEvent.KEY_TYPED, event -> handleTyped(event));
|
||||
|
||||
new AnimationTimer() {
|
||||
@@ -42,7 +52,7 @@ public final class Main extends Application {
|
||||
terminalView.canvas().requestFocus();
|
||||
}
|
||||
|
||||
private void handlePressed(AppConfig config, KeyEvent event) {
|
||||
private void handlePressed(KeyEvent event) {
|
||||
if (config.keybindings().get("navigate_left").matches(event)) {
|
||||
workspace.navigate(Direction.LEFT);
|
||||
event.consume();
|
||||
@@ -67,6 +77,9 @@ public final class Main extends Application {
|
||||
} else if (config.keybindings().get("close_pane").matches(event)) {
|
||||
workspace.closeActivePane();
|
||||
event.consume();
|
||||
} else if (config.keybindings().get("open_font_selector").matches(event)) {
|
||||
openFontSelector();
|
||||
event.consume();
|
||||
} else {
|
||||
String encoded = KeyEncoder.encode(event);
|
||||
if (encoded != null) {
|
||||
@@ -88,6 +101,49 @@ public final class Main extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
private void openFontSelector() {
|
||||
Dialog<ButtonType> dialog = new Dialog<>();
|
||||
dialog.setTitle("Font");
|
||||
dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
|
||||
ComboBox<String> family = new ComboBox<>();
|
||||
family.getItems().setAll(Font.getFamilies());
|
||||
family.setEditable(true);
|
||||
family.setMaxWidth(Double.MAX_VALUE);
|
||||
family.setValue(config.fontFamily());
|
||||
|
||||
Spinner<Double> size = new Spinner<>();
|
||||
size.setEditable(true);
|
||||
size.setValueFactory(new SpinnerValueFactory.DoubleSpinnerValueFactory(6.0, 48.0, config.fontSize(), 0.5));
|
||||
|
||||
GridPane content = new GridPane();
|
||||
content.setHgap(10.0);
|
||||
content.setVgap(10.0);
|
||||
content.add(new Label("Family"), 0, 0);
|
||||
content.add(family, 1, 0);
|
||||
content.add(new Label("Size"), 0, 1);
|
||||
content.add(size, 1, 1);
|
||||
dialog.getDialogPane().setContent(content);
|
||||
|
||||
dialog.showAndWait()
|
||||
.filter(button -> button == ButtonType.OK)
|
||||
.ifPresent(ignored -> {
|
||||
String selectedFamily = family.getEditor().getText();
|
||||
if (selectedFamily == null || selectedFamily.isBlank()) {
|
||||
selectedFamily = family.getValue();
|
||||
}
|
||||
if (selectedFamily == null || selectedFamily.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
double selectedSize = size.getValue();
|
||||
config = config.withFont(selectedFamily.trim(), selectedSize);
|
||||
config.save();
|
||||
terminalView.setFont(config.fontFamily(), config.fontSize());
|
||||
terminalView.canvas().requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.setProperty("prism.order", System.getProperty("prism.order", "es2,sw"));
|
||||
launch(Main.class, args);
|
||||
|
||||
@@ -19,7 +19,7 @@ public final class ShellSession implements AutoCloseable {
|
||||
private final ExecutorService reader;
|
||||
private volatile boolean closed;
|
||||
|
||||
private ShellSession(PtyProcess process, TerminalPane pane) {
|
||||
private ShellSession(PtyProcess process) {
|
||||
this.process = process;
|
||||
this.stdin = process.getOutputStream();
|
||||
this.reader = Executors.newSingleThreadExecutor(runnable -> {
|
||||
@@ -27,7 +27,6 @@ public final class ShellSession implements AutoCloseable {
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
reader.submit(() -> readOutput(pane));
|
||||
}
|
||||
|
||||
public static ShellSession start(String shell, TerminalPane pane, int columns, int rows) {
|
||||
@@ -42,13 +41,17 @@ public final class ShellSession implements AutoCloseable {
|
||||
.setInitialRows(rows)
|
||||
.setDirectory(System.getProperty("user.home"))
|
||||
.start();
|
||||
return new ShellSession(process, pane);
|
||||
return new ShellSession(process);
|
||||
} catch (IOException ex) {
|
||||
pane.write("failed to start shell: " + ex.getMessage() + "\r\n");
|
||||
throw new IllegalStateException("Could not start shell " + shell, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void startReading(TerminalPane pane) {
|
||||
reader.submit(() -> readOutput(pane));
|
||||
}
|
||||
|
||||
public void resize(int columns, int rows) {
|
||||
if (closed) {
|
||||
return;
|
||||
@@ -57,11 +60,15 @@ public final class ShellSession implements AutoCloseable {
|
||||
}
|
||||
|
||||
public void send(String text) {
|
||||
send(text.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public void send(byte[] bytes) {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
stdin.write(text.getBytes(StandardCharsets.UTF_8));
|
||||
stdin.write(bytes);
|
||||
stdin.flush();
|
||||
} catch (IOException ex) {
|
||||
close();
|
||||
|
||||
@@ -32,10 +32,14 @@ public final class TerminalCanvasView {
|
||||
private final TerminalWorkspace workspace;
|
||||
private final AppConfig config;
|
||||
private final Map<Long, Image> kittyImageCache = new HashMap<>();
|
||||
private String fontFamily;
|
||||
private double fontSize;
|
||||
|
||||
public TerminalCanvasView(TerminalWorkspace workspace, AppConfig config) {
|
||||
this.workspace = workspace;
|
||||
this.config = config;
|
||||
this.fontFamily = config.fontFamily();
|
||||
this.fontSize = config.fontSize();
|
||||
canvas.setFocusTraversable(true);
|
||||
}
|
||||
|
||||
@@ -43,6 +47,11 @@ public final class TerminalCanvasView {
|
||||
return canvas;
|
||||
}
|
||||
|
||||
public void setFont(String family, double size) {
|
||||
this.fontFamily = family;
|
||||
this.fontSize = size;
|
||||
}
|
||||
|
||||
public void render() {
|
||||
double width = canvas.getWidth();
|
||||
double height = canvas.getHeight();
|
||||
@@ -75,7 +84,7 @@ public final class TerminalCanvasView {
|
||||
gc.setLineWidth(workspace.isActive(pane) ? 2.0 : 1.0);
|
||||
gc.strokeRect(pane.x() + 0.5, pane.y() + 0.5, pane.width() - 1.0, pane.height() - 1.0);
|
||||
|
||||
Font font = Font.font(config.fontFamily(), config.fontSize());
|
||||
Font font = Font.font(fontFamily, fontSize);
|
||||
gc.setFont(font);
|
||||
|
||||
FontMetrics metrics = measureFontMetrics(font);
|
||||
@@ -105,15 +114,14 @@ public final class TerminalCanvasView {
|
||||
}
|
||||
|
||||
private static FontMetrics measureFontMetrics(Font font) {
|
||||
Text text = new Text("Mg");
|
||||
Text text = new Text("┃MgÅjy");
|
||||
text.setFont(font);
|
||||
double textHeight = text.getLayoutBounds().getHeight();
|
||||
double lineHeight = Math.max(1.0, Math.ceil(textHeight * 1.2));
|
||||
double baselineOffset = -text.getLayoutBounds().getMinY() + ((lineHeight - textHeight) / 2.0);
|
||||
double lineHeight = Math.max(1.0, text.getLayoutBounds().getHeight());
|
||||
double baselineOffset = -text.getLayoutBounds().getMinY();
|
||||
|
||||
Text cell = new Text("M");
|
||||
cell.setFont(font);
|
||||
double cellWidth = Math.max(1.0, Math.ceil(cell.getLayoutBounds().getWidth()));
|
||||
double cellWidth = Math.max(1.0, cell.getLayoutBounds().getWidth());
|
||||
return new FontMetrics(cellWidth, lineHeight, baselineOffset);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import dev.jlibghostty.KittyGraphics;
|
||||
import dev.jlibghostty.RenderStateSnapshot;
|
||||
import dev.jlibghostty.Terminal;
|
||||
import dev.jlibghostty.TerminalOptions;
|
||||
import dev.jlibghostty.DeviceAttributes;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@@ -32,6 +33,7 @@ public final class TerminalPane implements AutoCloseable {
|
||||
|
||||
public static TerminalPane create(int columns, int rows) {
|
||||
Terminal terminal = Ghostty.open(TerminalOptions.of(columns, rows));
|
||||
terminal.setDeviceAttributesProvider(DeviceAttributes::xtermCompatible);
|
||||
TerminalPane pane = new TerminalPane(terminal, columns, rows);
|
||||
pane.refresh();
|
||||
return pane;
|
||||
@@ -53,6 +55,13 @@ public final class TerminalPane implements AutoCloseable {
|
||||
|
||||
public void attach(ShellSession session) {
|
||||
this.session = session;
|
||||
terminal.setPtyWriter(bytes -> {
|
||||
ShellSession current = this.session;
|
||||
if (current != null) {
|
||||
current.send(bytes);
|
||||
}
|
||||
});
|
||||
session.startReading(this);
|
||||
}
|
||||
|
||||
public void send(String text) {
|
||||
|
||||
Reference in New Issue
Block a user