From 335065887ee8335509c0fa0c341cc3a46c508fc9 Mon Sep 17 00:00:00 2001 From: Adam Murdoch Date: Sat, 4 Aug 2012 10:00:32 +1000 Subject: [PATCH] - Added support for setting the terminal foreground color. - Some improvements to error handling. --- readme.md | 1 + src/main/cpp/posixFunctions.c | 66 +++++++++++++++---- .../net/rubygrapefruit/platform/Main.java | 13 ++++ .../net/rubygrapefruit/platform/Platform.java | 56 ++++++++++------ .../net/rubygrapefruit/platform/Terminal.java | 22 +++++-- .../platform/internal/FunctionResult.java | 19 ++++-- .../internal/NativeLibraryFunctions.java | 2 +- .../platform/internal/TerminfoFunctions.java | 9 ++- .../platform/PosixFileTest.groovy | 4 +- .../platform/TerminalTest.groovy | 2 +- 10 files changed, 145 insertions(+), 49 deletions(-) diff --git a/readme.md b/readme.md index 5ef01b5..00741b9 100644 --- a/readme.md +++ b/readme.md @@ -6,6 +6,7 @@ Provides Java bindings for various native APIs. * 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 only ported to OS X (10.7.4) and Linux (Ubuntu 12.04). diff --git a/src/main/cpp/posixFunctions.c b/src/main/cpp/posixFunctions.c index 100d250..8b9eb61 100644 --- a/src/main/cpp/posixFunctions.c +++ b/src/main/cpp/posixFunctions.c @@ -8,10 +8,24 @@ #include #include -void markFailed(JNIEnv *env, jobject result) { +/* + * Marks the given result as failed, using the current value of errno + */ +void mark_failed_with_errno(JNIEnv *env, const char* message, jobject result) { jclass destClass = env->GetObjectClass(result); - jmethodID method = env->GetMethodID(destClass, "failed", "(I)V"); - env->CallVoidMethod(result, method, errno); + jmethodID method = env->GetMethodID(destClass, "failed", "(Ljava/lang/String;I)V"); + jstring message_str = env->NewStringUTF(message); + env->CallVoidMethod(result, method, message_str, errno); +} + +/* + * Marks the given result as failed, using the given error message + */ +void mark_failed_with_message(JNIEnv *env, const char* message, jobject result) { + jclass destClass = env->GetObjectClass(result); + jmethodID method = env->GetMethodID(destClass, "failed", "(Ljava/lang/String;)V"); + jstring message_str = env->NewStringUTF(message); + env->CallVoidMethod(result, method, message_str); } /* @@ -20,7 +34,7 @@ void markFailed(JNIEnv *env, jobject result) { JNIEXPORT jint JNICALL Java_net_rubygrapefruit_platform_internal_NativeLibraryFunctions_getVersion(JNIEnv *env, jclass target) { - return 1; + return 2; } /* @@ -31,8 +45,9 @@ JNIEXPORT void JNICALL Java_net_rubygrapefruit_platform_internal_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) { - markFailed(env, result); + mark_failed_with_errno(env, "could not chmod file", result); } } @@ -41,8 +56,9 @@ Java_net_rubygrapefruit_platform_internal_PosixFileFunctions_stat(JNIEnv *env, j struct stat fileInfo; const char* pathUtf8 = env->GetStringUTFChars(path, NULL); int retval = stat(pathUtf8, &fileInfo); + env->ReleaseStringUTFChars(path, pathUtf8); if (retval != 0) { - markFailed(env, result); + mark_failed_with_errno(env, "could not stat file", result); return; } jclass destClass = env->GetObjectClass(dest); @@ -79,7 +95,7 @@ Java_net_rubygrapefruit_platform_internal_PosixTerminalFunctions_getTerminalSize struct winsize screen_size; int retval = ioctl(output+1, TIOCGWINSZ, &screen_size); if (retval != 0) { - markFailed(env, result); + mark_failed_with_errno(env, "could not fetch terminal size", result); return; } jclass dimensionClass = env->GetObjectClass(dimension); @@ -102,25 +118,29 @@ int write_to_terminal(int ch) { void write_capability(JNIEnv *env, const char* capability, jobject result) { char* cap = tgetstr((char*)capability, NULL); if (cap == NULL) { - markFailed(env, result); + mark_failed_with_message(env, "unknown terminal capability", result); return; } if (tputs(cap, 1, write_to_terminal) == ERR) { - markFailed(env, result); + mark_failed_with_message(env, "could not write to terminal", result); return; } } JNIEXPORT void JNICALL Java_net_rubygrapefruit_platform_internal_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) { - markFailed(env, result); + mark_failed_with_message(env, "$TERM not set", result); return; } int retval = tgetent(NULL, termType); if (retval != 1) { - markFailed(env, result); + mark_failed_with_message(env, "could not get termcap entry", result); return; } current_terminal = output + 1; @@ -128,11 +148,31 @@ Java_net_rubygrapefruit_platform_internal_TerminfoFunctions_initTerminal(JNIEnv } JNIEXPORT void JNICALL -Java_net_rubygrapefruit_platform_internal_TerminfoFunctions_bold(JNIEnv *env, jclass target, jint output, jobject result) { +Java_net_rubygrapefruit_platform_internal_TerminfoFunctions_bold(JNIEnv *env, jclass target, jobject result) { write_capability(env, "md", result); } JNIEXPORT void JNICALL -Java_net_rubygrapefruit_platform_internal_TerminfoFunctions_normal(JNIEnv *env, jclass target, jint output, jobject result) { +Java_net_rubygrapefruit_platform_internal_TerminfoFunctions_reset(JNIEnv *env, jclass target, jobject result) { write_capability(env, "me", result); } + +JNIEXPORT void JNICALL +Java_net_rubygrapefruit_platform_internal_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; + } +} diff --git a/src/main/java/net/rubygrapefruit/platform/Main.java b/src/main/java/net/rubygrapefruit/platform/Main.java index 868fe14..ae0e02a 100644 --- a/src/main/java/net/rubygrapefruit/platform/Main.java +++ b/src/main/java/net/rubygrapefruit/platform/Main.java @@ -15,11 +15,24 @@ public class Main { 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(); } } } diff --git a/src/main/java/net/rubygrapefruit/platform/Platform.java b/src/main/java/net/rubygrapefruit/platform/Platform.java index b3639e0..2ee6ed0 100644 --- a/src/main/java/net/rubygrapefruit/platform/Platform.java +++ b/src/main/java/net/rubygrapefruit/platform/Platform.java @@ -52,7 +52,7 @@ public class Platform { FunctionResult result = new FunctionResult(); PosixFileFunctions.chmod(file.getPath(), perms, result); if (result.isFailed()) { - throw new NativeException(String.format("Could not set UNIX mode on %s. Errno is %d.", file, result.getErrno())); + throw new NativeException(String.format("Could not set UNIX mode on %s: %s", file, result.getMessage())); } } @@ -62,7 +62,7 @@ public class Platform { FileStat stat = new FileStat(); PosixFileFunctions.stat(file.getPath(), stat, result); if (result.isFailed()) { - throw new NativeException(String.format("Could not get UNIX mode on %s. Errno is %d.", file, result.getErrno())); + throw new NativeException(String.format("Could not get UNIX mode on %s: %s", file, result.getMessage())); } return stat.mode; } @@ -100,6 +100,7 @@ public class Platform { private static class DefaultTerminal implements Terminal { private final TerminalAccess.Output output; private final PrintStream stream; + private Color foreground; public DefaultTerminal(TerminalAccess.Output output) { this.output = output; @@ -111,9 +112,14 @@ public class Platform { FunctionResult result = new FunctionResult(); TerminfoFunctions.initTerminal(output.ordinal(), result); if (result.isFailed()) { - throw new NativeException(String.format("Could not open terminal. Errno is %d.", - result.getErrno())); + throw new NativeException(String.format("Could not open terminal: %s", result.getMessage())); } + Runtime.getRuntime().addShutdownHook(new Thread(){ + @Override + public void run() { + reset(); + } + }); } @Override @@ -122,40 +128,50 @@ public class Platform { FunctionResult result = new FunctionResult(); PosixTerminalFunctions.getTerminalSize(output.ordinal(), terminalSize, result); if (result.isFailed()) { - throw new NativeException(String.format("Could not get terminal size. Errno is %d.", - result.getErrno())); + throw new NativeException(String.format("Could not get terminal size: %s", 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: %s", result.getMessage())); + } + foreground = color; + return this; + } + @Override public Terminal bold() { stream.flush(); FunctionResult result = new FunctionResult(); - TerminfoFunctions.bold(output.ordinal(), result); + TerminfoFunctions.bold(result); if (result.isFailed()) { - throw new NativeException(String.format("Could not switch to bold mode. Errno is %d.", - result.getErrno())); + throw new NativeException(String.format("Could not switch to bold mode: %s", result.getMessage())); } return this; } - @Override - public Terminal bold(String output) { - bold(); - stream.print(output); - normal(); - 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.normal(output.ordinal(), result); + TerminfoFunctions.reset(result); if (result.isFailed()) { - throw new NativeException(String.format("Could not switch to normal mode. Errno is %d.", - result.getErrno())); + throw new NativeException(String.format("Could not reset terminal: %s", result.getMessage())); } return this; } diff --git a/src/main/java/net/rubygrapefruit/platform/Terminal.java b/src/main/java/net/rubygrapefruit/platform/Terminal.java index d2e7ea2..ff23a21 100644 --- a/src/main/java/net/rubygrapefruit/platform/Terminal.java +++ b/src/main/java/net/rubygrapefruit/platform/Terminal.java @@ -1,20 +1,32 @@ package net.rubygrapefruit.platform; public interface Terminal { + enum Color { + Black, Red, Green, Yellow, Blue, Magenta, Cyan, White + } + + /** + * Returns the size of the terminal. + */ TerminalSize getTerminalSize(); + /** + * Sets the terminal foreground color. + */ + Terminal foreground(Color color); + /** * Switches the terminal to bold mode. */ Terminal bold(); - /** - * Switches the terminal to bold mode, outputs the given text, then switches to normal mode. - */ - Terminal bold(String output); - /** * Switches the terminal to normal mode. */ Terminal normal(); + + /** + * Switches the terminal to normal mode and restores default colors. + */ + Terminal reset(); } diff --git a/src/main/java/net/rubygrapefruit/platform/internal/FunctionResult.java b/src/main/java/net/rubygrapefruit/platform/internal/FunctionResult.java index 387ee12..ff3de73 100644 --- a/src/main/java/net/rubygrapefruit/platform/internal/FunctionResult.java +++ b/src/main/java/net/rubygrapefruit/platform/internal/FunctionResult.java @@ -1,17 +1,26 @@ package net.rubygrapefruit.platform.internal; public class FunctionResult { + String message; int errno; - void failed(int errno) { + void failed(String message, int errno) { + this.message = message; this.errno = errno; } - public boolean isFailed() { - return errno != 0; + void failed(String message) { + this.message = message; } - public int getErrno() { - return errno; + public boolean isFailed() { + return message != null; + } + + public String getMessage() { + if (errno != 0) { + return String.format("%s (errno %d)", message, errno); + } + return message; } } diff --git a/src/main/java/net/rubygrapefruit/platform/internal/NativeLibraryFunctions.java b/src/main/java/net/rubygrapefruit/platform/internal/NativeLibraryFunctions.java index 67a3a21..d979ad2 100644 --- a/src/main/java/net/rubygrapefruit/platform/internal/NativeLibraryFunctions.java +++ b/src/main/java/net/rubygrapefruit/platform/internal/NativeLibraryFunctions.java @@ -1,7 +1,7 @@ package net.rubygrapefruit.platform.internal; public class NativeLibraryFunctions { - public static final int VERSION = 1; + public static final int VERSION = 2; public static native int getVersion(); } diff --git a/src/main/java/net/rubygrapefruit/platform/internal/TerminfoFunctions.java b/src/main/java/net/rubygrapefruit/platform/internal/TerminfoFunctions.java index f064cd0..ec9f6fd 100644 --- a/src/main/java/net/rubygrapefruit/platform/internal/TerminfoFunctions.java +++ b/src/main/java/net/rubygrapefruit/platform/internal/TerminfoFunctions.java @@ -6,7 +6,12 @@ public class TerminfoFunctions { */ public static native void initTerminal(int filedes, FunctionResult result); - public static native void bold(int filedes, FunctionResult result); + public static native void bold(FunctionResult result); - public static native void normal(int filedes, 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); } diff --git a/src/test/groovy/net/rubygrapefruit/platform/PosixFileTest.groovy b/src/test/groovy/net/rubygrapefruit/platform/PosixFileTest.groovy index 3d3acaf..810a277 100644 --- a/src/test/groovy/net/rubygrapefruit/platform/PosixFileTest.groovy +++ b/src/test/groovy/net/rubygrapefruit/platform/PosixFileTest.groovy @@ -36,7 +36,7 @@ class PosixFileTest extends Specification { then: NativeException e = thrown() - e.message == "Could not set UNIX mode on $file. Errno is 2." + e.message == "Could not set UNIX mode on $file: could not chmod file (errno 2)" } def "throws exception on failure to get mode"() { @@ -47,6 +47,6 @@ class PosixFileTest extends Specification { then: NativeException e = thrown() - e.message == "Could not get UNIX mode on $file. Errno is 2." + e.message == "Could not get UNIX mode on $file: could not stat file (errno 2)" } } diff --git a/src/test/groovy/net/rubygrapefruit/platform/TerminalTest.groovy b/src/test/groovy/net/rubygrapefruit/platform/TerminalTest.groovy index e1bc185..922fbab 100644 --- a/src/test/groovy/net/rubygrapefruit/platform/TerminalTest.groovy +++ b/src/test/groovy/net/rubygrapefruit/platform/TerminalTest.groovy @@ -20,6 +20,6 @@ class TerminalTest extends Specification { then: NativeException e = thrown() - e.message.startsWith('Could not open terminal. Errno is ') + e.message == 'Could not open terminal: not a terminal' } }