package com.kodedu.terminalfx;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kodedu.terminalfx.annotation.WebkitCall;
import com.kodedu.terminalfx.config.TerminalConfig;
import com.kodedu.terminalfx.helper.ThreadHelper;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.layout.Pane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import netscape.javascript.JSObject;
import org.apache.commons.io.FileUtils;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
public class TerminalView extends Pane {
private final WebView webView;
private final ReadOnlyIntegerWrapper columnsProperty;
private final ReadOnlyIntegerWrapper rowsProperty;
private final ObjectProperty<Reader> inputReaderProperty;
private final ObjectProperty<Reader> errorReaderProperty;
private TerminalConfig terminalConfig = new TerminalConfig();
protected final CountDownLatch countDownLatch = new CountDownLatch(1);
private static Path tempDirectory;
static {
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
try {
if (Objects.nonNull(tempDirectory) && Files.exists(tempDirectory)) {
FileUtils.deleteDirectory(tempDirectory.toFile());
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
});
}
public TerminalView() {
initializeResources();
webView = new WebView();
columnsProperty = new ReadOnlyIntegerWrapper(150);
rowsProperty = new ReadOnlyIntegerWrapper(10);
inputReaderProperty = new SimpleObjectProperty<>();
errorReaderProperty = new SimpleObjectProperty<>();
inputReaderProperty.addListener((observable, oldValue, newValue) -> {
ThreadHelper.start(() -> {
printReader(newValue);
});
});
errorReaderProperty.addListener((observable, oldValue, newValue) -> {
ThreadHelper.start(() -> {
printReader(newValue);
});
});
webView.getEngine().getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
getWindow().setMember("app", this);
});
webView.prefHeightProperty().bind(heightProperty());
webView.prefWidthProperty().bind(widthProperty());
Path htmlPath = tempDirectory.resolve("hterm.html");
webEngine().load(htmlPath.toUri().toString());
}
private void initializeResources() {
try {
if(Objects.isNull(tempDirectory) || Files.notExists(tempDirectory)){
this.tempDirectory = Files.createTempDirectory("TerminalFX_Temp");
}
} catch (IOException e) {
throw new RuntimeException(e);
}
Path htmlPath = tempDirectory.resolve("hterm.html");
if(Files.notExists(htmlPath)) {
try (InputStream html = TerminalView.class.getResourceAsStream("/hterm.html");) {
Files.copy(html, htmlPath, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
Path htermJsPath = tempDirectory.resolve("hterm_all.js");
if(Files.notExists(htermJsPath)) {
try (InputStream html = TerminalView.class.getResourceAsStream("/hterm_all.js");) {
Files.copy(html, htermJsPath, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@WebkitCall(from = "hterm")
public String getPrefs() {
try {
return new ObjectMapper().writeValueAsString(getTerminalConfig());
} catch(final Exception e) {
throw new RuntimeException(e);
}
}
public void updatePrefs(TerminalConfig terminalConfig) {
if(getTerminalConfig().equals(terminalConfig)) {
return;
}
setTerminalConfig(terminalConfig);
final String prefs = getPrefs();
ThreadHelper.runActionLater(() -> {
try {
getWindow().call("updatePrefs", prefs);
} catch(final Exception e) {
e.printStackTrace();
}
}, true);
}
@WebkitCall(from = "hterm")
public void resizeTerminal(int columns, int rows) {
columnsProperty.set(columns);
rowsProperty.set(rows);
}
@WebkitCall
public void onTerminalInit() {
ThreadHelper.runActionLater(() -> {
getChildren().add(webView);
}, true);
}
@WebkitCall
/**
* Internal use only
*/
public void onTerminalReady() {
ThreadHelper.start(() -> {
try {
focusCursor();
countDownLatch.countDown();
} catch(final Exception e) {
}
});
}
private void printReader(Reader bufferedReader) {
try {
int nRead;
final char[] data = new char[1 * 1024];
while((nRead = bufferedReader.read(data, 0, data.length)) != -1) {
final StringBuilder builder = new StringBuilder(nRead);
builder.append(data, 0, nRead);
print(builder.toString());
}
} catch(final Exception e) {
e.printStackTrace();
}
}
@WebkitCall(from = "hterm")
public void copy(String text) {
final Clipboard clipboard = Clipboard.getSystemClipboard();
final ClipboardContent clipboardContent = new ClipboardContent();
clipboardContent.putString(text);
clipboard.setContent(clipboardContent);
}
public void onTerminalFxReady(Runnable onReadyAction) {
ThreadHelper.start(() -> {
ThreadHelper.awaitLatch(countDownLatch);
if(Objects.nonNull(onReadyAction)) {
ThreadHelper.start(onReadyAction);
}
});
}
protected void print(String text) {
ThreadHelper.awaitLatch(countDownLatch);
ThreadHelper.runActionLater(() -> {
getTerminalIO().call("print", text);
});
}
public void focusCursor() {
ThreadHelper.runActionLater(() -> {
webView.requestFocus();
getTerminal().call("focus");
}, true);
}
private JSObject getTerminal() {
return (JSObject) webEngine().executeScript("t");
}
private JSObject getTerminalIO() {
return (JSObject) webEngine().executeScript("t.io");
}
public JSObject getWindow() {
return (JSObject) webEngine().executeScript("window");
}
private WebEngine webEngine() {
return webView.getEngine();
}
public TerminalConfig getTerminalConfig() {
if(Objects.isNull(terminalConfig)) {
terminalConfig = new TerminalConfig();
}
return terminalConfig;
}
public void setTerminalConfig(TerminalConfig terminalConfig) {
this.terminalConfig = terminalConfig;
}
public ReadOnlyIntegerProperty columnsProperty() {
return columnsProperty.getReadOnlyProperty();
}
public int getColumns() {
return columnsProperty.get();
}
public ReadOnlyIntegerProperty rowsProperty() {
return rowsProperty.getReadOnlyProperty();
}
public int getRows() {
return rowsProperty.get();
}
public ObjectProperty<Reader> inputReaderProperty() {
return inputReaderProperty;
}
public Reader getInputReader() {
return inputReaderProperty.get();
}
public void setInputReader(Reader reader) {
inputReaderProperty.set(reader);
}
public ObjectProperty<Reader> errorReaderProperty() {
return errorReaderProperty;
}
public Reader getErrorReader() {
return errorReaderProperty.get();
}
public void setErrorReader(Reader reader) {
errorReaderProperty.set(reader);
}
}