pretty good
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,3 +10,4 @@ devenv.local.yaml
|
||||
|
||||
# pre-commit
|
||||
.pre-commit-config.yaml
|
||||
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.
@@ -34,6 +34,8 @@ If `XDG_CONFIG_HOME` is unset, the fallback is:
|
||||
$HOME/.config/jprototerm/config.toml
|
||||
```
|
||||
|
||||
If no config file exists, jprototerm writes the default config on startup.
|
||||
|
||||
Example, also available in `config.example.toml`:
|
||||
|
||||
```toml
|
||||
@@ -60,6 +62,7 @@ toggle_floating = "ALT+F"
|
||||
new_floating = "ALT+SHIFT+F"
|
||||
next_floating = "ALT+F12"
|
||||
close_pane = "ALT+X"
|
||||
open_font_selector = "ALT+T"
|
||||
```
|
||||
|
||||
## Defaults
|
||||
@@ -69,5 +72,6 @@ close_pane = "ALT+X"
|
||||
- `Alt+Shift+f`: create a new floating pane
|
||||
- `Alt+F12`: cycle floating panes
|
||||
- `Alt+x`: close the active floating pane
|
||||
- `Alt+t`: open the font selector
|
||||
- Font default: `JetBrainsMono Nerd Font`
|
||||
- Kitty graphics protocol parsing is enabled by default
|
||||
|
||||
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.
@@ -21,3 +21,4 @@ toggle_floating = "ALT+F"
|
||||
new_floating = "ALT+SHIFT+F"
|
||||
next_floating = "ALT+F12"
|
||||
close_pane = "ALT+X"
|
||||
open_font_selector = "ALT+T"
|
||||
|
||||
22
devenv.nix
22
devenv.nix
@@ -7,6 +7,7 @@ let
|
||||
"git+https://gitea.gregorlohaus.com/gregor/jlibghostty.git";
|
||||
|
||||
jlib = jlibghostty.packages.${system}.jlibghostty;
|
||||
hostNvidiaLibs = ".devenv/host-nvidia-libs";
|
||||
in
|
||||
{
|
||||
packages = [
|
||||
@@ -26,9 +27,10 @@ in
|
||||
pkgs.libGL
|
||||
pkgs.gtk3
|
||||
pkgs.alsa-lib
|
||||
pkgs.mesa-demos
|
||||
];
|
||||
|
||||
env.LD_LIBRARY_PATH = lib.makeLibraryPath [
|
||||
env.LD_LIBRARY_PATH = "${hostNvidiaLibs}:" + lib.makeLibraryPath [
|
||||
pkgs.openjfx
|
||||
|
||||
pkgs.glib
|
||||
@@ -42,5 +44,23 @@ in
|
||||
pkgs.gtk3
|
||||
pkgs.alsa-lib
|
||||
] + ":/usr/lib/x86_64-linux-gnu/nvidia/current";
|
||||
env.__GLX_VENDOR_LIBRARY_NAME = "nvidia";
|
||||
env.__EGL_VENDOR_LIBRARY_FILENAMES = "/usr/share/glvnd/egl_vendor.d/10_nvidia.json";
|
||||
env.JLIBGHOSTTY_MAVEN_REPO = "${jlib}/maven";
|
||||
|
||||
enterShell = ''
|
||||
mkdir -p ${hostNvidiaLibs}
|
||||
for lib in \
|
||||
/usr/lib/x86_64-linux-gnu/libnvidia*.so* \
|
||||
/usr/lib/x86_64-linux-gnu/libGLX_nvidia.so* \
|
||||
/usr/lib/x86_64-linux-gnu/libEGL_nvidia.so* \
|
||||
/usr/lib/x86_64-linux-gnu/nvidia/current/libnvidia*.so* \
|
||||
/usr/lib/x86_64-linux-gnu/nvidia/current/libGLX_nvidia.so* \
|
||||
/usr/lib/x86_64-linux-gnu/nvidia/current/libEGL_nvidia.so*
|
||||
do
|
||||
if [ -e "$lib" ]; then
|
||||
ln -sfn "$lib" ${hostNvidiaLibs}/"$(basename "$lib")"
|
||||
fi
|
||||
done
|
||||
'';
|
||||
}
|
||||
|
||||
@@ -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