package gui.windows.device;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.scenes.scene2d.ui.ImageButton;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.Label.LabelStyle;
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane;
import com.badlogic.gdx.scenes.scene2d.ui.TextField;
import com.badlogic.gdx.scenes.scene2d.ui.TextField.TextFieldStyle;
import com.badlogic.gdx.scenes.scene2d.utils.Align;
import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable;
import game.Command;
import game.Hakd;
import networks.devices.Device;
import other.File;
import other.Util;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
public class Terminal extends SceneWindow {
private final TextField input;
private final Label display;
private final ScrollPane scroll;
private final List<String> history; // command history
private int line = 0; // holds the position of the history
private final Device device;
private Queue<Command> commandQueue;
private File directory;
private final Queue<Character> inputQueue;
private final InputStream inputStream;
private Thread commandThread;
public Terminal(DeviceScene scene) {
super(scene);
device = this.scene.getDevice();
directory = device.getHome();
commandQueue = new ConcurrentLinkedQueue<Command>(); // not sure if this should be concurrent, it might need to be in the future
history = new ArrayList<String>();
ImageButton close = new ImageButton(new TextureRegionDrawable(Hakd.assets.get("lTextures.txt", TextureAtlas.class).findRegion("close")));
close.setPosition(window.getWidth() - close.getWidth(), window.getHeight() - close.getHeight() - 20);
input = new TextField("", skin.get("console", TextFieldStyle.class));
display = new Label("", skin.get("console", LabelStyle.class));
scroll = new ScrollPane(display, skin);
display.setWrap(false); // this being true would mess up text line insertion
display.setAlignment(10, Align.left);
display.setText("Terminal [Version 0." + ((int) (Math.random() * 100)) / 10 + "]" + "\nroot @ " + device.getIp() + "\nMemory: " + device.getMemoryCapacity() + "MB\nStorage: " + device.getStorageCapacity() + "GB");
input.setFocusTraversal(false);
window.add(scroll).expand().fill();
window.row();
window.add(input).left().fillX();
window.addActor(close);
inputQueue = new ConcurrentLinkedQueue<Character>();
inputStream = new InputStream() {
@Override
public int read() throws IOException {
while (inputQueue.isEmpty()) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Gdx.app.error("Thread Sleep", "Insomnia", e);
}
}
if (!inputQueue.isEmpty()) {
return inputQueue.poll();
} else {
return '\n';
}
}
};
close.addListener(new InputListener() {
@Override
public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) {
return true;
}
@Override
public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
super.touchUp(event, x, y, pointer, button);
close();
}
});
input.addListener(new InputListener() {
@Override
public boolean keyDown(InputEvent event, int keycode) {
if (keycode == Keys.ENTER && commandQueue.isEmpty()) {
addTextln("\nroot @ " + device.getIp() + " : " + directory.getPath() + "\n$ " + input.getText());
history.add(input.getText());
command();
line = history.size();
input.setText("");
} else if (keycode == Keys.C && (Gdx.input.isKeyPressed(Keys.CONTROL_LEFT) || Gdx.input.isKeyPressed(Keys.CONTROL_RIGHT)) && !commandQueue.isEmpty()) {
addTextln("Program Stopped");
stop();
} else if (keycode == Keys.TAB && commandQueue.isEmpty()) {
tab(); // this was getting way too long
} else if (keycode == Keys.DOWN && line < history.size() - 1) {
line++;
input.setText(history.get(line));
input.setCursorPosition(input.getText().length());
} else if (keycode == Keys.UP && line > 0) {
line--;
input.setText(history.get(line));
input.setCursorPosition(input.getText().length());
} else if (!commandQueue.isEmpty()) {
if (keycode == Keys.ENTER) {
input.setText("");
}
inputQueue.offer(event.getCharacter());
}
return true;
}
@Override
public boolean keyUp(InputEvent event, int keycode) {
if (keycode == Keys.ENTER || (keycode == Keys.C && (Gdx.input.isKeyPressed(Keys.CONTROL_LEFT) || Gdx.input.isKeyPressed(Keys.CONTROL_RIGHT)) && !commandQueue.isEmpty())) {
scroll.setScrollY(scroll.getMaxY());
}
return true;
}
});
display.addListener(new InputListener() {
@Override
public boolean keyDown(InputEvent event, int keycode) {
super.keyDown(event, keycode);
if (!commandQueue.isEmpty()) {
if (keycode == Keys.ENTER) {
input.setText("");
}
inputQueue.offer(event.getCharacter());
}
return true;
}
@Override
public boolean keyUp(InputEvent event, int keycode) {
super.keyUp(event, keycode);
return true;
}
});
}
private void command() {
String s = input.getText() + ";";
String inputString = input.getText();
Scanner scanner = new Scanner(s);
String next;
while (true) {
next = scanner.findInLine(".*?[|&;]{1,2}");
if (next == null) {
break;
}
Command.Operation operation = Command.Operation.EITHER;
if (next.endsWith("|")) {
operation = Command.Operation.PIPE;
} else if (next.endsWith("&&")) {
operation = Command.Operation.SUCCESSFUL;
} else if (next.endsWith("||")) {
operation = Command.Operation.NOT_SUCCESSFUL;
} else if (next.endsWith(";")) {
operation = Command.Operation.EITHER;
}
next = next.replaceAll("[|&;]{1,2}", ""); // is this assignment needed? does replace all need to be assigned to the string it is called on?
inputString = inputString.substring(next.length());
Command c = new Command(next, Terminal.this, operation);
commandQueue.offer(c);
}
commandThread = new Thread(new Runnable() {
@Override
public void run() {
boolean successful = false;
List<String> previousOutput = null;
while (!commandQueue.isEmpty()) {
Command c = commandQueue.poll();
Command.Operation operation = c.getOperation();
c.setPipedInput(previousOutput);
if (operation == Command.Operation.PIPE) { // assumes successful
successful = c.run();
} else if (operation == Command.Operation.EITHER) {
successful = c.run();
} else if (operation == Command.Operation.SUCCESSFUL && successful) {
successful = c.run();
} else if (operation == Command.Operation.NOT_SUCCESSFUL && !successful) {
successful = c.run();
}
previousOutput = c.getPipedOutput();
}
}
});
commandThread.start();
}
public void addTextln(final String text) {
Gdx.app.postRunnable(new Runnable() {
@Override
public void run() {
display.setText(display.getText() + text + "\n ");
scroll.setScrollY(scroll.getMaxY());
}
});
}
public void addText(final char c) {
Gdx.app.postRunnable(new Runnable() {
@Override
public void run() {
if (c == '\n') {
display.setText(display.getText() + "\n ");
} else {
display.setText(display.getText() + "" + c);
}
scroll.setScrollY(scroll.getMaxY());
}
});
}
public 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());
final String text = s;
Gdx.app.postRunnable(new Runnable() {
@Override
public void run() {
display.setText(display.getText() + "\n " + text);
scroll.setScrollY(scroll.getMaxY());
}
});
}
/**
* Tab completion.
*/
private void tab() {
File tempDirectory = directory;
List<String> parameters = new ArrayList<String>();
int currentParameter = 0;
int cursorPosition = input.getCursorPosition();
// parse input into parameters of text and space
if (!input.getText().isEmpty()) {
boolean isSpace = input.getText().charAt(0) == ' ';
String tempString = "";
for (int i = 0; i < input.getText().length(); i++) {
if (isSpace != (input.getText().charAt(i) == ' ')) {
parameters.add(tempString);
tempString = "";
isSpace = !isSpace;
}
tempString += input.getText().charAt(i);
}
parameters.add(tempString);
for (String p : parameters) {
if (cursorPosition <= p.length() && p.matches("\\S+")) {
currentParameter = parameters.indexOf(p);
break;
}
cursorPosition -= p.length();
}
if (currentParameter == 0 || cursorPosition <= 0) {
cursorPosition = input.getCursorPosition();
for (String p : parameters) {
if (cursorPosition <= p.length()) {
currentParameter = parameters.indexOf(p);
break;
}
cursorPosition -= p.length();
}
}
} else {
parameters.add("");
}
String currentParameterText = parameters.get(currentParameter);
// arrays to hold possible options of text completion
Set<File> files = new HashSet<File>();
Set<File> filesCopy = new HashSet<File>();
String completedText = ""; // make a string containing the text of the current parameter, before the cursor, to manipulate into the finished parameter
if (!currentParameterText.matches("^\\s+$")) {
completedText = currentParameterText;
completedText = completedText.substring(0, cursorPosition);
}
// fill the arrays with executable files in /bin as well as all the files in the current directory
files.addAll(device.getBin().getRecursiveFileList(device.getBin()));
if (currentParameter > 0) {
files.addAll(directory.getFileMap().values());
}
// exclude files that don't start with the current parameter before the cursor
filesCopy.addAll(files);
for (other.File f : filesCopy) {
if (!f.getName().startsWith(completedText)) {
files.remove(f);
}
}
if (files.isEmpty()) {
return;
}
int totalLength;
File tempFile = (File) files.toArray()[0]; // I am running out of names
if (files.size() > 1) { // show the list of possible options and set the manipulated parameter to the common beginning text of that list
addTextln("");
for (File f : files) {
String availableFiles = f.getName();
if (f.isDirectory()) {
availableFiles += "/";
}
addTextln(availableFiles);
}
if (currentParameterText.matches("^\\s+$")) {
return;
}
int lastSameCharacter = 0; // cut the loops down by setting this to cursorposition, but I don't want to risk creating bugs
l1:
for (int i = 0; i < tempFile.getName().length(); i++) {
char character = tempFile.getName().charAt(i);
for (File f : files) {
if (f.getName().charAt(i) != character) {
lastSameCharacter = i;
break l1;
}
}
}
completedText = tempFile.getName().substring(0, lastSameCharacter) + currentParameterText.substring(cursorPosition);
parameters.set(currentParameter, completedText);
totalLength = lastSameCharacter;
} else if (files.size() == 1) { // set the manipulated text to this parameter, adding any extra text after the cursor to the end
if (cursorPosition == tempFile.getName().length()) {
return;
}
completedText = tempFile.getName();
totalLength = completedText.length() + 1;
completedText += " " + currentParameterText.substring(cursorPosition);
parameters.set(currentParameter, completedText);
} else { // there are no matches
return;
}
// fill a string with each parameter to make the new input text
String s = "";
for (String parameter : parameters) {
s += parameter;
}
// find the total length up to the end of the new parameter, and set the cursor to it
for (int i = 0; i < currentParameter; i++) {
totalLength += parameters.get(i).length();
}
input.setText(s);
input.setCursorPosition(totalLength);
directory = tempDirectory;
}
/**
* Stops the command thread. This uses the deprecated Thread.stop(), which means it
* will stop it no matter what. Normally this is very bad to do because it
* will be in the middle of something. This is realistic so I will leave it,
* there is also no better way.
*/
@SuppressWarnings("deprecation")
public void stop() {
// commandThread.interrupt();
// if (!commandThread.isInterrupted() || commandThread.isAlive()) {
commandThread.stop();
// }
commandQueue.clear();
}
public Label getDisplay() {
return display;
}
public Queue<Command> getCommandQueue() {
return commandQueue;
}
public void setCommandQueue(Queue<Command> commandQueue) {
this.commandQueue = commandQueue;
}
public Thread getCommandThread() {
return commandThread;
}
public void setCommandThread(Thread commandThread) {
this.commandThread = commandThread;
}
public Queue<Character> getInputQueue() {
return inputQueue;
}
public List<String> getHistory() {
return history;
}
public Device getDevice() {
return device;
}
public File getDirectory() {
return directory;
}
public InputStream getInputStream() {
return inputStream;
}
}