From ec9d8d7bf8e2bdc93b4ee71683f2c7d83a0d4fcb Mon Sep 17 00:00:00 2001 From: Adam Murdoch Date: Sat, 4 Aug 2012 16:18:30 +1000 Subject: [PATCH] Added support for Terminal.up(), down(), left(), right() for the terminal. --- readme.md | 81 ++-- src/main/cpp/generic.cpp | 42 +-- src/main/cpp/posix.cpp | 355 ++++++++++-------- .../net/rubygrapefruit/platform/Main.java | 107 ++++-- .../rubygrapefruit/platform/PosixFile.java | 12 + .../net/rubygrapefruit/platform/Process.java | 23 +- .../net/rubygrapefruit/platform/Terminal.java | 53 ++- .../platform/TerminalAccess.java | 19 +- .../platform/internal/TerminfoTerminal.java | 224 ++++++----- .../platform/internal/WindowsTerminal.java | 180 +++++---- .../internal/jni/NativeLibraryFunctions.java | 14 +- .../internal/jni/TerminfoFunctions.java | 43 ++- 12 files changed, 686 insertions(+), 467 deletions(-) diff --git a/readme.md b/readme.md index 3599e81..a50f0d4 100755 --- a/readme.md +++ b/readme.md @@ -1,33 +1,48 @@ - -Provides Java bindings for various native APIs. - -* Get and set UNIX file mode. -* Get PID of current process. -* Determine if stdout/stderr are attached to a terminal. -* Query the terminal size. -* Switch between bold and normal mode on the terminal. -* Change foreground color on the terminal. - -Currently ported to OS X, Linux and Windows. Tested on: - -* OS X 10.7.4 -* Ubunutu 12.04 (amd64) -* Windows 7 (amd64) - -## Building - -### Ubuntu - -You need to install the `libncurses5-dev` package to pick up the ncurses header files. Also worth installing the `ncurses-doc` package too. - -## TODO - -* Fix TERM=dumb on linux -* Split out separate native library for terminal handling. -* String names for errno values. -* Split into multiple projects. -* Handle multiple architectures. -* IBM JVM. -* Convert to c. -* Thread safety. -* Windows: flush System.out or System.err on attribute change. + + +Provides Java bindings for various native APIs. + +# Available bindings + +## Generic + +* Get and set UNIX file mode. +* Get PID of current process. + +## Terminal and console + +These bindings work for both the UNIX terminal and Windows console: + +* Determine if stdout/stderr are attached to a terminal. +* Query the terminal size. +* Switch between bold and normal mode on the terminal. +* Change foreground color on the terminal. +* Move terminal cursor up, down, left, right. + +Currently ported to OS X, Linux and Windows. Tested on: + +* OS X 10.7.4 +* Ubunutu 12.04 (amd64) +* Windows 7 (amd64) + +# Building + +## Ubuntu + +You need to install the `libncurses5-dev` package to pick up the ncurses header files. Also worth installing the `ncurses-doc` package too. + +## Windows + +You need to install Visual studio, and build from a Visual studio command prompt. + +# TODO + +* Fix TERM=dumb on linux +* Split out separate native library for terminal handling. +* String names for errno values. +* Split into multiple projects. +* Handle multiple architectures. +* IBM JVM. +* Convert to c. +* Thread safety. +* Windows: flush System.out or System.err on attribute change. diff --git a/src/main/cpp/generic.cpp b/src/main/cpp/generic.cpp index 943720a..25fe59d 100755 --- a/src/main/cpp/generic.cpp +++ b/src/main/cpp/generic.cpp @@ -1,21 +1,21 @@ -/* - * Generic functions - */ -#include "native.h" -#include "generic.h" - -void mark_failed_with_message(JNIEnv *env, const char* message, jobject result) { - mark_failed_with_code(env, message, 0, result); -} - -void mark_failed_with_code(JNIEnv *env, const char* message, int error_code, jobject result) { - jclass destClass = env->GetObjectClass(result); - jmethodID method = env->GetMethodID(destClass, "failed", "(Ljava/lang/String;I)V"); - jstring message_str = env->NewStringUTF(message); - env->CallVoidMethod(result, method, message_str, error_code); -} - -JNIEXPORT jint JNICALL -Java_net_rubygrapefruit_platform_internal_jni_NativeLibraryFunctions_getVersion(JNIEnv *env, jclass target) { - return 2; -} +/* + * Generic functions + */ +#include "native.h" +#include "generic.h" + +void mark_failed_with_message(JNIEnv *env, const char* message, jobject result) { + mark_failed_with_code(env, message, 0, result); +} + +void mark_failed_with_code(JNIEnv *env, const char* message, int error_code, jobject result) { + jclass destClass = env->GetObjectClass(result); + jmethodID method = env->GetMethodID(destClass, "failed", "(Ljava/lang/String;I)V"); + jstring message_str = env->NewStringUTF(message); + env->CallVoidMethod(result, method, message_str, error_code); +} + +JNIEXPORT jint JNICALL +Java_net_rubygrapefruit_platform_internal_jni_NativeLibraryFunctions_getVersion(JNIEnv *env, jclass target) { + return 3; +} diff --git a/src/main/cpp/posix.cpp b/src/main/cpp/posix.cpp index 743783a..acf3241 100755 --- a/src/main/cpp/posix.cpp +++ b/src/main/cpp/posix.cpp @@ -1,161 +1,194 @@ -#ifndef WIN32 - -#include "native.h" -#include "generic.h" -#include -#include -#include -#include -#include -#include -#include -#include - -/* - * Marks the given result as failed, using the current value of errno - */ -void mark_failed_with_errno(JNIEnv *env, const char* message, jobject result) { - mark_failed_with_code(env, message, errno, result); -} - -/* - * File functions - */ - -JNIEXPORT void JNICALL -Java_net_rubygrapefruit_platform_internal_jni_PosixFileFunctions_chmod(JNIEnv *env, jclass target, jstring path, jint mode, jobject result) { - const char* pathUtf8 = env->GetStringUTFChars(path, NULL); - int retval = chmod(pathUtf8, mode); - env->ReleaseStringUTFChars(path, pathUtf8); - if (retval != 0) { - mark_failed_with_errno(env, "could not chmod file", result); - } -} - -JNIEXPORT void JNICALL -Java_net_rubygrapefruit_platform_internal_jni_PosixFileFunctions_stat(JNIEnv *env, jclass target, jstring path, jobject dest, jobject result) { - struct stat fileInfo; - const char* pathUtf8 = env->GetStringUTFChars(path, NULL); - int retval = stat(pathUtf8, &fileInfo); - env->ReleaseStringUTFChars(path, pathUtf8); - if (retval != 0) { - mark_failed_with_errno(env, "could not stat file", result); - return; - } - jclass destClass = env->GetObjectClass(dest); - jfieldID modeField = env->GetFieldID(destClass, "mode", "I"); - env->SetIntField(dest, modeField, 0777 & fileInfo.st_mode); -} - -/* - * Process functions - */ - -JNIEXPORT jint JNICALL -Java_net_rubygrapefruit_platform_internal_jni_PosixProcessFunctions_getPid(JNIEnv *env, jclass target) { - return getpid(); -} - -/* - * Terminal functions - */ - -JNIEXPORT jboolean JNICALL -Java_net_rubygrapefruit_platform_internal_jni_PosixTerminalFunctions_isatty(JNIEnv *env, jclass target, jint output) { - switch (output) { - case 0: - case 1: - return isatty(output+1) ? JNI_TRUE : JNI_FALSE; - default: - return JNI_FALSE; - } -} - -JNIEXPORT void JNICALL -Java_net_rubygrapefruit_platform_internal_jni_PosixTerminalFunctions_getTerminalSize(JNIEnv *env, jclass target, jint output, jobject dimension, jobject result) { - struct winsize screen_size; - int retval = ioctl(output+1, TIOCGWINSZ, &screen_size); - if (retval != 0) { - mark_failed_with_errno(env, "could not fetch terminal size", result); - return; - } - jclass dimensionClass = env->GetObjectClass(dimension); - jfieldID widthField = env->GetFieldID(dimensionClass, "cols", "I"); - env->SetIntField(dimension, widthField, screen_size.ws_col); - jfieldID heightField = env->GetFieldID(dimensionClass, "rows", "I"); - env->SetIntField(dimension, heightField, screen_size.ws_row); -} - -/* - * Terminfo functions - */ - -int current_terminal = -1; - -int write_to_terminal(int ch) { - write(current_terminal, &ch, 1); -} - -void write_capability(JNIEnv *env, const char* capability, jobject result) { - char* cap = tgetstr((char*)capability, NULL); - if (cap == NULL) { - mark_failed_with_message(env, "unknown terminal capability", result); - return; - } - if (tputs(cap, 1, write_to_terminal) == ERR) { - mark_failed_with_message(env, "could not write to terminal", result); - return; - } -} - -JNIEXPORT void JNICALL -Java_net_rubygrapefruit_platform_internal_jni_TerminfoFunctions_initTerminal(JNIEnv *env, jclass target, jint output, jobject result) { - if (!isatty(output+1)) { - mark_failed_with_message(env, "not a terminal", result); - return; - } - char* termType = getenv("TERM"); - if (termType == NULL) { - mark_failed_with_message(env, "$TERM not set", result); - return; - } - int retval = tgetent(NULL, termType); - if (retval != 1) { - mark_failed_with_message(env, "could not get termcap entry", result); - return; - } - current_terminal = output + 1; - write_capability(env, "me", result); -} - -JNIEXPORT void JNICALL -Java_net_rubygrapefruit_platform_internal_jni_TerminfoFunctions_bold(JNIEnv *env, jclass target, jobject result) { - write_capability(env, "md", result); -} - -JNIEXPORT void JNICALL -Java_net_rubygrapefruit_platform_internal_jni_TerminfoFunctions_reset(JNIEnv *env, jclass target, jobject result) { - write_capability(env, "me", result); -} - -JNIEXPORT void JNICALL -Java_net_rubygrapefruit_platform_internal_jni_TerminfoFunctions_foreground(JNIEnv *env, jclass target, jint color, jobject result) { - char* capability = tgetstr((char*)"AF", NULL); - if (capability == NULL) { - mark_failed_with_message(env, "unknown terminal capability", result); - return; - } - - capability = tparm(capability, color, 0, 0, 0, 0, 0, 0, 0, 0); - if (capability == NULL) { - mark_failed_with_message(env, "could not format terminal capability string", result); - return; - } - - if (tputs(capability, 1, write_to_terminal) == ERR) { - mark_failed_with_message(env, "could not write to terminal", result); - return; - } -} - -#endif +#ifndef WIN32 + +#include "native.h" +#include "generic.h" +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * Marks the given result as failed, using the current value of errno + */ +void mark_failed_with_errno(JNIEnv *env, const char* message, jobject result) { + mark_failed_with_code(env, message, errno, result); +} + +/* + * File functions + */ + +JNIEXPORT void JNICALL +Java_net_rubygrapefruit_platform_internal_jni_PosixFileFunctions_chmod(JNIEnv *env, jclass target, jstring path, jint mode, jobject result) { + const char* pathUtf8 = env->GetStringUTFChars(path, NULL); + int retval = chmod(pathUtf8, mode); + env->ReleaseStringUTFChars(path, pathUtf8); + if (retval != 0) { + mark_failed_with_errno(env, "could not chmod file", result); + } +} + +JNIEXPORT void JNICALL +Java_net_rubygrapefruit_platform_internal_jni_PosixFileFunctions_stat(JNIEnv *env, jclass target, jstring path, jobject dest, jobject result) { + struct stat fileInfo; + const char* pathUtf8 = env->GetStringUTFChars(path, NULL); + int retval = stat(pathUtf8, &fileInfo); + env->ReleaseStringUTFChars(path, pathUtf8); + if (retval != 0) { + mark_failed_with_errno(env, "could not stat file", result); + return; + } + jclass destClass = env->GetObjectClass(dest); + jfieldID modeField = env->GetFieldID(destClass, "mode", "I"); + env->SetIntField(dest, modeField, 0777 & fileInfo.st_mode); +} + +/* + * Process functions + */ + +JNIEXPORT jint JNICALL +Java_net_rubygrapefruit_platform_internal_jni_PosixProcessFunctions_getPid(JNIEnv *env, jclass target) { + return getpid(); +} + +/* + * Terminal functions + */ + +JNIEXPORT jboolean JNICALL +Java_net_rubygrapefruit_platform_internal_jni_PosixTerminalFunctions_isatty(JNIEnv *env, jclass target, jint output) { + switch (output) { + case 0: + case 1: + return isatty(output+1) ? JNI_TRUE : JNI_FALSE; + default: + return JNI_FALSE; + } +} + +JNIEXPORT void JNICALL +Java_net_rubygrapefruit_platform_internal_jni_PosixTerminalFunctions_getTerminalSize(JNIEnv *env, jclass target, jint output, jobject dimension, jobject result) { + struct winsize screen_size; + int retval = ioctl(output+1, TIOCGWINSZ, &screen_size); + if (retval != 0) { + mark_failed_with_errno(env, "could not fetch terminal size", result); + return; + } + jclass dimensionClass = env->GetObjectClass(dimension); + jfieldID widthField = env->GetFieldID(dimensionClass, "cols", "I"); + env->SetIntField(dimension, widthField, screen_size.ws_col); + jfieldID heightField = env->GetFieldID(dimensionClass, "rows", "I"); + env->SetIntField(dimension, heightField, screen_size.ws_row); +} + +/* + * Terminfo functions + */ + +int current_terminal = -1; + +int write_to_terminal(int ch) { + write(current_terminal, &ch, 1); +} + +void write_capability(JNIEnv *env, const char* capability, jobject result) { + char* cap = tgetstr((char*)capability, NULL); + if (cap == NULL) { + mark_failed_with_message(env, "unknown terminal capability", result); + return; + } + if (tputs(cap, 1, write_to_terminal) == ERR) { + mark_failed_with_message(env, "could not write to terminal", result); + return; + } +} + +void write_param_capability(JNIEnv *env, const char* capability, int count, jobject result) { + char* cap = tgetstr((char*)capability, NULL); + if (cap == NULL) { + mark_failed_with_message(env, "unknown terminal capability", result); + return; + } + + cap = tparm(cap, count, 0, 0, 0, 0, 0, 0, 0, 0); + if (cap == NULL) { + mark_failed_with_message(env, "could not format terminal capability string", result); + return; + } + + if (tputs(cap, 1, write_to_terminal) == ERR) { + mark_failed_with_message(env, "could not write to terminal", result); + return; + } +} + +JNIEXPORT void JNICALL +Java_net_rubygrapefruit_platform_internal_jni_TerminfoFunctions_initTerminal(JNIEnv *env, jclass target, jint output, jobject result) { + if (!isatty(output+1)) { + mark_failed_with_message(env, "not a terminal", result); + return; + } + char* termType = getenv("TERM"); + if (termType == NULL) { + mark_failed_with_message(env, "$TERM not set", result); + return; + } + int retval = tgetent(NULL, termType); + if (retval != 1) { + mark_failed_with_message(env, "could not get termcap entry", result); + return; + } + current_terminal = output + 1; + write_capability(env, "me", result); +} + +JNIEXPORT void JNICALL +Java_net_rubygrapefruit_platform_internal_jni_TerminfoFunctions_bold(JNIEnv *env, jclass target, jobject result) { + write_capability(env, "md", result); +} + +JNIEXPORT void JNICALL +Java_net_rubygrapefruit_platform_internal_jni_TerminfoFunctions_reset(JNIEnv *env, jclass target, jobject result) { + write_capability(env, "me", result); +} + +JNIEXPORT void JNICALL +Java_net_rubygrapefruit_platform_internal_jni_TerminfoFunctions_foreground(JNIEnv *env, jclass target, jint color, jobject result) { + write_param_capability(env, "AF", color, result); +} + +JNIEXPORT void JNICALL +Java_net_rubygrapefruit_platform_internal_jni_TerminfoFunctions_up(JNIEnv *env, jclass target, jint count, jobject result) { + for (jint i = 0; i < count; i++) { + write_capability(env, "up", result); + } +} + +JNIEXPORT void JNICALL +Java_net_rubygrapefruit_platform_internal_jni_TerminfoFunctions_down(JNIEnv *env, jclass target, jint count, jobject result) { + for (jint i = 0; i < count; i++) { + write_capability(env, "do", result); + } +} + +JNIEXPORT void JNICALL +Java_net_rubygrapefruit_platform_internal_jni_TerminfoFunctions_left(JNIEnv *env, jclass target, jint count, jobject result) { + for (jint i = 0; i < count; i++) { + write_capability(env, "le", result); + } +} + +JNIEXPORT void JNICALL +Java_net_rubygrapefruit_platform_internal_jni_TerminfoFunctions_right(JNIEnv *env, jclass target, jint count, jobject result) { + for (jint i = 0; i < count; i++) { + write_capability(env, "nd", result); + } +} + + +#endif diff --git a/src/main/java/net/rubygrapefruit/platform/Main.java b/src/main/java/net/rubygrapefruit/platform/Main.java index 6b9ddd5..5105354 100755 --- a/src/main/java/net/rubygrapefruit/platform/Main.java +++ b/src/main/java/net/rubygrapefruit/platform/Main.java @@ -1,41 +1,66 @@ -package net.rubygrapefruit.platform; - -public class Main { - public static void main(String[] args) { - System.out.println(); - System.out.println("* OS: " + System.getProperty("os.name") + ' ' + System.getProperty("os.version") + ' ' + System.getProperty("os.arch")); - - Process process = Native.get(Process.class); - System.out.println("* PID: " + process.getProcessId()); - - TerminalAccess terminalAccess = Native.get(TerminalAccess.class); - boolean stdoutIsTerminal = terminalAccess.isTerminal(TerminalAccess.Output.Stdout); - boolean stderrIsTerminal = terminalAccess.isTerminal(TerminalAccess.Output.Stderr); - System.out.println("* stdout: " + (stdoutIsTerminal ? "terminal" : "not a terminal")); - System.out.println("* stderr: " + (stderrIsTerminal ? "terminal" : "not a terminal")); - if (stdoutIsTerminal) { - Terminal terminal = terminalAccess.getTerminal(TerminalAccess.Output.Stdout); - TerminalSize terminalSize = terminal.getTerminalSize(); - System.out.println("* terminal size: " + terminalSize.getCols() + " cols x " + terminalSize.getRows() + " rows"); - System.out.println(); - System.out.println("TERMINAL OUTPUT"); - System.out.print("[normal] "); - terminal.bold(); - System.out.print("[bold]"); - terminal.normal(); - System.out.println(" [normal]"); - - System.out.println("here are the colors:"); - for (Terminal.Color color : Terminal.Color.values()) { - terminal.foreground(color); - System.out.print(String.format("[%s] ", color.toString().toLowerCase())); - terminal.bold(); - System.out.print(String.format("[%s]", color.toString().toLowerCase())); - terminal.normal(); - System.out.println(); - } - } - - System.out.println(); - } -} +package net.rubygrapefruit.platform; + +public class Main { + public static void main(String[] args) { + System.out.println(); + System.out.println("* OS: " + System.getProperty("os.name") + ' ' + System.getProperty("os.version") + ' ' + System.getProperty("os.arch")); + + Process process = Native.get(Process.class); + System.out.println("* PID: " + process.getProcessId()); + + TerminalAccess terminalAccess = Native.get(TerminalAccess.class); + boolean stdoutIsTerminal = terminalAccess.isTerminal(TerminalAccess.Output.Stdout); + boolean stderrIsTerminal = terminalAccess.isTerminal(TerminalAccess.Output.Stderr); + System.out.println("* stdout: " + (stdoutIsTerminal ? "terminal" : "not a terminal")); + System.out.println("* stderr: " + (stderrIsTerminal ? "terminal" : "not a terminal")); + if (stdoutIsTerminal) { + Terminal terminal = terminalAccess.getTerminal(TerminalAccess.Output.Stdout); + TerminalSize terminalSize = terminal.getTerminalSize(); + System.out.println("* terminal size: " + terminalSize.getCols() + " cols x " + terminalSize.getRows() + " rows"); + System.out.println(); + System.out.println("TERMINAL OUTPUT"); + System.out.print("[normal] "); + terminal.bold(); + System.out.print("[bold]"); + terminal.normal(); + System.out.println(" [normal]"); + + System.out.println("here are the colors:"); + for (Terminal.Color color : Terminal.Color.values()) { + terminal.foreground(color); + System.out.print(String.format("[%s] ", color.toString().toLowerCase())); + terminal.bold(); + System.out.print(String.format("[%s]", color.toString().toLowerCase())); + terminal.normal(); + System.out.println(); + } + + System.out.println(); + terminal.reset(); + + System.out.println("CURSOR MOVEMENT"); + System.out.println(" "); + System.out.println(" "); + System.out.println(" "); + System.out.print("draw "); + + terminal.cursorLeft(10); + terminal.cursorUp(1); + terminal.cursorRight(10); + System.out.print("[4]"); + terminal.cursorUp(1); + terminal.cursorLeft(3); + System.out.print("[2]"); + terminal.cursorLeft(13); + System.out.print("[1]"); + terminal.cursorLeft(3); + terminal.cursorDown(1); + System.out.print("[3]"); + terminal.cursorDown(1); + terminal.cursorRight(10); + System.out.println("done!"); + } + + System.out.println(); + } +} diff --git a/src/main/java/net/rubygrapefruit/platform/PosixFile.java b/src/main/java/net/rubygrapefruit/platform/PosixFile.java index 0fa97ef..88b8876 100644 --- a/src/main/java/net/rubygrapefruit/platform/PosixFile.java +++ b/src/main/java/net/rubygrapefruit/platform/PosixFile.java @@ -4,9 +4,21 @@ import java.io.File; /** * Functions to query and modify a file's POSIX meta-data. + * + * Supported on Linux, OS X */ public interface PosixFile extends NativeIntegration { + /** + * Sets the mode for the given file. + * + * @throws NativeException On failure. + */ void setMode(File path, int perms) throws NativeException; + /** + * Gets the mode for the given file. + * + * @throws NativeException On failure. + */ int getMode(File path) throws NativeException; } diff --git a/src/main/java/net/rubygrapefruit/platform/Process.java b/src/main/java/net/rubygrapefruit/platform/Process.java index aa8dfc7..9caa5b8 100755 --- a/src/main/java/net/rubygrapefruit/platform/Process.java +++ b/src/main/java/net/rubygrapefruit/platform/Process.java @@ -1,8 +1,15 @@ -package net.rubygrapefruit.platform; - -/** - * Functions to query and modify a process' meta-data - */ -public interface Process extends NativeIntegration { - int getProcessId() throws NativeException; -} +package net.rubygrapefruit.platform; + +/** + * Functions to query and modify a process' meta-data + * + * Supported on Linux, OS X, Windows. + */ +public interface Process extends NativeIntegration { + /** + * Returns the process identifier. + * + * @throws NativeException On failure. + */ + int getProcessId() throws NativeException; +} diff --git a/src/main/java/net/rubygrapefruit/platform/Terminal.java b/src/main/java/net/rubygrapefruit/platform/Terminal.java index ff23a21..e2901e0 100644 --- a/src/main/java/net/rubygrapefruit/platform/Terminal.java +++ b/src/main/java/net/rubygrapefruit/platform/Terminal.java @@ -1,5 +1,10 @@ package net.rubygrapefruit.platform; +/** + * Allows the terminal/console to be manipulated. + * + * Supported on Linux, OS X, Windows. + */ public interface Terminal { enum Color { Black, Red, Green, Yellow, Blue, Magenta, Cyan, White @@ -7,26 +12,64 @@ public interface Terminal { /** * Returns the size of the terminal. + * + * @throws NativeException On failure. */ - TerminalSize getTerminalSize(); + TerminalSize getTerminalSize() throws NativeException; /** * Sets the terminal foreground color. + * + * @throws NativeException On failure. */ - Terminal foreground(Color color); + Terminal foreground(Color color) throws NativeException; /** * Switches the terminal to bold mode. + * + * @throws NativeException On failure. */ - Terminal bold(); + Terminal bold() throws NativeException; /** * Switches the terminal to normal mode. + * + * @throws NativeException On failure. */ - Terminal normal(); + Terminal normal() throws NativeException; /** * Switches the terminal to normal mode and restores default colors. + * + * @throws NativeException On failure. */ - Terminal reset(); + Terminal reset() throws NativeException; + + /** + * Moves the cursor the given number of characters to the left. + * + * @throws NativeException On failure. + */ + Terminal cursorLeft(int count) throws NativeException; + + /** + * Moves the cursor the given number of characters to the right. + * + * @throws NativeException On failure. + */ + Terminal cursorRight(int count) throws NativeException; + + /** + * Moves the cursor the given number of characters up. + * + * @throws NativeException On failure. + */ + Terminal cursorUp(int count) throws NativeException; + + /** + * Moves the cursor the given number of characters down. + * + * @throws NativeException On failure. + */ + Terminal cursorDown(int count) throws NativeException; } diff --git a/src/main/java/net/rubygrapefruit/platform/TerminalAccess.java b/src/main/java/net/rubygrapefruit/platform/TerminalAccess.java index cd60f41..94b63a2 100644 --- a/src/main/java/net/rubygrapefruit/platform/TerminalAccess.java +++ b/src/main/java/net/rubygrapefruit/platform/TerminalAccess.java @@ -1,9 +1,24 @@ package net.rubygrapefruit.platform; +/** + * Provides access to the terminal/console. + * + * Supported on Linux, OS X, Windows. + */ public interface TerminalAccess extends NativeIntegration { enum Output {Stdout, Stderr} - boolean isTerminal(Output output); + /** + * Returns true if the given output is attached to a terminal. + * + * @throws NativeException On failure. + */ + boolean isTerminal(Output output) throws NativeException; - Terminal getTerminal(Output output); + /** + * Returns the terminal attached to the given output. + * + * @throws NativeException When the output is not attached to a terminal. + */ + Terminal getTerminal(Output output) throws NativeException; } diff --git a/src/main/java/net/rubygrapefruit/platform/internal/TerminfoTerminal.java b/src/main/java/net/rubygrapefruit/platform/internal/TerminfoTerminal.java index 9a5c092..d4ec154 100755 --- a/src/main/java/net/rubygrapefruit/platform/internal/TerminfoTerminal.java +++ b/src/main/java/net/rubygrapefruit/platform/internal/TerminfoTerminal.java @@ -1,90 +1,134 @@ -package net.rubygrapefruit.platform.internal; - -import net.rubygrapefruit.platform.NativeException; -import net.rubygrapefruit.platform.Terminal; -import net.rubygrapefruit.platform.TerminalAccess; -import net.rubygrapefruit.platform.TerminalSize; -import net.rubygrapefruit.platform.internal.jni.PosixTerminalFunctions; -import net.rubygrapefruit.platform.internal.jni.TerminfoFunctions; - -import java.io.PrintStream; - -public class TerminfoTerminal extends AbstractTerminal { - private final TerminalAccess.Output output; - private final PrintStream stream; - private Color foreground; - - public TerminfoTerminal(TerminalAccess.Output output) { - this.output = output; - stream = output == TerminalAccess.Output.Stdout ? System.out : System.err; - } - - @Override - public String toString() { - return output.toString().toLowerCase(); - } - - @Override - protected void doInit() { - stream.flush(); - FunctionResult result = new FunctionResult(); - TerminfoFunctions.initTerminal(output.ordinal(), result); - if (result.isFailed()) { - throw new NativeException(String.format("Could not open terminal for %s: %s", this, result.getMessage())); - } - } - - @Override - public TerminalSize getTerminalSize() { - MutableTerminalSize terminalSize = new MutableTerminalSize(); - FunctionResult result = new FunctionResult(); - PosixTerminalFunctions.getTerminalSize(output.ordinal(), terminalSize, result); - if (result.isFailed()) { - throw new NativeException(String.format("Could not get terminal size for %s: %s", this, result.getMessage())); - } - return terminalSize; - } - - @Override - public Terminal foreground(Color color) { - stream.flush(); - FunctionResult result = new FunctionResult(); - TerminfoFunctions.foreground(color.ordinal(), result); - if (result.isFailed()) { - throw new NativeException(String.format("Could not switch foreground color for %s: %s", this, result.getMessage())); - } - foreground = color; - return this; - } - - @Override - public Terminal bold() { - stream.flush(); - FunctionResult result = new FunctionResult(); - TerminfoFunctions.bold(result); - if (result.isFailed()) { - throw new NativeException(String.format("Could not switch to bold mode for %s: %s", this, result.getMessage())); - } - return this; - } - - @Override - public Terminal normal() { - reset(); - if (foreground != null) { - foreground(foreground); - } - return this; - } - - @Override - public Terminal reset() { - stream.flush(); - FunctionResult result = new FunctionResult(); - TerminfoFunctions.reset(result); - if (result.isFailed()) { - throw new NativeException(String.format("Could not reset terminal for %s: %s", this, result.getMessage())); - } - return this; - } -} +package net.rubygrapefruit.platform.internal; + +import net.rubygrapefruit.platform.NativeException; +import net.rubygrapefruit.platform.Terminal; +import net.rubygrapefruit.platform.TerminalAccess; +import net.rubygrapefruit.platform.TerminalSize; +import net.rubygrapefruit.platform.internal.jni.PosixTerminalFunctions; +import net.rubygrapefruit.platform.internal.jni.TerminfoFunctions; + +import java.io.PrintStream; + +public class TerminfoTerminal extends AbstractTerminal { + private final TerminalAccess.Output output; + private final PrintStream stream; + private Color foreground; + + public TerminfoTerminal(TerminalAccess.Output output) { + this.output = output; + stream = output == TerminalAccess.Output.Stdout ? System.out : System.err; + } + + @Override + public String toString() { + return output.toString().toLowerCase(); + } + + @Override + protected void doInit() { + stream.flush(); + FunctionResult result = new FunctionResult(); + TerminfoFunctions.initTerminal(output.ordinal(), result); + if (result.isFailed()) { + throw new NativeException(String.format("Could not open terminal for %s: %s", this, result.getMessage())); + } + } + + @Override + public TerminalSize getTerminalSize() { + MutableTerminalSize terminalSize = new MutableTerminalSize(); + FunctionResult result = new FunctionResult(); + PosixTerminalFunctions.getTerminalSize(output.ordinal(), terminalSize, result); + if (result.isFailed()) { + throw new NativeException(String.format("Could not get terminal size for %s: %s", this, result.getMessage())); + } + return terminalSize; + } + + @Override + public Terminal foreground(Color color) { + stream.flush(); + FunctionResult result = new FunctionResult(); + TerminfoFunctions.foreground(color.ordinal(), result); + if (result.isFailed()) { + throw new NativeException(String.format("Could not switch foreground color for %s: %s", this, result.getMessage())); + } + foreground = color; + return this; + } + + @Override + public Terminal bold() { + stream.flush(); + FunctionResult result = new FunctionResult(); + TerminfoFunctions.bold(result); + if (result.isFailed()) { + throw new NativeException(String.format("Could not switch to bold mode for %s: %s", this, result.getMessage())); + } + return this; + } + + @Override + public Terminal normal() { + reset(); + if (foreground != null) { + foreground(foreground); + } + return this; + } + + @Override + public Terminal reset() { + stream.flush(); + FunctionResult result = new FunctionResult(); + TerminfoFunctions.reset(result); + if (result.isFailed()) { + throw new NativeException(String.format("Could not reset terminal for %s: %s", this, result.getMessage())); + } + return this; + } + + @Override + public Terminal cursorDown(int count) { + stream.flush(); + FunctionResult result = new FunctionResult(); + TerminfoFunctions.down(count, result); + if (result.isFailed()) { + throw new NativeException(String.format("Could not move cursor down for %s: %s", this, result.getMessage())); + } + return this; + } + + @Override + public Terminal cursorUp(int count) { + stream.flush(); + FunctionResult result = new FunctionResult(); + TerminfoFunctions.up(count, result); + if (result.isFailed()) { + throw new NativeException(String.format("Could not move cursor up for %s: %s", this, result.getMessage())); + } + return this; + } + + @Override + public Terminal cursorLeft(int count) { + stream.flush(); + FunctionResult result = new FunctionResult(); + TerminfoFunctions.left(count, result); + if (result.isFailed()) { + throw new NativeException(String.format("Could not move cursor left for %s: %s", this, result.getMessage())); + } + return this; + } + + @Override + public Terminal cursorRight(int count) { + stream.flush(); + FunctionResult result = new FunctionResult(); + TerminfoFunctions.right(count, result); + if (result.isFailed()) { + throw new NativeException(String.format("Could not move cursor right for %s: %s", this, result.getMessage())); + } + return this; + } +} diff --git a/src/main/java/net/rubygrapefruit/platform/internal/WindowsTerminal.java b/src/main/java/net/rubygrapefruit/platform/internal/WindowsTerminal.java index 51ec49c..4e01573 100755 --- a/src/main/java/net/rubygrapefruit/platform/internal/WindowsTerminal.java +++ b/src/main/java/net/rubygrapefruit/platform/internal/WindowsTerminal.java @@ -1,80 +1,100 @@ -package net.rubygrapefruit.platform.internal; - -import net.rubygrapefruit.platform.NativeException; -import net.rubygrapefruit.platform.Terminal; -import net.rubygrapefruit.platform.TerminalAccess; -import net.rubygrapefruit.platform.TerminalSize; -import net.rubygrapefruit.platform.internal.jni.WindowsConsoleFunctions; - -public class WindowsTerminal extends AbstractTerminal { - private final TerminalAccess.Output output; - - public WindowsTerminal(TerminalAccess.Output output) { - this.output = output; - } - - @Override - public String toString() { - return output.toString().toLowerCase(); - } - - @Override - protected void doInit() { - FunctionResult result = new FunctionResult(); - WindowsConsoleFunctions.initConsole(output.ordinal(), result); - if (result.isFailed()) { - throw new NativeException(String.format("Could not open console for %s: %s", this, result.getMessage())); - } - } - - @Override - public TerminalSize getTerminalSize() { - FunctionResult result = new FunctionResult(); - MutableTerminalSize size = new MutableTerminalSize(); - WindowsConsoleFunctions.getConsoleSize(output.ordinal(), size, result); - if (result.isFailed()) { - throw new NativeException(String.format("Could not determine console size for %s: %s", this, result.getMessage())); - } - return size; - } - - @Override - public Terminal bold() { - FunctionResult result = new FunctionResult(); - WindowsConsoleFunctions.bold(result); - if (result.isFailed()) { - throw new NativeException(String.format("Could not switch console to bold mode for %s: %s", this, result.getMessage())); - } - return this; - } - - @Override - public Terminal foreground(Color color) { - FunctionResult result = new FunctionResult(); - WindowsConsoleFunctions.foreground(color.ordinal(), result); - if (result.isFailed()) { - throw new NativeException(String.format("Could not change console foreground color for %s: %s", this, result.getMessage())); - } - return this; - } - - @Override - public Terminal normal() { - FunctionResult result = new FunctionResult(); - WindowsConsoleFunctions.normal(result); - if (result.isFailed()) { - throw new NativeException(String.format("Could not switch console to normal mode for %s: %s", this, result.getMessage())); - } - return this; - } - - @Override - public Terminal reset() { - FunctionResult result = new FunctionResult(); - WindowsConsoleFunctions.reset(result); - if (result.isFailed()) { - throw new NativeException(String.format("Could not reset console for %s: %s", this, result.getMessage())); - } - return this; - } -} +package net.rubygrapefruit.platform.internal; + +import net.rubygrapefruit.platform.NativeException; +import net.rubygrapefruit.platform.Terminal; +import net.rubygrapefruit.platform.TerminalAccess; +import net.rubygrapefruit.platform.TerminalSize; +import net.rubygrapefruit.platform.internal.jni.WindowsConsoleFunctions; + +public class WindowsTerminal extends AbstractTerminal { + private final TerminalAccess.Output output; + + public WindowsTerminal(TerminalAccess.Output output) { + this.output = output; + } + + @Override + public String toString() { + return output.toString().toLowerCase(); + } + + @Override + protected void doInit() { + FunctionResult result = new FunctionResult(); + WindowsConsoleFunctions.initConsole(output.ordinal(), result); + if (result.isFailed()) { + throw new NativeException(String.format("Could not open console for %s: %s", this, result.getMessage())); + } + } + + @Override + public TerminalSize getTerminalSize() { + FunctionResult result = new FunctionResult(); + MutableTerminalSize size = new MutableTerminalSize(); + WindowsConsoleFunctions.getConsoleSize(output.ordinal(), size, result); + if (result.isFailed()) { + throw new NativeException(String.format("Could not determine console size for %s: %s", this, result.getMessage())); + } + return size; + } + + @Override + public Terminal bold() { + FunctionResult result = new FunctionResult(); + WindowsConsoleFunctions.bold(result); + if (result.isFailed()) { + throw new NativeException(String.format("Could not switch console to bold mode for %s: %s", this, result.getMessage())); + } + return this; + } + + @Override + public Terminal foreground(Color color) { + FunctionResult result = new FunctionResult(); + WindowsConsoleFunctions.foreground(color.ordinal(), result); + if (result.isFailed()) { + throw new NativeException(String.format("Could not change console foreground color for %s: %s", this, result.getMessage())); + } + return this; + } + + @Override + public Terminal normal() { + FunctionResult result = new FunctionResult(); + WindowsConsoleFunctions.normal(result); + if (result.isFailed()) { + throw new NativeException(String.format("Could not switch console to normal mode for %s: %s", this, result.getMessage())); + } + return this; + } + + @Override + public Terminal reset() { + FunctionResult result = new FunctionResult(); + WindowsConsoleFunctions.reset(result); + if (result.isFailed()) { + throw new NativeException(String.format("Could not reset console for %s: %s", this, result.getMessage())); + } + return this; + } + + @Override + public Terminal cursorDown(int count) throws NativeException { + throw new UnsupportedOperationException(); + } + + @Override + public Terminal cursorUp(int count) throws NativeException { + throw new UnsupportedOperationException(); + } + + @Override + public Terminal cursorLeft(int count) throws NativeException { + throw new UnsupportedOperationException(); + } + + @Override + public Terminal cursorRight(int count) throws NativeException { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/net/rubygrapefruit/platform/internal/jni/NativeLibraryFunctions.java b/src/main/java/net/rubygrapefruit/platform/internal/jni/NativeLibraryFunctions.java index 47785bd..bc8d209 100755 --- a/src/main/java/net/rubygrapefruit/platform/internal/jni/NativeLibraryFunctions.java +++ b/src/main/java/net/rubygrapefruit/platform/internal/jni/NativeLibraryFunctions.java @@ -1,7 +1,7 @@ -package net.rubygrapefruit.platform.internal.jni; - -public class NativeLibraryFunctions { - public static final int VERSION = 2; - - public static native int getVersion(); -} +package net.rubygrapefruit.platform.internal.jni; + +public class NativeLibraryFunctions { + public static final int VERSION = 3; + + public static native int getVersion(); +} diff --git a/src/main/java/net/rubygrapefruit/platform/internal/jni/TerminfoFunctions.java b/src/main/java/net/rubygrapefruit/platform/internal/jni/TerminfoFunctions.java index ea61468..92dcd25 100755 --- a/src/main/java/net/rubygrapefruit/platform/internal/jni/TerminfoFunctions.java +++ b/src/main/java/net/rubygrapefruit/platform/internal/jni/TerminfoFunctions.java @@ -1,19 +1,24 @@ -package net.rubygrapefruit.platform.internal.jni; - -import net.rubygrapefruit.platform.internal.FunctionResult; - -public class TerminfoFunctions { - /** - * Sets up terminal info and switches output to normal mode. - */ - public static native void initTerminal(int filedes, FunctionResult result); - - public static native void bold(FunctionResult result); - - public static native void reset(FunctionResult result); - - /** - * Set the foreground color to the given ansi color. - */ - public static native void foreground(int ansiColor, FunctionResult result); -} +package net.rubygrapefruit.platform.internal.jni; + +import net.rubygrapefruit.platform.internal.FunctionResult; + +public class TerminfoFunctions { + /** + * Sets up terminal info and switches output to normal mode. + */ + public static native void initTerminal(int filedes, FunctionResult result); + + public static native void bold(FunctionResult result); + + public static native void reset(FunctionResult result); + + public static native void foreground(int ansiColor, FunctionResult result); + + public static native void left(int count, FunctionResult result); + + public static native void right(int count, FunctionResult result); + + public static native void up(int count, FunctionResult result); + + public static native void down(int count, FunctionResult result); +}