diff --git a/readme.md b/readme.md index 06647d2..3599e81 100755 --- a/readme.md +++ b/readme.md @@ -29,3 +29,5 @@ You need to install the `libncurses5-dev` package to pick up the ncurses header * 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/win.cpp b/src/main/cpp/win.cpp index 8c9c11a..dbab1ba 100755 --- a/src/main/cpp/win.cpp +++ b/src/main/cpp/win.cpp @@ -70,4 +70,90 @@ Java_net_rubygrapefruit_platform_internal_jni_WindowsConsoleFunctions_getConsole env->SetIntField(dimension, heightField, console_info.srWindow.Bottom - console_info.srWindow.Top + 1); } +HANDLE current_console = NULL; +WORD original_attributes = 0; +WORD current_attributes = 0; + +JNIEXPORT void JNICALL +Java_net_rubygrapefruit_platform_internal_jni_WindowsConsoleFunctions_initConsole(JNIEnv *env, jclass target, jint output, jobject result) { + CONSOLE_SCREEN_BUFFER_INFO console_info; + HANDLE handle = getHandle(env, output, result); + if (handle == NULL) { + mark_failed_with_message(env, "not a terminal", result); + return; + } + if (!GetConsoleScreenBufferInfo(handle, &console_info)) { + if (GetLastError() == ERROR_INVALID_HANDLE) { + mark_failed_with_message(env, "not a console", result); + } else { + mark_failed_with_errno(env, "could not get console buffer", result); + } + return; + } + current_console = handle; + original_attributes = console_info.wAttributes; + current_attributes = original_attributes; + Java_net_rubygrapefruit_platform_internal_jni_WindowsConsoleFunctions_normal(env, target, result); +} + +JNIEXPORT void JNICALL +Java_net_rubygrapefruit_platform_internal_jni_WindowsConsoleFunctions_bold(JNIEnv *env, jclass target, jobject result) { + current_attributes |= FOREGROUND_INTENSITY; + if (!SetConsoleTextAttribute(current_console, current_attributes)) { + mark_failed_with_errno(env, "could not set text attributes", result); + } +} + +JNIEXPORT void JNICALL +Java_net_rubygrapefruit_platform_internal_jni_WindowsConsoleFunctions_normal(JNIEnv *env, jclass target, jobject result) { + current_attributes &= ~FOREGROUND_INTENSITY; + SetConsoleTextAttribute(current_console, current_attributes); + if (!SetConsoleTextAttribute(current_console, current_attributes)) { + mark_failed_with_errno(env, "could not set text attributes", result); + } +} + +JNIEXPORT void JNICALL +Java_net_rubygrapefruit_platform_internal_jni_WindowsConsoleFunctions_reset(JNIEnv *env, jclass target, jobject result) { + SetConsoleTextAttribute(current_console, original_attributes); + if (!SetConsoleTextAttribute(current_console, current_attributes)) { + mark_failed_with_errno(env, "could not set text attributes", result); + } +} + +JNIEXPORT void JNICALL +Java_net_rubygrapefruit_platform_internal_jni_WindowsConsoleFunctions_foreground(JNIEnv *env, jclass target, jint color, jobject result) { + current_attributes &= ~ (FOREGROUND_BLUE|FOREGROUND_RED|FOREGROUND_GREEN); + switch (color) { + case 0: + break; + case 1: + current_attributes |= FOREGROUND_RED; + break; + case 2: + current_attributes |= FOREGROUND_GREEN; + break; + case 3: + current_attributes |= FOREGROUND_RED|FOREGROUND_GREEN; + break; + case 4: + current_attributes |= FOREGROUND_BLUE; + break; + case 5: + current_attributes |= FOREGROUND_RED|FOREGROUND_BLUE; + break; + case 6: + current_attributes |= FOREGROUND_GREEN|FOREGROUND_BLUE; + break; + default: + current_attributes |= FOREGROUND_RED|FOREGROUND_GREEN|FOREGROUND_BLUE; + break; + } + + SetConsoleTextAttribute(current_console, current_attributes); + if (!SetConsoleTextAttribute(current_console, current_attributes)) { + mark_failed_with_errno(env, "could not set text attributes", result); + } +} + #endif diff --git a/src/main/java/net/rubygrapefruit/platform/internal/AbstractTerminal.java b/src/main/java/net/rubygrapefruit/platform/internal/AbstractTerminal.java new file mode 100755 index 0000000..f52b874 --- /dev/null +++ b/src/main/java/net/rubygrapefruit/platform/internal/AbstractTerminal.java @@ -0,0 +1,17 @@ +package net.rubygrapefruit.platform.internal; + +import net.rubygrapefruit.platform.Terminal; + +public abstract class AbstractTerminal implements Terminal { + public final void init() { + doInit(); + Runtime.getRuntime().addShutdownHook(new Thread(){ + @Override + public void run() { + reset(); + } + }); + } + + protected abstract void doInit(); +} diff --git a/src/main/java/net/rubygrapefruit/platform/internal/DefaultTerminal.java b/src/main/java/net/rubygrapefruit/platform/internal/TerminfoTerminal.java similarity index 79% rename from src/main/java/net/rubygrapefruit/platform/internal/DefaultTerminal.java rename to src/main/java/net/rubygrapefruit/platform/internal/TerminfoTerminal.java index b86d8e4..9a5c092 100755 --- a/src/main/java/net/rubygrapefruit/platform/internal/DefaultTerminal.java +++ b/src/main/java/net/rubygrapefruit/platform/internal/TerminfoTerminal.java @@ -9,29 +9,29 @@ import net.rubygrapefruit.platform.internal.jni.TerminfoFunctions; import java.io.PrintStream; -public class DefaultTerminal implements Terminal { +public class TerminfoTerminal extends AbstractTerminal { private final TerminalAccess.Output output; private final PrintStream stream; private Color foreground; - public DefaultTerminal(TerminalAccess.Output output) { + public TerminfoTerminal(TerminalAccess.Output output) { this.output = output; stream = output == TerminalAccess.Output.Stdout ? System.out : System.err; } - public void init() { + @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: %s", result.getMessage())); + throw new NativeException(String.format("Could not open terminal for %s: %s", this, result.getMessage())); } - Runtime.getRuntime().addShutdownHook(new Thread(){ - @Override - public void run() { - reset(); - } - }); } @Override @@ -40,7 +40,7 @@ public class DefaultTerminal implements Terminal { FunctionResult result = new FunctionResult(); PosixTerminalFunctions.getTerminalSize(output.ordinal(), terminalSize, result); if (result.isFailed()) { - throw new NativeException(String.format("Could not get terminal size: %s", result.getMessage())); + throw new NativeException(String.format("Could not get terminal size for %s: %s", this, result.getMessage())); } return terminalSize; } @@ -51,7 +51,7 @@ public class DefaultTerminal implements Terminal { 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())); + throw new NativeException(String.format("Could not switch foreground color for %s: %s", this, result.getMessage())); } foreground = color; return this; @@ -63,7 +63,7 @@ public class DefaultTerminal implements Terminal { FunctionResult result = new FunctionResult(); TerminfoFunctions.bold(result); if (result.isFailed()) { - throw new NativeException(String.format("Could not switch to bold mode: %s", result.getMessage())); + throw new NativeException(String.format("Could not switch to bold mode for %s: %s", this, result.getMessage())); } return this; } @@ -83,7 +83,7 @@ public class DefaultTerminal implements Terminal { FunctionResult result = new FunctionResult(); TerminfoFunctions.reset(result); if (result.isFailed()) { - throw new NativeException(String.format("Could not reset terminal: %s", result.getMessage())); + throw new NativeException(String.format("Could not reset terminal for %s: %s", this, result.getMessage())); } return this; } diff --git a/src/main/java/net/rubygrapefruit/platform/internal/TerminfoTerminalAccess.java b/src/main/java/net/rubygrapefruit/platform/internal/TerminfoTerminalAccess.java index 6e9e7ce..897fbd3 100755 --- a/src/main/java/net/rubygrapefruit/platform/internal/TerminfoTerminalAccess.java +++ b/src/main/java/net/rubygrapefruit/platform/internal/TerminfoTerminalAccess.java @@ -18,7 +18,7 @@ public class TerminfoTerminalAccess implements TerminalAccess { throw new UnsupportedOperationException("Currently only one output can be used as a terminal."); } - DefaultTerminal terminal = new DefaultTerminal(output); + TerminfoTerminal terminal = new TerminfoTerminal(output); terminal.init(); currentlyOpen = output; diff --git a/src/main/java/net/rubygrapefruit/platform/internal/WindowsTerminal.java b/src/main/java/net/rubygrapefruit/platform/internal/WindowsTerminal.java index b3bedb8..51ec49c 100755 --- a/src/main/java/net/rubygrapefruit/platform/internal/WindowsTerminal.java +++ b/src/main/java/net/rubygrapefruit/platform/internal/WindowsTerminal.java @@ -6,41 +6,75 @@ import net.rubygrapefruit.platform.TerminalAccess; import net.rubygrapefruit.platform.TerminalSize; import net.rubygrapefruit.platform.internal.jni.WindowsConsoleFunctions; -public class WindowsTerminal implements Terminal { +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 terminal size: %s", result.getMessage())); + throw new NativeException(String.format("Could not determine console size for %s: %s", this, result.getMessage())); } return size; } @Override public Terminal bold() { - throw new UnsupportedOperationException(); + 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) { - throw new UnsupportedOperationException(); + 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() { - throw new UnsupportedOperationException(); + 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() { - throw new UnsupportedOperationException(); + 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; } } diff --git a/src/main/java/net/rubygrapefruit/platform/internal/WindowsTerminalAccess.java b/src/main/java/net/rubygrapefruit/platform/internal/WindowsTerminalAccess.java index 34b682e..36328d6 100755 --- a/src/main/java/net/rubygrapefruit/platform/internal/WindowsTerminalAccess.java +++ b/src/main/java/net/rubygrapefruit/platform/internal/WindowsTerminalAccess.java @@ -6,6 +6,8 @@ import net.rubygrapefruit.platform.TerminalAccess; import net.rubygrapefruit.platform.internal.jni.WindowsConsoleFunctions; public class WindowsTerminalAccess implements TerminalAccess { + private static Output currentlyOpen; + @Override public boolean isTerminal(Output output) { FunctionResult result = new FunctionResult(); @@ -19,6 +21,14 @@ public class WindowsTerminalAccess implements TerminalAccess { @Override public Terminal getTerminal(Output output) { - return new WindowsTerminal(output); + if (currentlyOpen != null) { + throw new UnsupportedOperationException("Currently only one output can be used as a terminal."); + } + + WindowsTerminal terminal = new WindowsTerminal(output); + terminal.init(); + + currentlyOpen = output; + return terminal; } } diff --git a/src/main/java/net/rubygrapefruit/platform/internal/jni/WindowsConsoleFunctions.java b/src/main/java/net/rubygrapefruit/platform/internal/jni/WindowsConsoleFunctions.java index 4c905cc..eb0fc12 100755 --- a/src/main/java/net/rubygrapefruit/platform/internal/jni/WindowsConsoleFunctions.java +++ b/src/main/java/net/rubygrapefruit/platform/internal/jni/WindowsConsoleFunctions.java @@ -7,4 +7,14 @@ public class WindowsConsoleFunctions { public static native boolean isConsole(int filedes, FunctionResult result); public static native void getConsoleSize(int filedes, MutableTerminalSize size, FunctionResult result); + + public static native void initConsole(int filedes, FunctionResult result); + + public static native void bold(FunctionResult result); + + public static native void normal(FunctionResult result); + + public static native void reset(FunctionResult result); + + public static native void foreground(int ansiColor, FunctionResult result); } diff --git a/src/test/groovy/net/rubygrapefruit/platform/TerminalTest.groovy b/src/test/groovy/net/rubygrapefruit/platform/TerminalTest.groovy index b6ac707..64d5102 100755 --- a/src/test/groovy/net/rubygrapefruit/platform/TerminalTest.groovy +++ b/src/test/groovy/net/rubygrapefruit/platform/TerminalTest.groovy @@ -3,6 +3,8 @@ package net.rubygrapefruit.platform import org.junit.Rule import org.junit.rules.TemporaryFolder import spock.lang.Specification +import net.rubygrapefruit.platform.internal.Platform +import spock.lang.IgnoreIf class TerminalTest extends Specification { @Rule TemporaryFolder tmpDir @@ -14,12 +16,23 @@ class TerminalTest extends Specification { !terminal.isTerminal(TerminalAccess.Output.Stderr); } - def "cannot access terminal from a test"() { + @IgnoreIf({Platform.current().windows}) + def "cannot access posix terminal from a test"() { when: terminal.getTerminal(TerminalAccess.Output.Stdout) then: NativeException e = thrown() - e.message == 'Could not open terminal: not a terminal' + e.message == 'Could not open terminal for stdout: not a terminal' + } + + @IgnoreIf({!Platform.current().windows}) + def "cannot access windows console from a test"() { + when: + terminal.getTerminal(TerminalAccess.Output.Stdout) + + then: + NativeException e = thrown() + e.message == 'Could not open console for stdout: not a console' } }