package game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.utils.Align;
import game.pythonapi.menu.PyMenu;
import gui.screens.MenuScreen;
import jline.ConsoleReader;
import jline.Terminal;
import joptsimple.BuiltinHelpFormatter;
import joptsimple.OptionException;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import org.python.core.Py;
import org.python.core.PyString;
import org.python.util.PythonInterpreter;
import other.Util;
import java.io.*;
import java.util.Arrays;
public class MenuTerminal extends Terminal {
private Label display;
private ScrollPane scroll;
private boolean echoEnabled;
private char echoChar;
private ConsoleReader consoleReader;
private char blinkTempChar;
private boolean cursorAtEnd;
private static char BLINK_CHAR = '_'; // this would be preferred: '█';
private boolean blinkCharShown = false;
public static final float BLINK_SPEED = .6f; // in seconds
private boolean newline = false;
private MenuScreen menuScreen;
private Thread commandThread;
private boolean commandRunning = false;
private boolean commandStarted = false;
private boolean atBottom; // used to keep the terminal scrolled down
private int tab = 0; // used to remove unwnated text added by jline
private boolean tabComplete = false;
private static final int spacesJlineAdds = 3; // the amount of spaces jline adds after a complete character is sent (it may be jline's tab)
@Override
public void initializeTerminal() throws Exception {
Skin skin = Hakd.assets.get(("skins/uiskin.json"), Skin.class);
display = new Label("", skin.get("console", Label.LabelStyle.class));
scroll = new ScrollPane(display, skin);
// display.setFontScale(.6f);
display.getStyle().font.setMarkupEnabled(true); // for now, this is broken for labels, but I will leave it in so I can easily tell when it gets fixed
display.setWrap(false);
display.setAlignment(10, Align.left);
String terminalInfo = "[#3C91BF]Terminal [[Version 0." + ((int) (Math.random() * 100)) / 10 + "]";
terminalInfo += "\n[#38FF4C]" + System.getProperty("user.name") + "[] @ [#FFC123]127.0.0.1[]";
terminalInfo += "\nStorage:";
// makes hakd start a bit slower, but lets the first jython program start a bit sooner
new PythonInterpreter().exec("import sys"); // it is a trade off that lets the game feel more responsive
// only shows the partition in which root is mounted from on linux, this will be the only time something works on windows but not linux
// I could just search for /dev/sd* if on linux(mac too?) and read the size of them some how, not worth the work
for (int i = 0; i < File.listRoots().length; i++) {
terminalInfo += "\n Drive[[" + i + "] " + (-File.listRoots()[i].getFreeSpace() +
File.listRoots()[i].getTotalSpace()) / 1000000000 + "GB Used, " +
File.listRoots()[i].getTotalSpace() / 1000000000 + "GB Total";
}
display.setText(terminalInfo + "\n-----------------------------------------------------\n" +
" Type \"help\" to get started.[]\n\n");
}
@Override
public void beforeReadLine(ConsoleReader reader, String prompt, Character mask) {
super.beforeReadLine(reader, prompt, mask);
blink(false);
if (menuScreen.getInputQueue().peek() == '\n') {
newline = true;
display.setText(display.getText() + "\n");
} else if (menuScreen.getInputQueue().peek() == -58) { // used to remove unwanted text added by jline
write(new char[]{'\r'});
tabComplete = true;
}
}
@Override
public void afterReadLine(ConsoleReader reader, String prompt, Character mask) {
super.afterReadLine(reader, prompt, mask);
if (newline) {
newline = false;
} else if (tabComplete) {
tabComplete = false;
}
atBottom = true;
}
@Override
public int getTerminalWidth() {
return 20;
}
@Override
public int getTerminalHeight() {
return 20;
}
@Override
public boolean isSupported() {
return true;
}
@Override
public boolean getEcho() {
return false;
}
@Override
public boolean isEchoEnabled() {
return echoEnabled;
}
@Override
public void enableEcho() {
echoEnabled = true;
}
@Override
public void disableEcho() {
echoEnabled = false;
}
@Override
public boolean isANSISupported() {
return false; // for now
}
@Override
public InputStream getDefaultBindings() {
return Terminal.class.getResourceAsStream("keybindings.properties");
}
@Override
public int readVirtualKey(InputStream in) throws IOException {
int code = super.readVirtualKey(in);
if (code >= 0) {
return code;
}
switch (code) {
case MOVE_TO_BEG:
return 1;
case PREV_CHAR:
return 2;
case EXIT:
return 4;
case MOVE_TO_END:
return 5;
case NEXT_CHAR:
return 6;
case ABORT:
return 7;
case DELETE_PREV_CHAR:
return 8;
case COMPLETE:
return 9;
case NEWLINE: // LF newline
return 10;
case KILL_LINE:
return 11;
case CLEAR_SCREEN:
return 12;
// case NEWLINE: // CR newline, I don't know what to do with this, so I am disabling it and using LF for now
// return 13;
case NEXT_HISTORY:
return 14;
case PREV_HISTORY:
return 16;
case SEARCH_PREV:
return 18;
case KILL_LINE_PREV:
return 21;
case PASTE:
return 22;
case DELETE_PREV_WORD:
return 23;
case PREV_WORD:
return 24;
case REDISPLAY:
return 27;
case DELETE_NEXT_CHAR:
return 127;
default:
return -1;
}
}
public synchronized void write(char[] chars) {
if (chars.length == 0 || commandStarted || commandRunning || tab > 0) {
return;
}
if (tabComplete && chars.length == 1 && chars[0] == ' ') {
return;
}
String text = "";
for (char c : chars) {
if (c == '\r') {
display.setText(display.getText().toString().substring(0, display.getText().toString().lastIndexOf('\n') + 1));
return;
} else if (c == '\n' && newline) {
// nothing goes here
} else if (c != '\u0000' && c != '\b' && c != (char) -1) {
text += c;
}
}
if (text.length() > 0) {
display.setText(display.getText() + escapeBrackets(text));
// System.out.println(text);
}
}
public void run(final String command) {
commandThread = new Thread(new Runnable() {
String in = command;
@Override
public void run() {
Gdx.app.debug("Menu Command", command);
if (!command.isEmpty()) {
commandStarted = false;
commandRunning = true;
try {
runPython(command);
} catch (FileNotFoundException e) {
Gdx.app.debug("Menu Info", "FileNotFound");
} catch (OptionException e) {
addText(e.getMessage() + ". Try using -h.");
Gdx.app.debug("Jopt", e.getMessage());
} catch (Exception e) {
Gdx.app.debug("Menu Info", "Parser not found, ");
}
commandRunning = false;
} else {
commandStarted = false;
}
Gdx.app.postRunnable(new Runnable() {
@Override
public void run() {
try {
consoleReader.redrawLine();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
});
// commandThread.setName(name); might want to change this
commandThread.start();
}
private void runPython(String command) throws Exception {
String name = command.contains(" ") ? command.substring(0, command.indexOf(" ")) : command;
File[] files = Gdx.files.internal(Util.ASSETS + "/python/menu/").file().listFiles();
File file = null;
if (files == null) {
addText(name + ": command not found");
throw new FileNotFoundException();
}
for (File f : files) {
if (f.getName().equals(name + ".py")) {
file = f;
}
}
if (file == null || !file.exists()) {
throw new FileNotFoundException();
}
Gdx.app.debug("Menu Info", "python file: " + file.getPath());
PythonInterpreter pi = new PythonInterpreter();
Py.getSystemState().path.append(new PyString(file.getParentFile().getAbsolutePath()));
String parserCode = Util.getParserCode(file);
pi.exec(parserCode);
OptionParser parser = (OptionParser) pi.get("parser").__tojava__(OptionParser.class);
String arguments = command.substring(name.length());
OptionSet options = parser.parse(arguments.split("(?<!\\\\)\\s+"));
parser.formatHelpWith(new BuiltinHelpFormatter(65, 10)); // only for 800 pixel wide screen, which fits about 70 characters
if (options.has("h") || options.has("help")) {
CommandWriter writer = new CommandWriter();
try {
parser.printHelpOn(writer);
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
} else {
pi.set("options", options);
pi.set("menu", new PyMenu(menuScreen));
pi.execfile(file.getPath());
}
}
@SuppressWarnings("deprecation")
public void stop() {
if (commandThread != null) {
commandThread.stop();
Gdx.app.debug("Menu Info", "Command Stopped");
}
commandRunning = false;
}
/**
* Escapes square brackets to allow libgdx to use color markup.
* It does only allows hex colors, defined color names are banned.
* This is to allow programs to use brackets to use brackets if needed.
* The user is not allowed to enter brackets though.
*
* @param s The String to add escape characters to.
* @return The String with escape characters added.
*/
private String escapeBrackets(String s) {
String text = s;
if (text.matches(".*\\[(?!(?:#[\\da-fA-F]{6}\\])|(?:\\])).*")) { // same regex as below, but tests if the string contains it
text = text.replaceAll("\\[(?!(?:#[\\da-fA-F]{6}\\])|(?:\\]))", "[["); // looks for singular '[' characters that are not followed by a ']' or "#\d{6}]"
}
return text;
}
public synchronized void addText(String text) {
text = text.replace("\n", "\n ");
display.setText(display.getText() + " " + escapeBrackets(text) + "\n");
}
public synchronized void replaceText(String newText, int lineFromBottom) {
String t = display.getText().toString();
int n = t.length();
int m = n;
lineFromBottom++;
if (lineFromBottom < 1) {
return;
}
for (int i = 0; i < lineFromBottom; i++) {
m = n;
n = t.lastIndexOf("\n", n - 2);
}
String s = t.substring(0, n);
s += "\n " + newText;
s += t.substring(m, t.length());
display.setText(s);
scroll.setScrollY(display.getHeight());
}
public void blink(boolean blink) {
StringBuffer buffer = consoleReader.getCursorBuffer().getBuffer();
int cursor = consoleReader.getCursorBuffer().cursor;
if (!(blinkCharShown ^ blink)) {
return;
}
if (blink) {
blinkCharShown = true;
if (buffer.length() == cursor) {
cursorAtEnd = true;
blinkTempChar = 0;
buffer.append(BLINK_CHAR);
} else {
cursorAtEnd = false;
blinkTempChar = buffer.charAt(cursor);
buffer.setCharAt(cursor, BLINK_CHAR);
}
} else {
blinkCharShown = false;
if (cursorAtEnd) {
buffer.setCharAt(cursor, '\u0000');
buffer.setLength(cursor);
} else {
buffer.setCharAt(cursor, blinkTempChar);
}
}
}
// this class is strictly for the optionparser in runPython(), there is no character escapes or any other necessary modification
private class CommandWriter extends Writer {
@Override
public void write(char[] chars, int start, int end) throws IOException {
addText(new String(Arrays.copyOfRange(chars, start, end)));
}
@Override
public void flush() throws IOException {
}
@Override
public void close() throws IOException {
}
}
public void setConsoleReader(ConsoleReader consoleReader) {
if (this.consoleReader != null) {
return;
}
this.consoleReader = consoleReader;
}
public void setMenuScreen(MenuScreen menuScreen) {
if (this.menuScreen != null) {
return;
}
this.menuScreen = menuScreen;
}
public boolean isCommandRunning() {
return commandRunning;
}
public ScrollPane getScroll() {
return scroll;
}
public Label getDisplay() {
return display;
}
public boolean isBlinkCharShown() {
return blinkCharShown;
}
public boolean isAtBottom() {
return atBottom;
}
public void setAtBottom(boolean atBottom) {
this.atBottom = atBottom;
}
public boolean isCommandStarted() {
return commandStarted;
}
public void setCommandStarted(boolean commandStarted) {
this.commandStarted = commandStarted;
}
}