diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2681eb5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,28 @@ +# Set default behaviour, in case users don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files we want to always be normalized and converted to native line endings on checkout. +# These files are text and should be normalized (Convert crlf => lf) +*.java text +*.properties text +*.sh text +*.md text +*.info text +*.txt text +*.json text + +# Denote all files that are truly binary and should not be modified. +# (binary is a macro for -text -diff) +*.class binary +*.gif binary +*.ico binary +*.jar binary +*.jpg binary +*.jpeg binary +*.png binary + +# Make sure Windows batch files preserve CR/LF line endings, otherwise they may not be able to execute. Windows +# batch files require a CR/LF for labels to work properly, otherwise they may fail when labels straddle 512-byte +# block boundaries. This is important when files are downloaded through a zip archive that was authored on a +# Linux machine (the default behavior on GitHub) +*.bat eol=crlf diff --git a/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md similarity index 100% rename from CODE_OF_CONDUCT.md rename to .github/CODE_OF_CONDUCT.md diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md diff --git a/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md similarity index 100% rename from ISSUE_TEMPLATE.md rename to .github/ISSUE_TEMPLATE.md diff --git a/.github/README.md b/.github/README.md new file mode 100644 index 0000000..dc0a0dc --- /dev/null +++ b/.github/README.md @@ -0,0 +1,14 @@ +![JTerm](https://www.sergix.net/assets/img/logo/jterm.png) + +## What is it? +A terminal written for cross-platform usage. + +## Why will it help me? +Because the JTerm project is written in Java, this provides it various advantages: +- System compatibility downsides don't exist! +- Much safer than C++/C and other system languages + +Plus, it's open source, so if you find any issues, you can help out everyone else using JTerm! + +## Where do I get it? +Check the [releases](https://github.com/Sergix/JTerm/releases) page for binaries, as well as the source code. You can also look in the `/builds` directory for changelogs and other build-related files. diff --git a/ROADMAP.md b/.github/ROADMAP.md similarity index 100% rename from ROADMAP.md rename to .github/ROADMAP.md diff --git a/changelog.txt b/.github/changelog.txt similarity index 100% rename from changelog.txt rename to .github/changelog.txt diff --git a/.gitignore b/.gitignore index 14a9031..b10b82f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,90 +1,69 @@ -# Log file -*.log -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.war -*.ear -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* - - -# ignore gradle artifacts -.gradle/ -gradlew.bat -build/libs/ -build/tmp/ - -# test scripts -test.bat - -target/ -combined-batch.bat - -# IDEA files -.idea/ -jterm.iml - -# eclipse files -.classpath -.project -.settings - -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff: -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/dictionaries - -# Sensitive or high-churn files: -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.xml -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml - -# Gradle: -.idea/**/gradle.xml -.idea/**/libraries - -# CMake -cmake-build-debug/ - -# Mongo Explorer plugin: -.idea/**/mongoSettings.xml - -## File-based project format: +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore files that are not wanted in general +.* +_MACOSX +*.db + +# Don't ignore GitHub files +!.github +!.gitignore +!.gitattributes + +# or EditorConfig +!.editorconfig + +# Ignore Eclipse files +/bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +*.launch + +# Ignore IntelliJ IDEA files +*.iml +*.ipr *.iws - -## Plugin-specific files: - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin +/out/ atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties +# Ignore NetBeans files +nbproject/private/ +/build/ +nbbuild/ +/dist/ +nbdist/ +nbactions.xml +nb-configuration.xml + +# Ignore JVM crash logs +hs_err_pid* + +# Ignore generic & archive binaries +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip +*.war +*.ear +*.sar +*.class + +# Project-specific dependencies +/target/ \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index aaf7123..0000000 --- a/README.md +++ /dev/null @@ -1,31 +0,0 @@ -[![JTerm](https://sergix.github.io/img/jterm.png)](https://sergix.github.io/projects/jterm/index.html) - -![Build Status](https://travis-ci.org/Sergix/JTerm.svg?branch=master) -[![Code Triagers Badge](https://www.codetriage.com/sergix/jterm/badges/users.svg)](https://www.codetriage.com/sergix/jterm) - -[![Stories in Ready](https://badge.waffle.io/Sergix/JTerm.svg?label=ready&title=Ready)](http://waffle.io/Sergix/JTerm) -[![Throughput Graph](https://graphs.waffle.io/Sergix/JTerm/throughput.svg)](https://waffle.io/Sergix/JTerm/metrics/throughput) - -## What is it? -A terminal written for cross-platform usage. - -## Why will it help me? -Because the JTerm project is written in Java, this provides it various advantages: -- System compatibility downsides don't exist! -- Much safer than C++/C and other system languages - -Plus, it's open source, so if you find any issues, you can help out everyone else using JTerm! - -## Where do I get it? -Check the [releases](https://github.com/Sergix/JTerm/releases) page for binaries, as well as the source code. You can also look in the `/builds` directory for changelogs and other build-related stuff. - -## How can I help? -View the [Contributing Guidelines](https://github.com/Sergix/JTerm/blob/master/CONTRIBUTING.md) for more information. The JTerm project is open to anyone and any code! - -## Slack -The JTerm project now has a Slack messaging group! Request to join the [Sergix](https://sergix.slack.com/) team to recieve notifications on updates, Travis CI build status, and more! - -> JTerm v0.7.0 -> `jterm-v0.7.0.jar` -> This project and its source are held under the GNU General Public License, located in the LICENSE file in the project's directory. -> (c) 2017 diff --git a/docs/release/jterm-v0.7.0-docs.md b/docs/release/jterm-v0.7.0-docs.md index 0629de9..6bef2c2 100644 --- a/docs/release/jterm-v0.7.0-docs.md +++ b/docs/release/jterm-v0.7.0-docs.md @@ -17,15 +17,15 @@ This document provides information on changes included in release version "0.7.0 ## Build Targets ``` -[VERSION] [FILE] [STATE] +[VERSION] [FILE] [STATE] 0.1.0 jterm-v0.1.0.jar OK 0.2.0 jterm-v0.2.0.jar OK 0.2.1 jterm-v0.2.1.jar OK 0.3.0 jterm-v0.3.0.jar DEPRECATED 0.3.1 jterm-v0.3.1.jar OK 0.4.0 jterm-v0.4.0.jar OK -0.4.1 jterm-v0.4.1.jar OK -0.5.0 jterm-v0.5.0.jar OK +0.4.1 jterm-v0.4.1.jar OK +0.5.0 jterm-v0.5.0.jar OK 0.5.1 jterm-v0.5.1.jar OK 0.6.0 jterm-v0.6.0.jar OK 0.6.1 jterm-v0.6.1.jar DEPRECATED @@ -190,4 +190,4 @@ The `download`, `rmdir`, `mv`/`move`, `rn`, and 'regex' commands have been added > JTerm 0.7.0 > `jterm-v0.7.0.jar` > This project and its source are held under the GNU General Public License, located in the LICENSE file in the project's directory. -> (c) 2017 \ No newline at end of file +> (c) 2017 diff --git a/src/main/java/jterm/JTerm.java b/src/main/java/jterm/JTerm.java index d233a46..bb5eca9 100644 --- a/src/main/java/jterm/JTerm.java +++ b/src/main/java/jterm/JTerm.java @@ -21,12 +21,12 @@ import jterm.command.CommandException; import jterm.command.CommandExecutor; import jterm.gui.Terminal; -import jterm.io.input.InputHandler; import jterm.io.input.Keys; import jterm.io.output.GuiPrinter; import jterm.io.output.HeadlessPrinter; import jterm.io.output.Printer; import jterm.io.output.TextColor; +import jterm.io.terminal.HeadlessTerminal; import jterm.util.Util; import java.io.BufferedReader; @@ -39,10 +39,9 @@ import java.util.zip.ZipInputStream; public class JTerm { - private static final Map COMMANDS = new HashMap<>(); public static Printer out; - public static final String VERSION = "0.7.1"; + public static final String VERSION = "0.7.0"; public static String PROMPT = ">> "; public static String dirChar; public static final String LICENSE = "JTerm Copyright (C) 2017 Sergix, NCSGeek, chromechris\n" @@ -52,7 +51,7 @@ public class JTerm { // Default value of getProperty("user.dir") is equal to the default directory set when the program starts // Global directory variable (use "cd" command to change) - public static String currentDirectory = System.getProperty("user.dir"); + public static String currentDirectory = System.getProperty("user.dir") + "/"; public static final String USER_HOME_DIR = System.getProperty("user.home"); public static boolean IS_WIN; @@ -65,7 +64,6 @@ public class JTerm { public static void main(String[] args) { setOS(); initCommands(); - if (args.length > 0 && args[0].equals("headless")) { out = new HeadlessPrinter(); headless = true; @@ -78,76 +76,62 @@ public static void main(String[] args) { out = new GuiPrinter(terminal.getTextPane()); Keys.initGUI(); } - JTerm.out.println(TextColor.INFO, JTerm.LICENSE); - - JTerm.out.printf(TextColor.INFO, "TEST LINE (OS): %b\n\n", JTerm.IS_UNIX); - JTerm.out.printPrompt(); - - if (headless) { - try { - while (true) { - InputHandler.read(); - } - } catch (IOException e){ - e.printStackTrace(); - } - } + if (headless) + new HeadlessTerminal().run(); } - public static void executeCommand(String options) { - List optionsArray = Util.getAsArray(options); + public static boolean executeCommand(final String options) { + final List optionsArray = Util.getAsArray(options); if (optionsArray.size() == 0) { - return; + return false; } - String command = optionsArray.remove(0); - if (!COMMANDS.containsKey(command)) { - out.printf(TextColor.INFO, "\nTEST LINE (INPUT): %s\n\n", command); - out.printf(TextColor.ERROR,"Command \"%s\" is not available\n", command); - return; - } + final String command = optionsArray.remove(0); + if (!COMMANDS.containsKey(command)) + return false; try { - if (JTerm.isHeadless()) { - out.println(); - } - + if (JTerm.isHeadless()) out.println(); COMMANDS.get(command).execute(optionsArray); + return true; } catch (CommandException e) { System.err.println(e.getMessage()); + return false; } } private static void initCommands() { - ArrayList methods = new ArrayList<>(); - ArrayList classes = new ArrayList<>(); + final ArrayList methods = new ArrayList<>(); + final ArrayList classes = new ArrayList<>(); try { - CodeSource src = JTerm.class.getProtectionDomain().getCodeSource(); + final CodeSource src = JTerm.class.getProtectionDomain().getCodeSource(); if (src != null) { - ZipInputStream zip = new ZipInputStream(src.getLocation().openStream()); + final ZipInputStream zip = new ZipInputStream(src.getLocation().openStream()); while (true) { - ZipEntry e = zip.getNextEntry(); + final ZipEntry e = zip.getNextEntry(); if (e == null) { break; } - String name = e.getName(); - if (name.startsWith("jterm/command") - && (name.compareTo("jterm/command/") != 0)) { + final String name = e.getName(); + if (name.startsWith("jterm/command")) { classes.add(name.replace('/', '.').substring(0, name.length() - 6)); } } } } catch (IOException ioe) { - out.println(TextColor.ERROR, ioe.toString()); + out.println(TextColor.ERROR,ioe.toString()); } + // TODO: This line makes the program crash on Linux Kubuntu, don't know about windows + classes.remove(0); + classes.forEach(aClass -> { try { Arrays.stream(Class.forName(aClass).getDeclaredMethods()).forEach(method -> { @@ -162,9 +146,9 @@ private static void initCommands() { methods.forEach(method -> { method.setAccessible(true); - Command command = method.getDeclaredAnnotation(Command.class); + final Command command = method.getDeclaredAnnotation(Command.class); Arrays.stream(command.name()).forEach(commandName -> { - CommandExecutor executor = new CommandExecutor() + final CommandExecutor executor = new CommandExecutor() .setCommandName(commandName) .setSyntax(command.syntax()) .setMinOptions(command.minOptions()) @@ -183,7 +167,7 @@ private static void initCommands() { } private static void setOS() { - String os = System.getProperty("os.name").toLowerCase(); + final String os = System.getProperty("os.name").toLowerCase(); if (os.contains("windows")) { JTerm.IS_WIN = true; dirChar = "\\"; @@ -208,7 +192,7 @@ public static Set getCommands() { } /** For Unit Tests **/ - public static void setheadless(boolean b){ + public static void setHeadless(boolean b) { headless = b; } @@ -219,8 +203,4 @@ public static void setTerminal(Terminal terminal) { public static void setPrompt(String prompt) { PROMPT = prompt; } - - public static void setCurrentDirectory(String currentDirectory) { - JTerm.currentDirectory = currentDirectory; - } } diff --git a/src/main/java/jterm/gui/Terminal.java b/src/main/java/jterm/gui/Terminal.java index b57dc59..5869cac 100644 --- a/src/main/java/jterm/gui/Terminal.java +++ b/src/main/java/jterm/gui/Terminal.java @@ -1,9 +1,11 @@ package jterm.gui; import jterm.JTerm; -import jterm.io.input.InputHandler; +import jterm.io.handlers.InputHandler; import jterm.io.input.Keys; import jterm.io.output.TextColor; +import jterm.io.terminal.HeadlessTerminal; +import jterm.io.terminal.TermInputProcessor; import javax.swing.*; import java.awt.*; @@ -18,6 +20,8 @@ public class Terminal extends JFrame implements KeyListener { private JPanel contentPane; private JTextPane textPane; + private HeadlessTerminal headlessTerminal = new HeadlessTerminal(); + private InputHandler inputHandler = new TermInputProcessor(headlessTerminal); public Terminal() { TextColor.initGui(); @@ -60,10 +64,10 @@ public void keyPressed(KeyEvent e) { return; } if ((int) e.getKeyChar() == 65535) { - //An arrow key was pressed. Switch the key code into the negatives so it wont interfere with any real chars - new Thread(() -> InputHandler.process(Keys.getKeyByValue(e.getKeyCode() * -1), e.getKeyChar())).start(); +// An arrow key was pressed. Switch the key code into the negatives so it wont interfere with any real chars + new Thread(() -> inputHandler.process(Keys.getKeyByValue(e.getKeyCode() * -1))).start(); } else - new Thread(() -> InputHandler.process(Keys.getKeyByValue((int) e.getKeyChar()), e.getKeyChar())).start(); + new Thread(() -> inputHandler.process(Keys.getKeyByValue(e.getKeyChar()))).start(); } private void onCancel() { diff --git a/src/main/java/jterm/io/handlers/ArrowKeyHandler.java b/src/main/java/jterm/io/handlers/ArrowKeyHandler.java new file mode 100644 index 0000000..4ee406e --- /dev/null +++ b/src/main/java/jterm/io/handlers/ArrowKeyHandler.java @@ -0,0 +1,102 @@ +package jterm.io.handlers; + +import jterm.io.handlers.events.Event; +import jterm.io.input.Keys; + +/** + * Abstract class specifying how arrow keys should be handled. + * + * @see Event + */ +public abstract class ArrowKeyHandler { + + // Last arrow key that was pressed (if any other key is pressed sets to Keys.NONE) + protected static Keys lastArrowPress = Keys.NONE; + + private long lastPress = System.currentTimeMillis(); + + // Events to be implemented by any class that inherits ArrowKeyHandler + public Event leftArrEvent; + public Event rightArrEvent; + public Event upArrEvent; + public Event downArrEvent; + + /** + * Checks if last input was arrow key (only on Windows). + * + * @param i Integer value of last key press + * @return Arrow key pressed (or Keys.NONE if no arrow key was pressed) + */ + public static Keys arrowKeyCheckWindows(final int i) { + switch (i) { + case 57416: + return Keys.UP; + case 57424: + return Keys.DOWN; + case 57421: + return Keys.RIGHT; + case 57419: + return Keys.LEFT; + default: + return Keys.NONE; + } + } + + /** + * Checks if input was arrow key (only on Unix). + *

+ * When Unix processes arrow keys, they are read as a sequence of 3 numbers, for example 27 91 65 + * which means that the implementation of InputHandler owning the implementation of ArrowKeyHandler + * must read 3 values, only blocking for the first. That way, if an arrow key is pressed, all three values are + * caught, and if not, no input is lost, since the time for catching in non-blocking mode is ~1ms, and keyboard + * presses are only detected every ~30ms interval. + * + * @param i Integer value of last key press + * @return Arrow key pressed (or Keys.NONE if no arrow key was pressed) + */ + public static Keys arrowKeyCheckUnix(final int... i) { + + if (i[0] == 27 && i[1] == 91) { + switch (i[2]) { + case 65: + return Keys.UP; + case 66: + return Keys.DOWN; + case 67: + return Keys.RIGHT; + case 68: + return Keys.LEFT; + default: + return Keys.NONE; + } + } + + return Keys.NONE; + } + + /** + * Process an arrow key press. + * + * @param ak Arrow key to process + */ + public void process(final Keys ak) { + if (ak != Keys.NONE && System.currentTimeMillis() - lastPress > InputHandler.minWaitTime) { + lastPress = System.currentTimeMillis(); + switch (ak) { + case UP: + upArrEvent.process(); + break; + case DOWN: + downArrEvent.process(); + break; + case LEFT: + leftArrEvent.process(); + break; + case RIGHT: + rightArrEvent.process(); + break; + } + } + } +} + diff --git a/src/main/java/jterm/io/handlers/InputHandler.java b/src/main/java/jterm/io/handlers/InputHandler.java new file mode 100644 index 0000000..2f10b82 --- /dev/null +++ b/src/main/java/jterm/io/handlers/InputHandler.java @@ -0,0 +1,67 @@ +package jterm.io.handlers; + + +import jterm.io.input.Input; +import jterm.io.input.Keys; + +import java.io.IOException; + +/** + * Abstract class specifying how input should be handled. + */ +public abstract class InputHandler { + + protected ArrowKeyHandler arrowKeyHandler; + protected KeyHandler keyHandler; + + /** + * Determines how long the program will ignore user input (in ms). + * Prevents program from going visually insane, by not overloading the system with too much output. + */ + public static int minWaitTime = 10; + + /** + * Create key handler object with null arrow key handler and key handler. + * Only call this constructor if you plan to manually assign the handlers + * straight after this constructor returns. + */ + public InputHandler() { + arrowKeyHandler = null; + keyHandler = null; + } + + /** + * Set KeyHandler and ArrowKeyHandler for JTerm. + *

+ * Classes extending KeyHandler and ArrowKeyHandler should passed as parameters, + * so they can be later used to process input. + * + * @param kh Key handler to use + * @param akh Arrow key handler to use + */ + public InputHandler(KeyHandler kh, ArrowKeyHandler akh) { + this.arrowKeyHandler = akh; + this.keyHandler = kh; + } + + /** + * Code to run when processing input for JTerm headless mode. + * Can (and should) make use of keyHandler and/or arrowKeyHandler for input processing. + */ + public abstract void process(final Keys key); + + /** + * Returns key char value of last key pressed. + * + * @return Char value of key pressed + */ + public static Keys getKey() { + try { + return Keys.getKeyByValue(Input.read(true)); + } catch (IOException e) { + e.printStackTrace(); + } + + return Keys.NONE; + } +} diff --git a/src/main/java/jterm/io/handlers/KeyHandler.java b/src/main/java/jterm/io/handlers/KeyHandler.java new file mode 100644 index 0000000..549c523 --- /dev/null +++ b/src/main/java/jterm/io/handlers/KeyHandler.java @@ -0,0 +1,94 @@ +package jterm.io.handlers; + +import jterm.JTerm; +import jterm.io.handlers.events.CharEvent; +import jterm.io.handlers.events.Event; +import jterm.io.input.Keys; + +import java.util.HashMap; + +/** + * Abstract class specifying methods needed to process key events properly. + */ +public abstract class KeyHandler { + + // Stores all key integer values and maps them to the values in Keys enum + private static HashMap keymap = new HashMap<>(); + + private long lastPress = System.currentTimeMillis(); + + // Events to implemented by each class that inherits KeyHandler + public Event tabEvent; + public Event newLineEvent; + public CharEvent charEvent; + public Event backspaceEvent; + public Event ctrlCEvent; + public Event ctrlZEvent; + + // Returns a map pairing key values stored as ints to values in Keys enum + public static HashMap getKeymap() { + return keymap; + } + + /** + * Processes all input by relegating it to the appropriate lambda expression + * and triggering events so that other Modules can react appropriately. + * + * @param key Key pressed + */ + public void process(final Keys key) { + if (System.currentTimeMillis() - lastPress < InputHandler.minWaitTime) + return; + lastPress = System.currentTimeMillis(); + + if (key == Keys.BACKSPACE) + backspaceEvent.process(); + else if (key == Keys.TAB) + tabEvent.process(); + else if (key == Keys.NWLN) + newLineEvent.process(); + else if (key == Keys.CHAR) + charEvent.process((char) key.getValue()); + + signalCatch(key); + } + + /** + * Catches system signals, such as Ctrl+C. + * Useful for running while waiting for some process to finish, so user can cancel if they wish. + * For use in above use case, loop while process is not done and pass input.read(false) to this method. + * + * @param key Key pressed + * @return true if process should be cancelled, false if no signals were caught + */ + public static Keys signalCatch(final Keys key) { + if (key == Keys.CTRL_C || key == Keys.CTRL_Z) + key.executeAction(); + return Keys.NONE; + } + + /** + * Loads all integer values of keys to keymap. + */ + public static void initKeysMap() { + if (JTerm.IS_WIN) { + keymap.put(8, Keys.BACKSPACE); + keymap.put(9, Keys.TAB); + keymap.put(13, Keys.NWLN); + } else if (JTerm.IS_UNIX) { + keymap.put(127, Keys.BACKSPACE); + keymap.put((int) '\t', Keys.TAB); + keymap.put((int) '\n', Keys.NWLN); + } + } + + /** + * Returns associated key value from keymap. + * + * @param i Integer value of key pressed + */ + protected static Keys getKey(int i) { + return keymap.get(i); + } +} + diff --git a/src/main/java/jterm/io/handlers/events/CharEvent.java b/src/main/java/jterm/io/handlers/events/CharEvent.java new file mode 100644 index 0000000..d2b809a --- /dev/null +++ b/src/main/java/jterm/io/handlers/events/CharEvent.java @@ -0,0 +1,8 @@ +package jterm.io.handlers.events; + +/** + * Functional interface allowing the implementation of a lambda function that processes charEvents. + */ +public interface CharEvent { + void process(char input); +} diff --git a/src/main/java/jterm/io/handlers/events/Event.java b/src/main/java/jterm/io/handlers/events/Event.java new file mode 100644 index 0000000..9fd2ea6 --- /dev/null +++ b/src/main/java/jterm/io/handlers/events/Event.java @@ -0,0 +1,13 @@ +package jterm.io.handlers.events; + +/** + * Functional interface allowing the implementation of an event. + * This and CharEvent exist so that another module can implement their own versions + * of certain event handlers, in the event that the provided implementation does something + * undesirable or inconvenient. + * + * @see CharEvent + */ +public interface Event { + void process(); +} diff --git a/src/main/java/jterm/io/input/Input.java b/src/main/java/jterm/io/input/Input.java new file mode 100644 index 0000000..ed86f93 --- /dev/null +++ b/src/main/java/jterm/io/input/Input.java @@ -0,0 +1,91 @@ +package jterm.io.input; + +// Copyright 2015 Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland +// www.source-code.biz, www.inventec.ch/chdh +// +// This module is multi-licensed and may be used under the terms of any of the following licenses: +// +// LGPL, GNU Lesser General Public License, V2.1 or later, http://www.gnu.org/licenses/lgpl.html +// EPL, Eclipse Public License, V1.0 or later, http://www.eclipse.org/legal +// +// Please contact the author if you need another license. +// This module is provided "as is", without warranties of any kind. +// +// Home page: http://www.source-code.biz/snippets/java/RawConsoleInput + +import java.io.IOException; + +/** + * A JNA based driver for reading single characters from the console. + *

+ *

This class is used for console mode programs. + * It supports non-blocking reads of single key strokes without echo. + */ + +public class Input { + + protected static final boolean isWindows = System.getProperty("os.name").startsWith("Windows"); + protected static final int invalidKey = 0xFFFE; + protected static final String invalidKeyStr = String.valueOf((char) invalidKey); + + protected static boolean initDone; + protected static boolean stdinIsConsole; + protected static boolean consoleModeAltered; + + /** + * Reads a character from the console without echo. + * + * @param wait true to wait until an input character is available, + * false to return immediately if no character is available. + * @return -2 if wait is false and no character is available. + * -1 on EOF. + * Otherwise an Unicode character code within the range 0 to 0xFFFF. + */ + public static int read(boolean wait) throws IOException { + if (isWindows) { + return WinInput.readWindows(wait); + } else { + try { + return UnixInput.readUnix(wait); + } catch (Exception e) { + System.err.println("Error reading input"); + } + } + + return -1; + } + + /** + * Resets console mode to normal line mode with echo. + *

+ *

On Windows this method re-enables Ctrl-C processing. + *

+ *

On Unix this method switches the console back to echo mode. + * read() leaves the console in non-echo mode. + */ + private static void resetConsoleMode() throws IOException { + if (isWindows) { + WinInput.resetConsoleModeWindows(); + } else { + try { + UnixInput.resetConsoleModeUnix(); + } catch (Exception e) { + System.err.println("Error resetting console mode"); + } + } + } + + /** + * Handles registering of shutdown hook to return console to normal line mode. + */ + protected static void registerShutdownHook() { + Runtime.getRuntime().addShutdownHook(new Thread(Input::shutdownHook)); + } + + private static void shutdownHook() { + try { + resetConsoleMode(); + } catch (Exception ignored) { + } + } +} \ No newline at end of file diff --git a/src/main/java/jterm/io/input/InputHandler.java b/src/main/java/jterm/io/input/InputHandler.java deleted file mode 100644 index c29525a..0000000 --- a/src/main/java/jterm/io/input/InputHandler.java +++ /dev/null @@ -1,366 +0,0 @@ -package jterm.io.input; - -import jterm.JTerm; -import jterm.io.output.TextColor; -import jterm.util.FileAutocomplete; -import jterm.util.CommandAutocomplete; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedList; - -public class InputHandler { - - // Position on prevCommands list (used to iterate through it) - private static int commandListPosition = 0; - - // Stores current TermInputProcessor.command when iterating through prevCommands - private static String currCommand = ""; - - // Stores all entered commands - private static final ArrayList prevCommands = new ArrayList<>(); - private static String command = ""; - - // For resetting all variables in FileAutocomplete once a key press other than a tab is registered - private static boolean resetVars = false; - private static int cursorPos = 0; - private static char lastChar; - - // Last arrow key that was pressed (if any other key is pressed sets to Keys.NONE) - private static Keys lastArrowPress = Keys.NONE; - - private static void setCommandListPosition(int commandListPos) { - commandListPosition = commandListPos; - } - - - public static void read() throws IOException { - int c1 = RawConsoleInput.read(true); - int c2 = RawConsoleInput.read(false); - int c3 = RawConsoleInput.read(false); - Keys keyType; - if (!(c2 == -2 && c3 == -2)) c1 = (c1 + c2 + c3) * -1; - keyType = Keys.getKeyByValue(c1); - process(keyType, (char) c1); - } - - public static void process(Keys key, char c) { - lastChar = c; - if (key != Keys.TAB) - resetVars = true; - - JTerm.out.printf(TextColor.INFO, "TEST LINE (C_INPUT): %c", c); - key.executeAction(); - } - - static void ctrlCEvent() { - System.exit(0); - } - - static void ctrlZEvent() { - System.exit(0); - } - - static void processUp() { - prevCommandIterator(Keys.UP); - setCursorPos(command.length()); - } - - static void processDown() { - prevCommandIterator(Keys.DOWN); - setCursorPos(command.length()); - } - - static void processLeft() { - if (getCursorPos() > 0) { - if (JTerm.isHeadless()) JTerm.out.print(TextColor.INPUT, "\b"); - decreaseCursorPos(); - } - } - - static void processRight() { - if (getCursorPos() < command.length()) { - if (JTerm.isHeadless()) { - JTerm.out.clearLine(command, cursorPos, false); - JTerm.out.print(TextColor.INPUT, command); - } - increaseCursorPos(); - moveToCursorPos(); - } - } - - /** - * Iterates through the prevCommands list. Emulates Unix terminal behaviour when using - * vertical arrow keys. - * - * @param ak Arrow key to process - */ - private static void prevCommandIterator(Keys ak) { - // Saves currently typed command before moving through the list of previously typed commands - if (lastArrowPress == Keys.NONE) - currCommand = command; - lastArrowPress = ak; - JTerm.out.clearLine(command, cursorPos, false); - commandListPosition += ak == Keys.UP ? -1 : 1; - commandListPosition = Math.max(commandListPosition, 0); - if (commandListPosition >= getPrevCommands().size()) { - commandListPosition = Math.min(commandListPosition, getPrevCommands().size()); - JTerm.out.print(TextColor.INPUT, currCommand); - command = currCommand; - } else { - JTerm.out.print(TextColor.INPUT, getPrevCommands().get(commandListPosition)); - command = getPrevCommands().get(commandListPosition); - } - } - - static void tabEvent() { - lastArrowPress = Keys.NONE; - fileAutocomplete(); - setResetVars(false); - } - - static void newLineEvent() { - JTerm.out.printf(TextColor.INFO, "TEST LINE (N_INPUT): %s", command); - lastArrowPress = Keys.NONE; - boolean empty = command.trim().isEmpty(); - - ArrayList prevCommands = getPrevCommands(); - - if (!empty) - prevCommands.add(command); - - setCommandListPosition(prevCommands.size()); - currCommand = ""; - setCursorPos(0); - setResetVars(true); - //System.out.println(); - JTerm.out.printf(TextColor.INFO, "TEST LINE (E_INPUT): %s", command); - parse(); - command = ""; - JTerm.out.printPrompt(); - } - - static void charEvent() { - lastArrowPress = Keys.NONE; - int cursorPos = getCursorPos(); - - if (getCursorPos() == command.length()) { - if (JTerm.isHeadless()) JTerm.out.print(TextColor.INPUT, lastChar); - command += lastChar; - } else { - JTerm.out.clearLine(command, cursorPos, false); - command = new StringBuilder(command).insert(cursorPos, lastChar).toString(); - JTerm.out.print(TextColor.INPUT, command); - } - - increaseCursorPos(); - moveToCursorPos(); - setResetVars(true); - } - - static void backspaceEvent() { - lastArrowPress = Keys.NONE; - if (command.length() > 0 && getCursorPos() > 0) { - - int charToDelete = getCursorPos() - 1; - if (JTerm.isHeadless()) - JTerm.out.clearLine(command, cursorPos, false); - - command = new StringBuilder(command).deleteCharAt(charToDelete).toString(); - - if (JTerm.isHeadless()) - JTerm.out.print(TextColor.INPUT, command); - - decreaseCursorPos(); - moveToCursorPos(); - setResetVars(true); - } - } - - /** - * Sends command to terminal class for parsing, source is the newlineEvent in the key processor - */ - private static void parse() { - JTerm.out.printf(TextColor.INFO, "TEST LINE (P_INPUT): %s", command); - Arrays.stream(command.split("&&")).forEach(command -> JTerm.executeCommand(command.trim())); - } - - /** - * Moves the cursor from the end of the command to where it should be (if the user is using arrow keys) - * Usually only used after modifying 'command' - */ - private static void moveToCursorPos() { - if (JTerm.isHeadless()) { - for (int i = command.length(); i > cursorPos; i--) - JTerm.out.print(TextColor.INPUT, "\b"); - } - } - - /** - * Autocompletes desired file name similar to how terminals do it. - */ - private static void fileAutocomplete() { - - if (resetVars) - FileAutocomplete.resetVars(); - - if (FileAutocomplete.getFiles() == null) { - FileAutocomplete.init(disassembleCommand(command), false, false); - resetVars = false; - } else - FileAutocomplete.fileAutocomplete(); - - command = FileAutocomplete.getCommand(); - - if (FileAutocomplete.isResetVars()) - FileAutocomplete.resetVars(); - - // Get variables and set cursor position - setCursorPos(FileAutocomplete.getCursorPos()); - moveToCursorPos(); - } - - /** - * Autocompletes desired file name similar to how terminals do it. - */ - private static void commandAutocomplete() { - - if (resetVars) - CommandAutocomplete.resetVars(); - - if (CommandAutocomplete.getPossibleCommands() == null) { - CommandAutocomplete.init(disassembleCommand(command), false, false); - resetVars = false; - } else - CommandAutocomplete.commandAutocomplete(); - - command = CommandAutocomplete.getCommand(); - - if (CommandAutocomplete.isResetVars()) - CommandAutocomplete.resetVars(); - - // Get variables and set cursor position - setCursorPos(CommandAutocomplete.getCursorPos()); - moveToCursorPos(); - } - - /** - * Splits a command into 3 parts for the autocomplete function to operate properly. - *

- * Elements 0 and 2 are the non-relevant part of the command to the autocomplete function - * and are used when stitching the autocompleted command back together. - *

- * Element 1 is the portion of the command that needs completing, and the one on which - * the autocomplete class will operate on. - * - * @param command Command to split - * @return Returns disassembled string, with non relevant info in elements 0 and 2, and the string to autocomplete - * in element 1 - */ - private static String[] disassembleCommandOld(String command) { - - if (!command.contains("&&")) - return new String[]{"", command, ""}; - - LinkedList ampPos = new LinkedList<>(); - for (int i = 0; i < command.length() - 1; i++) { - if (command.substring(i, i + 2).equals("&&")) { - ampPos.add(i); - if (cursorPos - i < 2 && cursorPos - i > 0) - return new String[]{"", command, ""}; - } - } - - String[] splitCommand = new String[3]; - - if (ampPos.size() > 1) { - // Deals with commands that have more than one && - for (int i = 0; i < ampPos.size(); i++) { - if (ampPos.get(i) > cursorPos) { - splitCommand[0] = command.substring(0, ampPos.get(i - 1) + 2) + " "; - splitCommand[1] = command.substring(ampPos.get(i - 1) + 2, cursorPos); - splitCommand[2] = " " + command.substring(cursorPos, command.length()); - } else if (i + 1 == ampPos.size()) { - splitCommand[0] = command.substring(0, ampPos.get(i) + 2) + " "; - splitCommand[1] = command.substring(ampPos.get(i) + 2, cursorPos); - splitCommand[2] = " " + command.substring(cursorPos, command.length()); - } - } - } else { - // Deals with commands that have exactly one && - if (cursorPos > ampPos.get(0)) { - splitCommand[0] = command.substring(0, ampPos.get(0) + 2) + " "; - splitCommand[1] = command.substring(ampPos.get(0) + 2, cursorPos); - splitCommand[2] = command.substring(cursorPos, command.length()); - } else if (cursorPos < ampPos.get(0)) { - splitCommand[0] = ""; - splitCommand[1] = command.substring(0, cursorPos); - splitCommand[2] = command.substring(cursorPos, command.length()); - } else { - String[] split = command.split("&&"); - splitCommand[0] = split[0]; - splitCommand[1] = ""; - splitCommand[2] = "&&" + split[1]; - } - } - - // Remove spaces so that autocomplete can work properly - splitCommand[1] = splitCommand[1].trim(); - - return splitCommand; - } - - private static String[] disassembleCommand(String command) { - String[] out = new String[]{"", "", ""}; - - if (!command.contains("&&")) { - out[1] = command.substring(0, cursorPos); - out[2] = command.substring(cursorPos); - return out; - } - - String[] commands = Arrays.stream(command.split("&&")).map(String::trim).toArray(String[]::new); - int len = 0; - int i = 0; - - while (cursorPos > (len + commands[i].length() + 4)) { - out[0] += commands[i] + " && "; - i++; - len += commands[i].length() + 4; - } - - out[1] = commands[i].substring(0, cursorPos - len); - out[2] = commands[i++].substring(cursorPos - len); - - while (i < commands.length) { - out[2] += " && " + commands[i]; - i++; - } - return out; - } - - private static ArrayList getPrevCommands() { - return prevCommands; - } - - private static void setResetVars(boolean resetVa) { - resetVars = resetVa; - } - - private static void increaseCursorPos() { - cursorPos++; - } - - private static void decreaseCursorPos() { - cursorPos--; - } - - private static int getCursorPos() { - return cursorPos; - } - - public static void setCursorPos(int cursorPosition) { - cursorPos = cursorPosition; - } -} diff --git a/src/main/java/jterm/io/input/Keys.java b/src/main/java/jterm/io/input/Keys.java index 6c18286..a776327 100644 --- a/src/main/java/jterm/io/input/Keys.java +++ b/src/main/java/jterm/io/input/Keys.java @@ -1,42 +1,63 @@ package jterm.io.input; -public enum Keys { +import jterm.io.handlers.events.CharEvent; +import jterm.io.handlers.events.Event; - UP(InputHandler::processUp), - DOWN(InputHandler::processDown), - LEFT(InputHandler::processLeft), - RIGHT(InputHandler::processRight), - TAB(9, InputHandler::tabEvent), - BACKSPACE(InputHandler::backspaceEvent), - NWLN(InputHandler::newLineEvent), - CHAR(InputHandler::charEvent), - CTRL_C(3, InputHandler::ctrlCEvent), - CTRL_Z(26, InputHandler::ctrlZEvent), - NONE(-1, null); +public enum Keys { + UP, + DOWN, + LEFT, + RIGHT, + TAB(9), + BACKSPACE, + NWLN, + CHAR, + CTRL_C(3), + CTRL_Z(26), + NONE(-1); int value; - final Runnable r; - - Keys(Runnable r) { - this.r = r; - } + Event event; + CharEvent charEvent; - public void executeAction() { - if (r != null) r.run(); + Keys() { } - Keys(int value, Runnable r) { - this.r = r; + Keys(final int value) { this.value = value; } + public boolean executeAction() { + if (event != null) { + event.process(); + return true; + } + return false; + } + + public boolean executeAction(final char c) { + if (charEvent != null) { + charEvent.process(c); + return true; + } + return false; + } + public int getValue() { return value; } - public void setValue(int value) { + public void setValue(final int value) { this.value = value; } + public void setEvent(final Event event) { + this.event = event; + } + + public void setCharEvent(CharEvent charEvent) { + this.charEvent = charEvent; + } + public static void initWindows() { UP.value = 57416; DOWN.value = 57424; @@ -64,12 +85,13 @@ public static void initGUI() { NWLN.value = 10; } - public static Keys getKeyByValue(int c) { - Keys[] keys = Keys.values(); + public static Keys getKeyByValue(final int c) { + final Keys[] keys = Keys.values(); for (Keys key : keys) { if (c == (key.value)) return key; } + CHAR.setValue(c); return CHAR; } } diff --git a/src/main/java/jterm/io/input/RawConsoleInput.java b/src/main/java/jterm/io/input/RawConsoleInput.java deleted file mode 100644 index 70fb03c..0000000 --- a/src/main/java/jterm/io/input/RawConsoleInput.java +++ /dev/null @@ -1,368 +0,0 @@ -// Copyright 2015 Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland -// www.source-code.biz, www.inventec.ch/chdh -// -// This module is multi-licensed and may be used under the terms of any of the following licenses: -// -// LGPL, GNU Lesser General Public License, V2.1 or later, http://www.gnu.org/licenses/lgpl.html -// EPL, Eclipse Public License, V1.0 or later, http://www.eclipse.org/legal -// -// Please contact the author if you need another license. -// This module is provided "as is", without warranties of any kind. -// -// Home page: http://www.source-code.biz/snippets/java/RawConsoleInput - -package jterm.io.input; - -import com.sun.jna.*; -import com.sun.jna.ptr.IntByReference; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.Charset; -import java.nio.charset.CharsetDecoder; -import java.nio.charset.CodingErrorAction; -import java.util.Arrays; -import java.util.List; - -/** - * A JNA based driver for reading single characters from the console. - *

- *

This class is used for console mode programs. - * It supports non-blocking reads of single key strokes without echo. - */ -public class RawConsoleInput { - - private static final boolean isWindows = System.getProperty("os.name").startsWith("Windows"); - private static final int invalidKey = 0xFFFE; - private static final String invalidKeyStr = String.valueOf((char) invalidKey); - - private static boolean initDone; - private static boolean stdinIsConsole; - private static boolean consoleModeAltered; - - /** - * Reads a character from the console without echo. - * - * @param wait true to wait until an input character is available, - * false to return immediately if no character is available. - * @return -2 if wait is false and no character is available. - * -1 on EOF. - * Otherwise an Unicode character code within the range 0 to 0xFFFF. - */ - public static int read(boolean wait) throws IOException { - if (isWindows) { - return readWindows(wait); - } else { - return readUnix(wait); - } - } - - /** - * Resets console mode to normal line mode with echo. - *

- *

On Windows this method re-enables Ctrl-C processing. - *

- *

On Unix this method switches the console back to echo mode. - * read() leaves the console in non-echo mode. - */ - public static void resetConsoleMode() throws IOException { - if (isWindows) { - resetConsoleModeWindows(); - } else { - resetConsoleModeUnix(); - } - } - - private static void registerShutdownHook() { - Runtime.getRuntime().addShutdownHook(new Thread(RawConsoleInput::shutdownHook)); - } - - private static void shutdownHook() { - try { - resetConsoleMode(); - } catch (Exception e) { - } - } - -//--- Windows ------------------------------------------------------------------ - -// The Windows version uses _kbhit() and _getwch() from msvcrt.dll. - - private static Msvcrt msvcrt; - private static Kernel32 kernel32; - private static Pointer consoleHandle; - private static int originalConsoleMode; - - private static int readWindows(boolean wait) throws IOException { - initWindows(); - if (!stdinIsConsole) { - int c = msvcrt.getwchar(); - if (c == 0xFFFF) { - c = -1; - } - return c; - } - consoleModeAltered = true; - setConsoleMode(consoleHandle, originalConsoleMode & ~Kernel32Defs.ENABLE_PROCESSED_INPUT); - // ENABLE_PROCESSED_INPUT must remain off to prevent Ctrl-C from beeing processed by the system - // while the program is not within getwch(). - if (!wait && msvcrt._kbhit() == 0) { - return -2; - } // no key available - return getwch(); - } - - private static int getwch() { - int c = msvcrt._getwch(); - if (c == 0 || c == 0xE0) { // Function key or arrow key - c = msvcrt._getwch(); - if (c >= 0 && c <= 0x18FF) { - return 0xE000 + c; - } // construct key code in private Unicode range - return invalidKey; - } - if (c < 0 || c > 0xFFFF) { - return invalidKey; - } - return c; - } // normal key - - private static synchronized void initWindows() throws IOException { - if (initDone) { - return; - } - msvcrt = (Msvcrt) Native.loadLibrary("msvcrt", Msvcrt.class); - kernel32 = (Kernel32) Native.loadLibrary("kernel32", Kernel32.class); - try { - consoleHandle = getStdInputHandle(); - originalConsoleMode = getConsoleMode(consoleHandle); - stdinIsConsole = true; - } catch (IOException e) { - stdinIsConsole = false; - } - if (stdinIsConsole) { - registerShutdownHook(); - } - initDone = true; - } - - private static Pointer getStdInputHandle() throws IOException { - Pointer handle = kernel32.GetStdHandle(Kernel32Defs.STD_INPUT_HANDLE); - if (Pointer.nativeValue(handle) == 0 || Pointer.nativeValue(handle) == Kernel32Defs.INVALID_HANDLE_VALUE) { - throw new IOException("GetStdHandle(STD_INPUT_HANDLE) failed."); - } - return handle; - } - - private static int getConsoleMode(Pointer handle) throws IOException { - IntByReference mode = new IntByReference(); - int rc = kernel32.GetConsoleMode(handle, mode); - if (rc == 0) { - throw new IOException("GetConsoleMode() failed."); - } - return mode.getValue(); - } - - private static void setConsoleMode(Pointer handle, int mode) throws IOException { - int rc = kernel32.SetConsoleMode(handle, mode); - if (rc == 0) { - throw new IOException("SetConsoleMode() failed."); - } - } - - private static void resetConsoleModeWindows() throws IOException { - if (!initDone || !stdinIsConsole || !consoleModeAltered) { - return; - } - setConsoleMode(consoleHandle, originalConsoleMode); - consoleModeAltered = false; - } - - private static interface Msvcrt extends Library { - int _kbhit(); - - int _getwch(); - - int getwchar(); - } - - private static class Kernel32Defs { - static final int STD_INPUT_HANDLE = -10; - static final long INVALID_HANDLE_VALUE = (Pointer.SIZE == 8) ? -1 : 0xFFFFFFFFL; - static final int ENABLE_PROCESSED_INPUT = 0x0001; - static final int ENABLE_LINE_INPUT = 0x0002; - static final int ENABLE_ECHO_INPUT = 0x0004; - static final int ENABLE_WINDOW_INPUT = 0x0008; - } - - private static interface Kernel32 extends Library { - int GetConsoleMode(Pointer hConsoleHandle, IntByReference lpMode); - - int SetConsoleMode(Pointer hConsoleHandle, int dwMode); - - Pointer GetStdHandle(int nStdHandle); - } - -//--- Unix --------------------------------------------------------------------- - -// The Unix version uses tcsetattr() to switch the console to non-canonical mode, -// System.in.available() to check whether data is available and System.in.read() -// to read bytes from the console. -// A CharsetDecoder is used to convert bytes to characters. - - private static final int stdinFd = 0; - private static Libc libc; - private static CharsetDecoder charsetDecoder; - private static Termios originalTermios; - private static Termios rawTermios; - private static Termios intermediateTermios; - - private static int readUnix(boolean wait) throws IOException { - initUnix(); - if (!stdinIsConsole) { // STDIN is not a console - return readSingleCharFromByteStream(System.in); - } - consoleModeAltered = true; - setTerminalAttrs(stdinFd, rawTermios); // switch off canonical mode, echo and signals - try { - if (!wait && System.in.available() == 0) { - return -2; - } // no input available - return readSingleCharFromByteStream(System.in); - } finally { - setTerminalAttrs(stdinFd, intermediateTermios); - } - } // reset some console attributes - - private static Termios getTerminalAttrs(int fd) throws IOException { - Termios termios = new Termios(); - try { - int rc = libc.tcgetattr(fd, termios); - if (rc != 0) { - throw new RuntimeException("tcgetattr() failed."); - } - } catch (LastErrorException e) { - throw new IOException("tcgetattr() failed.", e); - } - return termios; - } - - private static void setTerminalAttrs(int fd, Termios termios) throws IOException { - try { - int rc = libc.tcsetattr(fd, LibcDefs.TCSANOW, termios); - if (rc != 0) { - throw new RuntimeException("tcsetattr() failed."); - } - } catch (LastErrorException e) { - throw new IOException("tcsetattr() failed.", e); - } - } - - private static int readSingleCharFromByteStream(InputStream inputStream) throws IOException { - byte[] inBuf = new byte[4]; - int inLen = 0; - while (true) { - if (inLen >= inBuf.length) { // input buffer overflow - return invalidKey; - } - int b = inputStream.read(); // read next byte - if (b == -1) { // EOF - return -1; - } - inBuf[inLen++] = (byte) b; - int c = decodeCharFromBytes(inBuf, inLen); - if (c != -1) { - return c; - } - } - } - - // (This method is synchronized because the charsetDecoder must only be used by a single thread at once.) - private static synchronized int decodeCharFromBytes(byte[] inBytes, int inLen) { - charsetDecoder.reset(); - charsetDecoder.onMalformedInput(CodingErrorAction.REPLACE); - charsetDecoder.replaceWith(invalidKeyStr); - ByteBuffer in = ByteBuffer.wrap(inBytes, 0, inLen); - CharBuffer out = CharBuffer.allocate(1); - charsetDecoder.decode(in, out, false); - if (out.position() == 0) { - return -1; - } - return out.get(0); - } - - private static synchronized void initUnix() throws IOException { - if (initDone) { - return; - } - libc = (Libc) Native.loadLibrary("c", Libc.class); - stdinIsConsole = libc.isatty(stdinFd) == 1; - charsetDecoder = Charset.defaultCharset().newDecoder(); - if (stdinIsConsole) { - originalTermios = getTerminalAttrs(stdinFd); - rawTermios = new Termios(originalTermios); - rawTermios.c_lflag &= ~(LibcDefs.ICANON | LibcDefs.ECHO | LibcDefs.ECHONL | LibcDefs.ISIG); - intermediateTermios = new Termios(rawTermios); - intermediateTermios.c_lflag |= LibcDefs.ICANON; - // Canonical mode can be switched off between the read() calls, but echo must remain disabled. - registerShutdownHook(); - } - initDone = true; - } - - private static void resetConsoleModeUnix() throws IOException { - if (!initDone || !stdinIsConsole || !consoleModeAltered) { - return; - } - setTerminalAttrs(stdinFd, originalTermios); - consoleModeAltered = false; - } - - protected static class Termios extends Structure { // termios.h - public int c_iflag; - public int c_oflag; - public int c_cflag; - public int c_lflag; - public byte c_line; - public byte[] filler = new byte[64]; // actual length is platform dependent - - @Override - protected List getFieldOrder() { - return Arrays.asList("c_iflag", "c_oflag", "c_cflag", "c_lflag", "c_line", "filler"); - } - - Termios() { - } - - Termios(Termios t) { - c_iflag = t.c_iflag; - c_oflag = t.c_oflag; - c_cflag = t.c_cflag; - c_lflag = t.c_lflag; - c_line = t.c_line; - filler = t.filler.clone(); - } - } - - private static class LibcDefs { - // termios.h - static final int ISIG = 0000001; - static final int ICANON = 0000002; - static final int ECHO = 0000010; - static final int ECHONL = 0000100; - static final int TCSANOW = 0; - } - - private static interface Libc extends Library { - // termios.h - int tcgetattr(int fd, Termios termios) throws LastErrorException; - - int tcsetattr(int fd, int opt, Termios termios) throws LastErrorException; - - // unistd.h - int isatty(int fd); - } - -} \ No newline at end of file diff --git a/src/main/java/jterm/io/input/UnixInput.java b/src/main/java/jterm/io/input/UnixInput.java new file mode 100644 index 0000000..432dfcc --- /dev/null +++ b/src/main/java/jterm/io/input/UnixInput.java @@ -0,0 +1,179 @@ +package jterm.io.input; + +import com.sun.jna.LastErrorException; +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Structure; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CodingErrorAction; +import java.util.Arrays; +import java.util.List; + +import static jterm.io.input.Input.*; + +public class UnixInput { + +// The Unix version uses tcsetattr() to switch the console to non-canonical mode, +// System.in.available() to check whether data is available and System.in.read() +// to read bytes from the console. +// A CharsetDecoder is used to convert bytes to characters. + + private static final int stdinFd = 0; + private static Libc libc; + private static CharsetDecoder charsetDecoder; + private static Termios originalTermios; + private static Termios rawTermios; + private static Termios intermediateTermios; + + protected static int readUnix(boolean wait) throws Exception { + initUnix(); + if (!stdinIsConsole) { // STDIN is not a console + return readSingleCharFromByteStream(System.in); + } + consoleModeAltered = true; + setTerminalAttrs(stdinFd, rawTermios); // switch off canonical mode, echo and signals + try { + if (!wait && System.in.available() == 0) { + return -2; + } // no input available + return readSingleCharFromByteStream(System.in); + } finally { + setTerminalAttrs(stdinFd, intermediateTermios); + } + } // reset some console attributes + + private static Termios getTerminalAttrs(int fd) throws Exception { + Termios termios = new Termios(); + try { + int rc = libc.tcgetattr(fd, termios); + if (rc != 0) { + throw new Exception("tcgetattr() failed."); + } + } catch (LastErrorException e) { + throw new IOException("tcgetattr() failed.", e); + } + return termios; + } + + private static void setTerminalAttrs(int fd, Termios termios) throws Exception { + try { + int rc = libc.tcsetattr(fd, LibcDefs.TCSANOW, termios); + if (rc != 0) { + throw new Exception("tcsetattr() failed."); + } + } catch (LastErrorException e) { + throw new IOException("tcsetattr() failed.", e); + } + } + + private static int readSingleCharFromByteStream(InputStream inputStream) throws IOException { + byte[] inBuf = new byte[4]; + int inLen = 0; + while (true) { + if (inLen >= inBuf.length) { // input buffer overflow + return invalidKey; + } + int b = inputStream.read(); // read next byte + if (b == -1) { // EOF + return -1; + } + inBuf[inLen++] = (byte) b; + int c = decodeCharFromBytes(inBuf, inLen); + if (c != -1) { + return c; + } + } + } + + // (This method is synchronized because the charsetDecoder must only be used by a single thread at once.) + private static synchronized int decodeCharFromBytes(byte[] inBytes, int inLen) { + charsetDecoder.reset(); + charsetDecoder.onMalformedInput(CodingErrorAction.REPLACE); + charsetDecoder.replaceWith(invalidKeyStr); + ByteBuffer in = ByteBuffer.wrap(inBytes, 0, inLen); + CharBuffer out = CharBuffer.allocate(1); + charsetDecoder.decode(in, out, false); + if (out.position() == 0) { + return -1; + } + return out.get(0); + } + + private static synchronized void initUnix() throws Exception { + if (initDone) { + return; + } + libc = Native.loadLibrary("c", Libc.class); + stdinIsConsole = libc.isatty(stdinFd) == 1; + charsetDecoder = Charset.defaultCharset().newDecoder(); + if (stdinIsConsole) { + originalTermios = getTerminalAttrs(stdinFd); + rawTermios = new Termios(originalTermios); + rawTermios.c_lflag &= ~(LibcDefs.ICANON | LibcDefs.ECHO | LibcDefs.ECHONL | LibcDefs.ISIG); + intermediateTermios = new Termios(rawTermios); + intermediateTermios.c_lflag |= LibcDefs.ICANON; + // Canonical mode can be switched off between the read() calls, but echo must remain disabled. + registerShutdownHook(); + } + initDone = true; + } + + protected static void resetConsoleModeUnix() throws Exception { + if (!initDone || !stdinIsConsole || !consoleModeAltered) { + return; + } + setTerminalAttrs(stdinFd, originalTermios); + consoleModeAltered = false; + } + + protected static class Termios extends Structure { // termios.h + public int c_iflag; + public int c_oflag; + public int c_cflag; + public int c_lflag; + public byte c_line; + public byte[] filler = new byte[64]; // actual length is platform dependent + + @Override + protected List getFieldOrder() { + return Arrays.asList("c_iflag", "c_oflag", "c_cflag", "c_lflag", "c_line", "filler"); + } + + Termios() { + } + + Termios(Termios t) { + c_iflag = t.c_iflag; + c_oflag = t.c_oflag; + c_cflag = t.c_cflag; + c_lflag = t.c_lflag; + c_line = t.c_line; + filler = t.filler.clone(); + } + } + + private static class LibcDefs { + // termios.h + private static final int ISIG = 0000001; + private static final int ICANON = 0000002; + private static final int ECHO = 0000010; + private static final int ECHONL = 0000100; + private static final int TCSANOW = 0; + } + + private interface Libc extends Library { + // termios.h + int tcgetattr(int fd, Termios termios) throws LastErrorException; + + int tcsetattr(int fd, int opt, Termios termios) throws LastErrorException; + + // unistd.h + int isatty(int fd); + } +} diff --git a/src/main/java/jterm/io/input/WinInput.java b/src/main/java/jterm/io/input/WinInput.java new file mode 100644 index 0000000..78d0e95 --- /dev/null +++ b/src/main/java/jterm/io/input/WinInput.java @@ -0,0 +1,128 @@ +package jterm.io.input; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.ptr.IntByReference; + +import java.io.IOException; + +import static jterm.io.input.Input.*; + +public class WinInput { + +// The Windows version uses _kbhit() and _getwch() from msvcrt.dll. + + private static Msvcrt msvcrt; + private static Kernel32 kernel32; + private static Pointer consoleHandle; + private static int originalConsoleMode; + + protected static int readWindows(boolean wait) throws IOException { + initWindows(); + if (!stdinIsConsole) { + int c = msvcrt.getwchar(); + if (c == 0xFFFF) { + c = -1; + } + return c; + } + consoleModeAltered = true; + setConsoleMode(consoleHandle, originalConsoleMode & ~Kernel32Defs.ENABLE_PROCESSED_INPUT); + // ENABLE_PROCESSED_INPUT must remain off to prevent Ctrl-C from beeing processed by the system + // while the program is not within getwch(). + if (!wait && msvcrt.kbhit() == 0) { + return -2; + } // no key available + return getwch(); + } + + private static int getwch() { + int c = msvcrt.getwch(); + if (c == 0 || c == 0xE0) { // Function key or arrow key + c = msvcrt.getwch(); + if (c >= 0 && c <= 0x18FF) { + return 0xE000 + c; + } // construct key code in private Unicode range + return invalidKey; + } + if (c < 0 || c > 0xFFFF) { + return invalidKey; + } + return c; + } // normal key + + private static synchronized void initWindows() throws IOException { + if (initDone) { + return; + } + msvcrt = Native.loadLibrary("msvcrt", Msvcrt.class); + kernel32 = Native.loadLibrary("kernel32", Kernel32.class); + try { + consoleHandle = getStdInputHandle(); + originalConsoleMode = getConsoleMode(consoleHandle); + stdinIsConsole = true; + } catch (IOException e) { + stdinIsConsole = false; + } + if (stdinIsConsole) { + registerShutdownHook(); + } + initDone = true; + } + + private static Pointer getStdInputHandle() throws IOException { + Pointer handle = kernel32.getStdHandle(Kernel32Defs.STD_INPUT_HANDLE); + if (Pointer.nativeValue(handle) == 0 || Pointer.nativeValue(handle) == Kernel32Defs.INVALID_HANDLE_VALUE) { + throw new IOException("GetStdHandle(STD_INPUT_HANDLE) failed."); + } + return handle; + } + + private static int getConsoleMode(Pointer handle) throws IOException { + IntByReference mode = new IntByReference(); + int rc = kernel32.getConsoleMode(handle, mode); + if (rc == 0) { + throw new IOException("GetConsoleMode() failed."); + } + return mode.getValue(); + } + + private static void setConsoleMode(Pointer handle, int mode) throws IOException { + int rc = kernel32.setConsoleMode(handle, mode); + if (rc == 0) { + throw new IOException("SetConsoleMode() failed."); + } + } + + protected static void resetConsoleModeWindows() throws IOException { + if (!initDone || !stdinIsConsole || !consoleModeAltered) { + return; + } + setConsoleMode(consoleHandle, originalConsoleMode); + consoleModeAltered = false; + } + + private interface Msvcrt extends Library { + int kbhit(); + + int getwch(); + + int getwchar(); + } + + private static class Kernel32Defs { + private static final int STD_INPUT_HANDLE = -10; + private static final long INVALID_HANDLE_VALUE = (Pointer.SIZE == 8) ? -1 : 0xFFFFFFFFL; + private static final int ENABLE_PROCESSED_INPUT = 0x0001; + } + + private interface Kernel32 extends Library { + int getConsoleMode(Pointer hConsoleHandle, IntByReference lpMode); + + int setConsoleMode(Pointer hConsoleHandle, int dwMode); + + Pointer getStdHandle(int nStdHandle); + } + +} diff --git a/src/main/java/jterm/io/output/GuiPrinter.java b/src/main/java/jterm/io/output/GuiPrinter.java index 6717956..69a5f7d 100644 --- a/src/main/java/jterm/io/output/GuiPrinter.java +++ b/src/main/java/jterm/io/output/GuiPrinter.java @@ -8,62 +8,66 @@ import java.lang.reflect.InvocationTargetException; import java.util.Locale; +/** + * Custom {@link Printer} implementation for the JTerm GUI mode. + */ public class GuiPrinter implements Printer { private final JTextPane textPane; - private ProtectedTextComponent protectedDoc; - private SimpleAttributeSet style; + private ProtectedTextComponent ptc; - public GuiPrinter(JTextPane textPane) { + public GuiPrinter(final JTextPane textPane) { this.textPane = textPane; - protectedDoc = new ProtectedTextComponent(textPane); - style = new SimpleAttributeSet(); - StyleConstants.setFontFamily(style, "Monospace Regular"); - StyleConstants.setBold(style, true); + ptc = new ProtectedTextComponent(textPane); } - public void print(TextColor color, String x) { - print(x, color); + public void print(final TextColor color, final String str) { + print(str, color); } @Override - public void print(TextColor color, char x) { - print(String.valueOf(x), color); + public void print(final TextColor color, final char c) { + print(String.valueOf(c), color); } - public void print(TextColor color, Object x) { - print(String.valueOf(x), color); + @Override + public void println() { + System.out.println(); } - public void println() { - print("\n", TextColor.INPUT); + public void print(final TextColor color, final Object o) { + print(String.valueOf(o), color); + } + + public void println(final TextColor color) { + print("\n", color); } - public void println(TextColor color, String x) { - print(x + "\n", color); + public void println(final TextColor color, final String str) { + print(str + "\n", color); } @Override - public void println(TextColor color, char x) { - print(String.valueOf(x) + "\n", color); + public void println(final TextColor color, final char c) { + print(c + "\n", color); } - public void println(TextColor color, Object x) { - print(String.valueOf(x) + "\n", color); + public void println(final TextColor color, final Object o) { + print(o + "\n", color); } - public GuiPrinter printf(TextColor color, String format, Object... args) { + public GuiPrinter printf(final TextColor color, final String format, final Object... args) { print(String.format(format, args), color); return this; } - public GuiPrinter printf(TextColor color, Locale l, String format, Object... args) { + public GuiPrinter printf(final TextColor color, final Locale l, final String format, final Object... args) { print(String.format(l, format, args), color); return this; } @Override - public void printWithPrompt(TextColor color, String s) { + public void printWithPrompt(final TextColor color, final String s) { printPrompt(); print(s, color); } @@ -72,45 +76,48 @@ public void printWithPrompt(TextColor color, String s) { public void printPrompt() { invoke(() -> { print(JTerm.currentDirectory, TextColor.PATH); - print(">>", TextColor.PROMPT); - print(" ", TextColor.INPUT); + print(JTerm.PROMPT, TextColor.PROMPT); + print("", TextColor.INPUT); int promptIndex = textPane.getDocument().getLength(); textPane.setCaretPosition(promptIndex); - protectedDoc.protectText(0, promptIndex - 1); + ptc.protectText(0, promptIndex - 1); }); } - public void clearLine(String line, int cursorPosition, boolean clearPrompt) { - if (clearPrompt) protectedDoc.clearProtections(); - String text = textPane.getText().replaceAll("\r", ""); - int ix = text.lastIndexOf("\n") + 1; - int len = line.length(); - int fullPromptLen = JTerm.PROMPT.length() + JTerm.currentDirectory.length(); - if (clearPrompt) len += fullPromptLen; - else ix += fullPromptLen; - if (ix >= text.length()) return; - try { - textPane.getDocument().remove(ix, len); - } catch (BadLocationException e) { - e.printStackTrace(); + public void clearLine(final String line, final int cursorPosition, final boolean clearPrompt) { + if (clearPrompt || line.length() > 0) { + if (clearPrompt) + ptc.clearProtections(); + + String textToClear = ""; + if (clearPrompt) + textToClear += JTerm.currentDirectory + JTerm.PROMPT; + textToClear += line; + + try { + textPane.getDocument().remove(textPane.getText().lastIndexOf(textToClear), textToClear.length()); + } catch (BadLocationException e) { + e.printStackTrace(); + } } } @Override public void clearAll() { - protectedDoc.clearProtections(); + ptc.clearProtections(); textPane.setText(""); } - private void print(String s, TextColor c) { - StyleConstants.setForeground(style, c.getColor()); - int len = textPane.getDocument().getLength(); + private void print(final String s, final TextColor c) { + final StyleContext sc = StyleContext.getDefaultStyleContext(); + final AttributeSet color = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, c.getColor()); + final int len = textPane.getDocument().getLength(); textPane.setCaretPosition(len); - textPane.setCharacterAttributes(style, false); + textPane.setCharacterAttributes(color, false); textPane.replaceSelection(s); } - private void invoke(Runnable action) { + private void invoke(final Runnable action) { try { SwingUtilities.invokeAndWait(action); } catch (InterruptedException | InvocationTargetException e) { diff --git a/src/main/java/jterm/io/output/HeadlessPrinter.java b/src/main/java/jterm/io/output/HeadlessPrinter.java index dbda165..b956db5 100644 --- a/src/main/java/jterm/io/output/HeadlessPrinter.java +++ b/src/main/java/jterm/io/output/HeadlessPrinter.java @@ -5,76 +5,76 @@ import java.io.IOException; +/** + * Custom {@link Printer} implementation for the headless JTerm mode. + */ public class HeadlessPrinter implements Printer { - private static final String ANSI_CLS = "\u001b[2J"; - private static final String ANSI_HOME = "\u001b[H"; + private static final String ANSI_CLS = "\u001b[2J"; + private static final String ANSI_HOME = "\u001b[H"; - private static final String RESET = (char) 27 + "[0m"; + @Override + public void print(final TextColor color, final String str) { + System.out.print(color.getANSIColor() + str); + } - @Override - public void print(TextColor color, String x) { - System.out.print(color.getANSIColor() + x + RESET + RESET); - } + @Override + public void print(final TextColor color, final char c) { + System.out.print(color.getANSIColor() + c); + } - @Override - public void print(TextColor color, char x) { - System.out.print(color.getANSIColor() + x + RESET); - } + @Override + public void println() { + System.out.println(); + } - @Override - public void println() { - System.out.println(); - } + @Override + public void println(final TextColor color, final String str) { + System.out.println(color.getANSIColor() + str); + } - @Override - public void println(TextColor color, String x) { - System.out.println(color.getANSIColor() + x + RESET); - } + @Override + public void println(final TextColor color, final char c) { + System.out.println(color.getANSIColor() + c); + } - @Override - public void println(TextColor color, char x) { - System.out.println(color.getANSIColor() + x + RESET); - } + @Override + public Printer printf(final TextColor color, final String format, final Object... args) { + System.out.println(color.getANSIColor() + String.format(format, args)); + return this; + } - @Override - public Printer printf(TextColor color, String format, Object... args) { - System.out.println(color.getANSIColor() + String.format(format, args) + RESET); - return this; - } + public void printPrompt() { + System.out.print(TextColor.PATH.getANSIColor() + JTerm.currentDirectory); + System.out.print(TextColor.PROMPT.getANSIColor() + JTerm.PROMPT); + } - public void printPrompt() { - System.out.print(TextColor.PATH.getANSIColor() + JTerm.currentDirectory + RESET); - System.out.print(TextColor.PATH.getANSIColor() + JTerm.PROMPT + RESET); - } + public void printWithPrompt(final TextColor color, final String s) { + printPrompt(); + System.out.print(color.getANSIColor() + s); + } - public void printWithPrompt(TextColor color, String s) { - printPrompt(); - System.out.print(color.getANSIColor() + s + RESET); - } + @Override + public void clearLine(final String line, final int cursorPosition, final boolean clearPrompt) { + final int charsToClear = line.length() + (clearPrompt ? JTerm.PROMPT.length() + JTerm.currentDirectory.length() : 0); + for (int i = 0; i < charsToClear; i++) + System.out.print('\b'); + for (int i = 0; i < charsToClear; i++) + System.out.print(' '); + for (int i = 0; i < charsToClear; i++) + System.out.print('\b'); + } - @Override - public void clearLine(String line, int cursorPosition, boolean clearPrompt) { - int promptLength = JTerm.PROMPT.length() + JTerm.currentDirectory.length(); - - for (int i = 0; i < cursorPosition + (clearPrompt ? promptLength : 0); i++) - JTerm.out.print(TextColor.INFO, '\b'); - for (int i = 0; i < line.length() + (clearPrompt ? promptLength : 0); i++) - JTerm.out.print(TextColor.INFO, ' '); - for (int i = 0; i < line.length() + (clearPrompt ? promptLength : 0); i++) - JTerm.out.print(TextColor.INFO, '\b'); - } - - @Override - public void clearAll() { - if (JTerm.IS_UNIX) { // escape sequences to clear the screen - System.out.print(ANSI_CLS + ANSI_HOME); - System.out.flush(); - } else if (JTerm.IS_WIN) { // Invoke the command line interpreter's own 'clear' command for Windows OS - try { - new ProcessBuilder("cmd", "/c", "cls").inheritIO().start().waitFor(); - } catch (IOException | InterruptedException e) { - throw new CommandException("Can't clear screen...", e); - } - } - } + @Override + public void clearAll() { + if (JTerm.IS_UNIX) { // escape sequences to clear the screen + System.out.print(ANSI_CLS + ANSI_HOME); + System.out.flush(); + } else if (JTerm.IS_WIN) { // Invoke the command line interpreter's own 'clear' command for Windows OS + try { + new ProcessBuilder("cmd", "/c", "cls").inheritIO().start().waitFor(); + } catch (IOException | InterruptedException e) { + throw new CommandException("Can't clear screen...", e); + } + } + } } diff --git a/src/main/java/jterm/io/output/Printer.java b/src/main/java/jterm/io/output/Printer.java index 1d29e79..77a4ccb 100644 --- a/src/main/java/jterm/io/output/Printer.java +++ b/src/main/java/jterm/io/output/Printer.java @@ -1,5 +1,8 @@ package jterm.io.output; +/** + * Interface meant to replace System.out, so that custom printers can be implemented for GUI and headless mode. + */ public interface Printer { void print(TextColor color, String x); diff --git a/src/main/java/jterm/io/terminal/HeadlessTerminal.java b/src/main/java/jterm/io/terminal/HeadlessTerminal.java new file mode 100644 index 0000000..0fb48ac --- /dev/null +++ b/src/main/java/jterm/io/terminal/HeadlessTerminal.java @@ -0,0 +1,241 @@ +package jterm.io.terminal; + +import jterm.JTerm; +import jterm.io.handlers.InputHandler; +import jterm.io.output.TextColor; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; + +/** + * Headless terminal module. Interfaces directly with bash, so it is as if you were running a normal terminal session. + */ +public class HeadlessTerminal { + + // Input handler for this module + public TermInputProcessor inputProcessor; + + // Hard cap on lines in history file. If more are added after this, the oldest ones are deleted + private final static int maxLinesInHistory = 10000; + + private boolean exit; + + public HeadlessTerminal() { + inputProcessor = new TermInputProcessor(this); + + importJTermHistory(); + } + + public void run() { + exit = false; + while (!exit) + inputProcessor.process(InputHandler.getKey()); + + writeCommandsToFile(); + } + + public void parse(String rawCommand) { + final String[] split = rawCommand.split("&&"); + JTerm.out.println(); + + for (String command : split) { + command = command.trim(); + + if (JTerm.executeCommand(command)) + continue; + + /* + * cd command has to be interpreted separately, since once the JVM runs any cd commands do not take effect + * inside the program, nor once it exists. + */ + if (command.startsWith("cd ")) { + changeDir(command); + continue; + } + + if ("exit".equals(command)) { + inputProcessor.setCursorPos(0); + inputProcessor.setCommand(""); + exit = true; + return; + } + + final ProcessBuilder pb; + final Process p; + try { // Assumes unix, Windows would require a separate implementation... + pb = new ProcessBuilder("/bin/bash", "-c", command); + pb.redirectInput(ProcessBuilder.Redirect.PIPE); + pb.redirectOutput(ProcessBuilder.Redirect.INHERIT); + pb.redirectError(ProcessBuilder.Redirect.PIPE); + pb.directory(new File(JTerm.currentDirectory)); // Set working directory for command + p = pb.start(); + p.waitFor(); + + if (p.exitValue() == 0) + JTerm.out.println(TextColor.INFO, readInputStream(p.getInputStream())); + else + JTerm.out.println(TextColor.ERROR, readInputStream(p.getErrorStream())); + p.destroy(); + } catch (IOException | IllegalArgumentException | InterruptedException e) { + System.err.println("Parsing command \"" + command + "\" failed, enter \"help\" for help using JTerm."); + } + } + + JTerm.out.printPrompt(); + } + + /** + * Reads an {@link InputStream} and returns its contents as a {@link String}. + * + * @param inputStream Input stream to read from + * @return Contents of the input stream, as a String + * @throws IOException If the stream is null + */ + private String readInputStream(final InputStream inputStream) throws IOException { + final BufferedReader in = new BufferedReader(new InputStreamReader(inputStream)); + final StringBuilder responseBuffer = new StringBuilder(); + + String line; + while ((line = in.readLine()) != null) + responseBuffer.append(line); + return responseBuffer.toString(); + } + + /** + * Reads JTerm history file and stores all the previous commands in the commandHistory list. + *

+ * If for some reason this fails, it will try to read the ~/.bash_history file + */ + private void importJTermHistory() { + try { + List history = Files.readAllLines(Paths.get(JTerm.USER_HOME_DIR + "/.jterm_history")); + inputProcessor.commandHistory.addAll(history); + inputProcessor.getArrowKeyProcessor().setCommandListPosition(history.size()); + if (history.size() > 0) + return; + } catch (IOException e) { + System.err.println("Error reading JTerm history file"); + importBashHistory(); + return; + } + + importBashHistory(); + } + + /** + * Reads bash history file and stores all command listed in the commandHistory list. + */ + private void importBashHistory() { + System.out.println("You currently have an empty or non-existent history file, importing bash history..."); + try { + List bashHistory = Files.readAllLines(Paths.get(JTerm.USER_HOME_DIR + "/.bash_history")); + inputProcessor.commandHistory.addAll(bashHistory); + inputProcessor.getArrowKeyProcessor().setCommandListPosition(bashHistory.size()); + writeCommandsToFile(); + } catch (IOException e) { + System.err.println(JTerm.USER_HOME_DIR + "/.bash_history"); + System.err.println("Error reading bash history file. Aborting import..."); + } + } + + /** + * Writes the commandHistory list to the ~/.jterm_history file. If this file does not exist, it will + * be created. + */ + private void writeCommandsToFile() { + File historyFile = new File(JTerm.USER_HOME_DIR + "/.jterm_history"); + + try { + historyFile.createNewFile(); + } catch (IOException e) { + System.err.println("Error creating new history file in directory: " + JTerm.USER_HOME_DIR); + return; + } + + List original; + try { + original = Files.readAllLines(Paths.get(historyFile.getAbsolutePath())); + } catch (IOException e) { + System.err.println("Error reading lines from original history file"); + return; + } + + if (historyFile.delete()) { + historyFile = new File(JTerm.USER_HOME_DIR + "/.jterm_history"); + + final PrintWriter pw; + try { + pw = new PrintWriter(historyFile); + } catch (FileNotFoundException e) { + System.err.println("Error creating print writer"); + return; + } + + final int startPos = original.size() + + inputProcessor.commandHistory.size() > maxLinesInHistory ? inputProcessor.commandHistory.size() : 0; + for (int i = startPos; i < original.size(); i++) + pw.println(original.get(i)); + + for (String s : inputProcessor.commandHistory) + pw.println(s); + + pw.close(); + } + } + + /** + * Changes the terminals directory, since the system does not interpret chdir commands. + * Attempts to emulate the "cd" command. + * + * @param command cd command to parse + */ + public void changeDir(String command) { + final String[] chdirSplit = command.split(" "); + + if (chdirSplit.length != 2 || !"cd".equals(chdirSplit[0])) { + JTerm.out.println(TextColor.PROMPT, "Invalid chdir command passed"); + } else { + final String dirChange = chdirSplit[1]; + final String currDir = JTerm.currentDirectory; + final File f; + + // "cd .." + if ("..".equals(dirChange) && !"/".equals(JTerm.currentDirectory)) { + final String[] dirSplit = JTerm.currentDirectory.split("/"); + final StringBuilder newPath = new StringBuilder(); + + for (int i = 0; i < dirSplit.length - 1; i++) + newPath.append(dirSplit[i]).append("/"); + + System.setProperty("user.dir", newPath.toString()); + JTerm.currentDirectory = newPath.toString(); + return; + } + + // "cd /home/username/example/" + if (dirChange.startsWith("/")) + f = Paths.get(dirChange).toFile(); + + // "cd ~/example/" + else if (dirChange.startsWith("~") && dirChange.length() > 1) + f = Paths.get(JTerm.USER_HOME_DIR + dirChange.substring(1)).toFile(); + + // "cd ~" + else if ("~".equals(dirChange)) + f = Paths.get(JTerm.USER_HOME_DIR).toFile(); + + // "cd example/src/morexamples/" + else + f = Paths.get(currDir + dirChange).toFile(); + + if (f.exists() && f.isDirectory()) { + System.setProperty("user.dir", f.getAbsolutePath()); + JTerm.currentDirectory = f.getAbsolutePath() + "/"; + } else { + JTerm.out.println(TextColor.INFO, "Please enter a valid directory to change to"); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/jterm/io/terminal/TermArrowKeyProcessor.java b/src/main/java/jterm/io/terminal/TermArrowKeyProcessor.java new file mode 100644 index 0000000..55af25f --- /dev/null +++ b/src/main/java/jterm/io/terminal/TermArrowKeyProcessor.java @@ -0,0 +1,129 @@ +package jterm.io.terminal; + +import jterm.JTerm; +import jterm.io.handlers.ArrowKeyHandler; +import jterm.io.input.Keys; +import jterm.io.output.TextColor; + +/** + * Processes arrow keys for JTerm headless terminal. + * + * @see HeadlessTerminal + * @see TermInputProcessor + */ +public class TermArrowKeyProcessor extends ArrowKeyHandler { + + // Input processor owning this class + private TermInputProcessor inputProcessor; + + // Position on prevCommands list (used to iterate through it) + private int commandListPosition = 0; + + // Stores current TermInputProcessor.command when iterating through prevCommands + private String currCommand = ""; + + TermArrowKeyProcessor(final TermInputProcessor inputProcessor) { + this.inputProcessor = inputProcessor; + setLArrowBehaviour(); + setRArrowBehaviour(); + setUArrowBehaviour(); + setDArrowBehaviour(); + } + + protected void setCurrCommand(final String currCommand) { + this.currCommand = currCommand; + } + + protected void setCommandListPosition(final int commandListPosition) { + this.commandListPosition = commandListPosition; + } + + private void setLArrowBehaviour() { + leftArrEvent = () -> { + if (inputProcessor.getCursorPos() > 0) { + JTerm.out.print(TextColor.INPUT, "\b"); + inputProcessor.decreaseCursorPos(); + } + }; + + Keys.LEFT.setEvent(leftArrEvent); + } + + private void setRArrowBehaviour() { + rightArrEvent = () -> { + if (inputProcessor.getCursorPos() < inputProcessor.getCommand().length()) { + JTerm.out.clearLine(inputProcessor.getCommand(), inputProcessor.getCursorPos(), true); + JTerm.out.printWithPrompt(TextColor.INPUT, inputProcessor.getCommand()); + inputProcessor.increaseCursorPos(); + inputProcessor.moveToCursorPos(); + } + }; + + Keys.RIGHT.setEvent(rightArrEvent); + } + + private void setUArrowBehaviour() { + upArrEvent = () -> { + prevCommandIterator(Keys.UP); + inputProcessor.setCursorPos(inputProcessor.getCommand().length()); + }; + + Keys.UP.setEvent(upArrEvent); + } + + private void setDArrowBehaviour() { + downArrEvent = () -> { + prevCommandIterator(Keys.DOWN); + inputProcessor.setCursorPos(inputProcessor.getCommand().length()); + }; + + Keys.DOWN.setEvent(downArrEvent); + } + + /** + * Iterates through the prevCommands list. Emulates Unix terminal behaviour when using + * vertical arrow keys in the terminal. + * + * @param ak Arrow key to process + */ + private void prevCommandIterator(final Keys ak) { + if (inputProcessor.commandHistory.size() == 0) + return; + + int cmdHistorySize = inputProcessor.commandHistory.size() - 1; + + if (commandListPosition == inputProcessor.commandHistory.size() && lastArrowPress == Keys.NONE) + currCommand = inputProcessor.getCommand(); + + if (ak == Keys.UP && commandListPosition > 0) { + // Move through the list towards first typed command + + lastArrowPress = ak; + JTerm.out.clearLine(inputProcessor.getCommand(), inputProcessor.getCursorPos(), true); + + if (commandListPosition > inputProcessor.commandHistory.size()) + commandListPosition = inputProcessor.commandHistory.size(); + + JTerm.out.printWithPrompt(TextColor.INPUT, inputProcessor.commandHistory.get(--commandListPosition)); + inputProcessor.setCommand(inputProcessor.commandHistory.get(commandListPosition)); + + } else if (ak == Keys.DOWN) { + lastArrowPress = ak; + + if (commandListPosition < cmdHistorySize) { + // Move through list towards last typed element + JTerm.out.clearLine(inputProcessor.getCommand(), inputProcessor.getCursorPos(), true); + + JTerm.out.printWithPrompt(TextColor.INPUT, inputProcessor.commandHistory.get(++commandListPosition)); + inputProcessor.setCommand(inputProcessor.commandHistory.get(commandListPosition)); + } else if (!inputProcessor.getCommand().equals(currCommand)) { + // Print command that was stored before iteration through list began + JTerm.out.clearLine(inputProcessor.getCommand(), inputProcessor.getCursorPos(), true); + commandListPosition++; + + JTerm.out.printWithPrompt(TextColor.INPUT, currCommand); + inputProcessor.setCommand(currCommand); + } + } + } +} diff --git a/src/main/java/jterm/io/terminal/TermInputProcessor.java b/src/main/java/jterm/io/terminal/TermInputProcessor.java new file mode 100644 index 0000000..2db43af --- /dev/null +++ b/src/main/java/jterm/io/terminal/TermInputProcessor.java @@ -0,0 +1,182 @@ +package jterm.io.terminal; + +import jterm.JTerm; +import jterm.io.handlers.ArrowKeyHandler; +import jterm.io.handlers.InputHandler; +import jterm.io.handlers.KeyHandler; +import jterm.io.input.Input; +import jterm.io.input.Keys; +import jterm.io.output.TextColor; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedList; + +/** + * Input processor for JTerm headless terminal. + * + * @see HeadlessTerminal + * @see TermKeyProcessor + * @see TermArrowKeyProcessor + */ +public class TermInputProcessor extends InputHandler { + + private HeadlessTerminal headlessTerminal; + + // Stores all entered commands + public ArrayList commandHistory = new ArrayList<>(); + + private String command = ""; + + private int cursorPos = 0; + + public TermInputProcessor(final HeadlessTerminal headlessTerminal) { + super(); + this.headlessTerminal = headlessTerminal; + + keyHandler = new TermKeyProcessor(this); + arrowKeyHandler = new TermArrowKeyProcessor(this); + + KeyHandler.initKeysMap(); + } + + public TermKeyProcessor getKeyProcessor() { + return (TermKeyProcessor) keyHandler; + } + + public TermArrowKeyProcessor getArrowKeyProcessor() { + return (TermArrowKeyProcessor) arrowKeyHandler; + } + + public String getCommand() { + return command; + } + + public void setCommand(String command) { + this.command = command; + } + + public void increaseCursorPos() { + cursorPos++; + } + + public void decreaseCursorPos() { + cursorPos--; + } + + public int getCursorPos() { + return cursorPos; + } + + protected void setCursorPos(int cursorPos) { + this.cursorPos = cursorPos; + } + + /** + * Calls appropriate method for handling input read from the input class. + */ + @Override + public void process(final Keys key) { + if (JTerm.IS_WIN) { + arrowKeyHandler.process(ArrowKeyHandler.arrowKeyCheckWindows(key.getValue())); + keyHandler.process(key); + } else if (JTerm.IS_UNIX) { + int c1, c2; + try { + c1 = Input.read(false); + c2 = Input.read(false); + + if (c1 == -2 && c2 == -2) + keyHandler.process(key); + else + arrowKeyHandler.process(ArrowKeyHandler.arrowKeyCheckUnix(key.getValue(), c1, c2)); + } catch (IOException e) { + System.err.println("Error reading arrow key press"); + } + } + } + + /** + * Sends command to terminal class for parsing, source is the newlineEvent in the key processor + */ + protected void parse() { + headlessTerminal.parse(command); + } + + /** + * Moves the cursor from the end of the command to where it should be (if the user is using arrow keys) + * Usually only used after modifying 'command' + */ + public void moveToCursorPos() { + for (int i = command.length(); i > cursorPos; i--) + JTerm.out.print(TextColor.INPUT, "\b"); + } + + /** + * Splits a command into 3 parts for the autocomplete function to operate properly. + *

+ * Elements 0 and 2 are the non-relevant part of the command to the autocomplete function + * and are used when stitching the autocompleted command back together. + *

+ * Element 1 is the portion of the command that needs completing, and the one on which + * the autocomplete class will operate on. + * + * @param command Command to split + * @return Returns disassembled string, with non relevant info in elements 0 and 2, and the string to autocomplete + * in element 1 + */ + protected static String[] disassembleCommand(final String command, final Integer cursorPos) { + + if (!command.contains("&&")) + return new String[]{"", command, ""}; + + LinkedList ampPos = new LinkedList<>(); + for (int i = 0; i < command.length() - 1; i++) { + if (command.substring(i, i + 2).equals("&&")) { + ampPos.add(i); + if (cursorPos - i == 1) + return new String[]{"", command, ""}; + } + } + + String[] splitCommand = new String[3]; + + final String rightSideSplit = command.substring(cursorPos); + if (ampPos.size() > 1) { + // Deals with commands that have more than one && + for (int i = 0; i < ampPos.size(); i++) { + if (ampPos.get(i) > cursorPos) { + splitCommand[0] = command.substring(0, ampPos.get(i - 1) + 2) + " "; + splitCommand[1] = command.substring(ampPos.get(i - 1) + 2, cursorPos); + splitCommand[2] = " " + rightSideSplit; + } else if (i + 1 == ampPos.size()) { + splitCommand[0] = command.substring(0, ampPos.get(i) + 2) + " "; + splitCommand[1] = command.substring(ampPos.get(i) + 2, cursorPos); + splitCommand[2] = " " + rightSideSplit; + } + } + } else { + // Deals with commands that have exactly one && + if (cursorPos > ampPos.get(0)) { + splitCommand[0] = command.substring(0, ampPos.get(0) + 2) + " "; + splitCommand[1] = command.substring(ampPos.get(0) + 2, cursorPos); + splitCommand[2] = rightSideSplit; + } else if (cursorPos < ampPos.get(0)) { + splitCommand[0] = ""; + splitCommand[1] = command.substring(0, cursorPos); + splitCommand[2] = rightSideSplit; + } else { + String[] split = command.split("&&"); + splitCommand[0] = split[0]; + splitCommand[1] = ""; + splitCommand[2] = "&&" + split[1]; + } + } + + // Remove spaces so that autocomplete can work properly + splitCommand[1] = splitCommand[1].trim(); + + return splitCommand; + } +} + diff --git a/src/main/java/jterm/io/terminal/TermKeyProcessor.java b/src/main/java/jterm/io/terminal/TermKeyProcessor.java new file mode 100644 index 0000000..1a846fe --- /dev/null +++ b/src/main/java/jterm/io/terminal/TermKeyProcessor.java @@ -0,0 +1,110 @@ +package jterm.io.terminal; + +import jterm.JTerm; +import jterm.io.handlers.KeyHandler; +import jterm.io.input.Keys; +import jterm.io.output.TextColor; + +/** + * Processes key presses (except arrow keys) for JTerm headless terminal. + * + * @see HeadlessTerminal + */ +public class TermKeyProcessor extends KeyHandler { + + private TermInputProcessor inputProcessor; + + TermKeyProcessor(final TermInputProcessor inputProcessor) { + this.inputProcessor = inputProcessor; + setUpTabEvents(); + setUpNWLNEvent(); + setUpCharEvents(); + setUpBackspaceEvent(); + setUpCtrlCEvent(); + setupCtrlZEvent(); + } + + private void setUpTabEvents() { + tabEvent = () -> {}; + } + + private void setUpNWLNEvent() { + newLineEvent = () -> { + final String command = inputProcessor.getCommand(); + final boolean empty = "".equals(command.trim()); + + if (!empty) + inputProcessor.commandHistory.add(command); + + inputProcessor.getArrowKeyProcessor().setCommandListPosition(inputProcessor.commandHistory.size()); + inputProcessor.getArrowKeyProcessor().setCurrCommand(""); + inputProcessor.setCursorPos(0); + inputProcessor.parse(); + + inputProcessor.setCommand(""); + }; + + Keys.NWLN.setEvent(newLineEvent); + } + + private void setUpCharEvents() { + charEvent = (char input) -> { + final String command = inputProcessor.getCommand(); + int cursorPos = inputProcessor.getCursorPos(); + + if (JTerm.isHeadless()) + JTerm.out.clearLine(command, cursorPos, true); + + if (inputProcessor.getCursorPos() == command.length()) + inputProcessor.setCommand(command + input); + else + inputProcessor.setCommand(new StringBuilder(command).insert(cursorPos, input).toString()); + + if (JTerm.isHeadless()) + JTerm.out.printWithPrompt(TextColor.INPUT, inputProcessor.getCommand()); + + inputProcessor.increaseCursorPos(); + inputProcessor.moveToCursorPos(); + }; + + Keys.CHAR.setCharEvent(charEvent); + } + + private void setUpBackspaceEvent() { + backspaceEvent = () -> { + if (inputProcessor.getCommand().length() > 0 && inputProcessor.getCursorPos() > 0) { + int charToDelete = inputProcessor.getCursorPos() - 1; + final String command = inputProcessor.getCommand(); + + if (JTerm.isHeadless()) + JTerm.out.clearLine(command, inputProcessor.getCursorPos(), true); + + inputProcessor.setCommand(new StringBuilder(command).deleteCharAt(charToDelete).toString()); + + if (JTerm.isHeadless()) + JTerm.out.printWithPrompt(TextColor.INPUT, inputProcessor.getCommand()); + + inputProcessor.decreaseCursorPos(); + inputProcessor.moveToCursorPos(); + } + }; + + Keys.BACKSPACE.setEvent(backspaceEvent); + } + + private void setUpCtrlCEvent() { + ctrlCEvent = () -> { + System.exit(130); + }; + + Keys.CTRL_C.setEvent(ctrlCEvent); + } + + private void setupCtrlZEvent() { + ctrlZEvent = () -> { + System.exit(131); + }; + + Keys.CTRL_Z.setEvent(ctrlZEvent); + } +} diff --git a/src/test/java/jterm/io/InputHandlerTest.java b/src/test/java/jterm/io/InputHandlerTest.java deleted file mode 100644 index 433eb41..0000000 --- a/src/test/java/jterm/io/InputHandlerTest.java +++ /dev/null @@ -1,78 +0,0 @@ -package jterm.io; - -import jterm.io.input.InputHandler; -import org.junit.jupiter.api.Test; - -import java.lang.reflect.Method; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; - -class InputHandlerTest { - @Test - void testDissassembleCommand1e() { - //Testing single command, cursor at the very end - InputHandler.setCursorPos(5); - // cursor: v - assertArrayEquals(new String[]{"", "cd JT", ""}, disassembleCommand("cd JT")); - } - - @Test - void testDissassembleCommand1t() { - //Testing single command, cursor in the autocomplete target - InputHandler.setCursorPos(4); - // cursor: v - assertArrayEquals(new String[]{"", "cd J", "T"}, disassembleCommand("cd JT")); - } - - @Test - void testDissassembleCommand2e() { - //Testing two commands, cursor at the very end - InputHandler.setCursorPos(14); - // cursor: v - assertArrayEquals(new String[]{"cd .. && ", "cd JT", ""}, disassembleCommand("cd .. && cd JT")); - } - - @Test - void testDissassembleCommand2t() { - //Testing two commands, cursor in the autocomplete target - InputHandler.setCursorPos(13); - // cursor: v - assertArrayEquals(new String[]{"cd .. && ", "cd J", "T"}, disassembleCommand("cd .. && cd JT")); - } - - @Test - void testDissassembleCommand3e() { - //Testing three commands, cursor at the very end - InputHandler.setCursorPos(26); - // cursor: v - assertArrayEquals(new String[]{"cd .. && cd JTerm && ", "cd ou", ""}, disassembleCommand("cd .. && cd JTerm && cd ou")); - } - - @Test - void testDissassembleCommand3t() { - //Testing three commands, cursor in the autocomplete target - InputHandler.setCursorPos(25); - // cursor: v - assertArrayEquals(new String[]{"cd .. && cd JTerm && ", "cd o", "u"}, disassembleCommand("cd .. && cd JTerm && cd ou")); - } - - @Test - void testDissassembleCommand3m() { - //Testing three commands, cursor in the middle command and in the autocomplete target - InputHandler.setCursorPos(13); - // cursor: v - assertArrayEquals(new String[]{"cd .. && ", "cd J", "T && cd out"}, disassembleCommand("cd .. && cd JT && cd out")); - } - - - private String[] disassembleCommand(String command) { - try { - Method method = InputHandler.class.getDeclaredMethod("disassembleCommand", String.class); - method.setAccessible(true); - return (String[]) method.invoke(InputHandler.class, command); - } catch (Exception e) { - System.out.println("Exception thrown"); - return null; - } - } -} diff --git a/src/test/java/jterm/io/TermInputProcessorTest.java b/src/test/java/jterm/io/TermInputProcessorTest.java new file mode 100644 index 0000000..5811946 --- /dev/null +++ b/src/test/java/jterm/io/TermInputProcessorTest.java @@ -0,0 +1,23 @@ +package jterm.io; + +import jterm.io.terminal.TermInputProcessor; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +class TermInputProcessorTest { + @Test + void testDissassembleCommand() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + assertArrayEquals(new String[]{"", "test", ""}, disassembleCommand("test")); + } + + + private String[] disassembleCommand(final String command) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Method method = TermInputProcessor.class.getDeclaredMethod("disassembleCommand", String.class, Integer.class); + method.setAccessible(true); + return (String[]) method.invoke(TermInputProcessor.class, command, command.length()); + } +} diff --git a/src/test/java/jterm/util/GuiUtilTest.java b/src/test/java/jterm/util/GuiUtilTest.java index 86a7bc5..89e7abc 100644 --- a/src/test/java/jterm/util/GuiUtilTest.java +++ b/src/test/java/jterm/util/GuiUtilTest.java @@ -18,7 +18,7 @@ public class GuiUtilTest { @BeforeAll static void init() { - JTerm.setheadless(false); + JTerm.setHeadless(false); } @BeforeEach diff --git a/src/test/java/jterm/util/HeadlessUtilTest.java b/src/test/java/jterm/util/HeadlessUtilTest.java index e5fec7b..2b8f196 100644 --- a/src/test/java/jterm/util/HeadlessUtilTest.java +++ b/src/test/java/jterm/util/HeadlessUtilTest.java @@ -15,9 +15,9 @@ public class HeadlessUtilTest { @BeforeAll static void init() { - JTerm.setheadless(true); + JTerm.setHeadless(true); JTerm.setPrompt(">> "); - JTerm.setCurrentDirectory("/dir"); + JTerm.currentDirectory = "/dir"; } @BeforeEach diff --git a/src/test/java/jterm/util/UtilTest.java b/src/test/java/jterm/util/UtilTest.java index ab46a7d..05dd8db 100644 --- a/src/test/java/jterm/util/UtilTest.java +++ b/src/test/java/jterm/util/UtilTest.java @@ -1,8 +1,15 @@ package jterm.util; import jterm.JTerm; +import jterm.gui.Terminal; +import jterm.io.output.CollectorPrinter; +import jterm.io.output.GuiPrinter; +import jterm.io.output.HeadlessPrinter; +import jterm.io.output.TextColor; import org.junit.jupiter.api.Test; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; import java.util.Arrays; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -13,6 +20,63 @@ void getRunTime() { assertEquals(Util.getRunTime(172799999), "1 days, 23 hours, 59 minutes, 59 seconds, 999 millis"); } + @Test + void clearLineHeadless() throws BadLocationException { + JTerm.setHeadless(true); + CollectorPrinter collector = new CollectorPrinter(new HeadlessPrinter()); + JTerm.out = collector; + JTerm.setPrompt("/dir>> "); + + JTerm.out.printPrompt(); + JTerm.out.clearLine("", 0, true); + assertEquals("/dir>> ", collector.export()); + + JTerm.out.printWithPrompt(TextColor.INPUT, "stuff"); + JTerm.out.clearLine("stuff", 5, true); + assertEquals("/dir>> stuff", collector.export()); + + JTerm.out.printPrompt(); + JTerm.out.clearLine("", 0, false); + assertEquals("/dir>> ", collector.export()); + + JTerm.out.printWithPrompt(TextColor.INPUT, "stuff"); + JTerm.out.clearLine("stuff", 5, false); + assertEquals("/dir>> stuff", collector.export()); + } + + @Test + void clearLineGUI() throws BadLocationException { + JTerm.setHeadless(false); + Terminal terminal = new Terminal(); + terminal.setTitle("JTerm"); + terminal.setSize(720, 480); + terminal.setVisible(true); + JTerm.setTerminal(terminal); + JTerm.out = new GuiPrinter(terminal.getTextPane()); + Document doc = terminal.getTextPane().getDocument(); + JTerm.currentDirectory = "/dir"; + + JTerm.out.clearAll(); + JTerm.out.printPrompt(); + JTerm.out.clearLine("", 0, true); + assertEquals("", doc.getText(0, doc.getLength())); + + JTerm.out.clearAll(); + JTerm.out.printPrompt(); + JTerm.out.clearLine("", 0, false); + assertEquals("/dir>> ", doc.getText(0, doc.getLength())); + + JTerm.out.clearAll(); + JTerm.out.printWithPrompt(TextColor.INPUT, "stuff"); + JTerm.out.clearLine("stuff", 0, true); + assertEquals("", doc.getText(0, doc.getLength())); + + JTerm.out.clearAll(); + JTerm.out.printWithPrompt(TextColor.INPUT, "stuff"); + JTerm.out.clearLine("stuff", 0, false); + assertEquals("/dir>> ", doc.getText(0, doc.getLength())); + } + @Test void getAsArray() { assertEquals(Util.getAsArray("This function is just splitting on spaces"), @@ -31,4 +95,36 @@ void getFullPath() { JTerm.currentDirectory = "/blah"; assertEquals("/blah/file.txt", Util.getFullPath("file.txt")); } + + private void compareTimes(Runnable r1, Runnable r2) { + averageTimeOfExecution(r1); + long r1time = averageTimeOfExecution(r1); + randomBranches(); + averageTimeOfExecution(r2); + long r2time = averageTimeOfExecution(r2); + System.out.println(String.format("R1: %d, R2: %d", r1time, r2time)); + } + + private void randomBranches() { + int rand = 0; + for (int i = 0; i < 10000; i++) { + if (i + (Math.random() * 100) < i + (Math.random() * 100)) { + rand++; + } else { + rand--; + } + } + } + + private long averageTimeOfExecution(Runnable r) { + long before, after; + long total = 0; + for (int i = 0; i < 1000; i++) { + before = System.nanoTime(); + r.run(); + after = System.nanoTime(); + total += after - before; + } + return total / 1000; + } } \ No newline at end of file