From 31e9d2b41c336a3a60171414880daaa3718b90d4 Mon Sep 17 00:00:00 2001 From: Adam Murdoch Date: Sat, 4 Aug 2012 07:06:11 +1000 Subject: [PATCH] Added support for switching the terminal to bold mode. --- build.gradle | 3 + readme.md | 6 ++ src/main/cpp/posixFunctions.c | 72 +++++++++++++++++++ .../net/rubygrapefruit/platform/Main.java | 5 ++ .../net/rubygrapefruit/platform/Platform.java | 62 +++++++++++++++- .../net/rubygrapefruit/platform/Terminal.java | 15 ++++ .../internal/NativeLibraryFunctions.java | 7 ++ .../platform/internal/TerminfoFunctions.java | 12 ++++ .../platform/TerminalTest.groovy | 2 +- 9 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 src/main/java/net/rubygrapefruit/platform/internal/NativeLibraryFunctions.java create mode 100644 src/main/java/net/rubygrapefruit/platform/internal/TerminfoFunctions.java diff --git a/build.gradle b/build.gradle index e93c527..16f65e6 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,7 @@ libraries { includes(["${org.gradle.internal.jvm.Jvm.current().javaHome}/include"]) includes(["${org.gradle.internal.jvm.Jvm.current().javaHome}/include/linux"]) } + args("-lcurses") } } } @@ -40,9 +41,11 @@ task nativeHeaders { executable org.gradle.internal.jvm.Jvm.current().getExecutable('javah') args '-o', outputFile args '-classpath', sourceSets.main.output.classesDir + args 'net.rubygrapefruit.platform.internal.NativeLibraryFunctions' args 'net.rubygrapefruit.platform.internal.PosixFileFunctions' args 'net.rubygrapefruit.platform.internal.PosixProcessFunctions' args 'net.rubygrapefruit.platform.internal.PosixTerminalFunctions' + args 'net.rubygrapefruit.platform.internal.TerminfoFunctions' } } } diff --git a/readme.md b/readme.md index 02df39e..5ef01b5 100644 --- a/readme.md +++ b/readme.md @@ -5,5 +5,11 @@ Provides Java bindings for various native APIs. * 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. Currently only ported to OS X (10.7.4) and Linux (Ubuntu 12.04). + +#### TODO + +* Split out separate native library for terminal handling. +* String names for errno values. diff --git a/src/main/cpp/posixFunctions.c b/src/main/cpp/posixFunctions.c index 9ee7b74..100d250 100644 --- a/src/main/cpp/posixFunctions.c +++ b/src/main/cpp/posixFunctions.c @@ -1,9 +1,12 @@ #include "native.h" +#include #include #include #include #include #include +#include +#include void markFailed(JNIEnv *env, jobject result) { jclass destClass = env->GetObjectClass(result); @@ -11,6 +14,19 @@ void markFailed(JNIEnv *env, jobject result) { env->CallVoidMethod(result, method, errno); } +/* + * Generic functions + */ + +JNIEXPORT jint JNICALL +Java_net_rubygrapefruit_platform_internal_NativeLibraryFunctions_getVersion(JNIEnv *env, jclass target) { + return 1; +} + +/* + * File functions + */ + 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); @@ -34,11 +50,19 @@ Java_net_rubygrapefruit_platform_internal_PosixFileFunctions_stat(JNIEnv *env, j env->SetIntField(dest, modeField, 0777 & fileInfo.st_mode); } +/* + * Process functions + */ + JNIEXPORT jint JNICALL Java_net_rubygrapefruit_platform_internal_PosixProcessFunctions_getPid(JNIEnv *env, jclass target) { return getpid(); } +/* + * Terminal functions + */ + JNIEXPORT jboolean JNICALL Java_net_rubygrapefruit_platform_internal_PosixTerminalFunctions_isatty(JNIEnv *env, jclass target, jint output) { switch (output) { @@ -64,3 +88,51 @@ Java_net_rubygrapefruit_platform_internal_PosixTerminalFunctions_getTerminalSize 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) { + markFailed(env, result); + return; + } + if (tputs(cap, 1, write_to_terminal) == ERR) { + markFailed(env, result); + return; + } +} + +JNIEXPORT void JNICALL +Java_net_rubygrapefruit_platform_internal_TerminfoFunctions_initTerminal(JNIEnv *env, jclass target, jint output, jobject result) { + char* termType = getenv("TERM"); + if (termType == NULL) { + markFailed(env, result); + return; + } + int retval = tgetent(NULL, termType); + if (retval != 1) { + markFailed(env, result); + return; + } + current_terminal = output + 1; + write_capability(env, "me", result); +} + +JNIEXPORT void JNICALL +Java_net_rubygrapefruit_platform_internal_TerminfoFunctions_bold(JNIEnv *env, jclass target, jint output, 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) { + write_capability(env, "me", result); +} diff --git a/src/main/java/net/rubygrapefruit/platform/Main.java b/src/main/java/net/rubygrapefruit/platform/Main.java index bfadfc1..868fe14 100644 --- a/src/main/java/net/rubygrapefruit/platform/Main.java +++ b/src/main/java/net/rubygrapefruit/platform/Main.java @@ -15,6 +15,11 @@ 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.print("[normal] "); + terminal.bold(); + System.out.print("[bold]"); + terminal.normal(); + System.out.println(" [normal]"); } } } diff --git a/src/main/java/net/rubygrapefruit/platform/Platform.java b/src/main/java/net/rubygrapefruit/platform/Platform.java index c588354..b3639e0 100644 --- a/src/main/java/net/rubygrapefruit/platform/Platform.java +++ b/src/main/java/net/rubygrapefruit/platform/Platform.java @@ -4,6 +4,7 @@ import net.rubygrapefruit.platform.internal.*; import java.io.File; import java.io.IOException; +import java.io.PrintStream; /** * Provides access to the native integrations. Use {@link #get(Class)} to load a particular integration. @@ -25,6 +26,10 @@ public class Platform { } catch (IOException e) { throw new RuntimeException(e); } + int nativeVersion = NativeLibraryFunctions.getVersion(); + if (nativeVersion != NativeLibraryFunctions.VERSION) { + throw new NativeException(String.format("Unexpected native library version loaded. Expected %s, was %s.", nativeVersion, NativeLibraryFunctions.VERSION)); + } loaded = true; } } @@ -71,6 +76,8 @@ public class Platform { } private static class DefaultTerminalAccess implements TerminalAccess { + private static Output currentlyOpen; + @Override public boolean isTerminal(Output output) { return PosixTerminalFunctions.isatty(output.ordinal()); @@ -78,18 +85,35 @@ public class Platform { @Override public Terminal getTerminal(Output output) { - if (!isTerminal(output)) { - throw new NativeException(String.format("%s is not attached to a terminal.", output)); + if (currentlyOpen != null) { + throw new UnsupportedOperationException("Currently only one output can be used as a terminal."); } - return new DefaultTerminal(output); + + DefaultTerminal terminal = new DefaultTerminal(output); + terminal.init(); + + currentlyOpen = output; + return terminal; } } private static class DefaultTerminal implements Terminal { private final TerminalAccess.Output output; + private final PrintStream stream; public DefaultTerminal(TerminalAccess.Output output) { this.output = output; + stream = output == TerminalAccess.Output.Stdout ? System.out : System.err; + } + + public void init() { + stream.flush(); + 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())); + } } @Override @@ -103,5 +127,37 @@ public class Platform { } return terminalSize; } + + @Override + public Terminal bold() { + stream.flush(); + FunctionResult result = new FunctionResult(); + TerminfoFunctions.bold(output.ordinal(), result); + if (result.isFailed()) { + throw new NativeException(String.format("Could not switch to bold mode. Errno is %d.", + result.getErrno())); + } + return this; + } + + @Override + public Terminal bold(String output) { + bold(); + stream.print(output); + normal(); + return this; + } + + @Override + public Terminal normal() { + stream.flush(); + FunctionResult result = new FunctionResult(); + TerminfoFunctions.normal(output.ordinal(), result); + if (result.isFailed()) { + throw new NativeException(String.format("Could not switch to normal mode. Errno is %d.", + result.getErrno())); + } + return this; + } } } diff --git a/src/main/java/net/rubygrapefruit/platform/Terminal.java b/src/main/java/net/rubygrapefruit/platform/Terminal.java index 3567647..d2e7ea2 100644 --- a/src/main/java/net/rubygrapefruit/platform/Terminal.java +++ b/src/main/java/net/rubygrapefruit/platform/Terminal.java @@ -2,4 +2,19 @@ package net.rubygrapefruit.platform; public interface Terminal { TerminalSize getTerminalSize(); + + /** + * 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(); } diff --git a/src/main/java/net/rubygrapefruit/platform/internal/NativeLibraryFunctions.java b/src/main/java/net/rubygrapefruit/platform/internal/NativeLibraryFunctions.java new file mode 100644 index 0000000..67a3a21 --- /dev/null +++ b/src/main/java/net/rubygrapefruit/platform/internal/NativeLibraryFunctions.java @@ -0,0 +1,7 @@ +package net.rubygrapefruit.platform.internal; + +public class NativeLibraryFunctions { + public static final int VERSION = 1; + + 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 new file mode 100644 index 0000000..f064cd0 --- /dev/null +++ b/src/main/java/net/rubygrapefruit/platform/internal/TerminfoFunctions.java @@ -0,0 +1,12 @@ +package net.rubygrapefruit.platform.internal; + +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(int filedes, FunctionResult result); + + public static native void normal(int filedes, FunctionResult result); +} diff --git a/src/test/groovy/net/rubygrapefruit/platform/TerminalTest.groovy b/src/test/groovy/net/rubygrapefruit/platform/TerminalTest.groovy index 972b7fd..e1bc185 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('Stdout is not attached to a terminal.') + e.message.startsWith('Could not open terminal. Errno is ') } }