diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 169a1924..e1d5db2a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest, windows-latest, macos-latest ] - java: [ '11', '17' ] + java: [ '21' ] steps: - uses: actions/checkout@v2 with: diff --git a/pom.xml b/pom.xml index 9cf9578f..2b8a8f61 100644 --- a/pom.xml +++ b/pom.xml @@ -160,13 +160,67 @@ + + org.apache.maven.plugins + maven-enforcer-plugin + 3.4.1 + + + enforce-java + + enforce + + + + + 21 + + + + + + org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.11.0 + true + ${jdkTarget} + ${jdkTarget} ${jdkTarget} + + -Xlint:-options + + + + default-compile + + + **/ffm/*.java + + + + + jdk-21 + + compile + + + 21 + + **/ffm/*.java + + + --enable-preview + + + + + default-testCompile + + org.apache.felix @@ -351,12 +405,12 @@ com.diffplug.spotless spotless-maven-plugin - 2.38.0 + 2.39.0 - 2.35.0 + 2.38.0 java|javax,org,,\# diff --git a/src/main/java/org/fusesource/jansi/AnsiConsole.java b/src/main/java/org/fusesource/jansi/AnsiConsole.java index 3b7b0032..c4ba530e 100644 --- a/src/main/java/org/fusesource/jansi/AnsiConsole.java +++ b/src/main/java/org/fusesource/jansi/AnsiConsole.java @@ -26,22 +26,9 @@ import java.nio.charset.UnsupportedCharsetException; import java.util.Locale; -import org.fusesource.jansi.internal.CLibrary; -import org.fusesource.jansi.internal.CLibrary.WinSize; -import org.fusesource.jansi.internal.Kernel32.CONSOLE_SCREEN_BUFFER_INFO; import org.fusesource.jansi.io.AnsiOutputStream; import org.fusesource.jansi.io.AnsiProcessor; import org.fusesource.jansi.io.FastBufferedOutputStream; -import org.fusesource.jansi.io.WindowsAnsiProcessor; - -import static org.fusesource.jansi.internal.CLibrary.ioctl; -import static org.fusesource.jansi.internal.CLibrary.isatty; -import static org.fusesource.jansi.internal.Kernel32.GetConsoleMode; -import static org.fusesource.jansi.internal.Kernel32.GetConsoleScreenBufferInfo; -import static org.fusesource.jansi.internal.Kernel32.GetStdHandle; -import static org.fusesource.jansi.internal.Kernel32.STD_ERROR_HANDLE; -import static org.fusesource.jansi.internal.Kernel32.STD_OUTPUT_HANDLE; -import static org.fusesource.jansi.internal.Kernel32.SetConsoleMode; /** * Provides consistent access to an ANSI aware console PrintStream or an ANSI codes stripping PrintStream @@ -167,6 +154,11 @@ public class AnsiConsole { */ public static final String JANSI_GRACEFUL = "jansi.graceful"; + public static final String JANSI_PROVIDERS = "jansi.providers"; + public static final String JANSI_PROVIDER_JNI = "jni"; + public static final String JANSI_PROVIDER_FFM = "ffm"; + public static final String JANSI_PROVIDERS_DEFAULT = JANSI_PROVIDER_FFM + "," + JANSI_PROVIDER_JNI; + /** * @deprecated this field will be made private in a future release, use {@link #sysOut()} instead */ @@ -246,9 +238,9 @@ private static AnsiPrintStream ansiStream(boolean stdout) { // the library can not be loaded on unsupported platforms final int fd = stdout ? STDOUT_FILENO : STDERR_FILENO; try { - // If we can detect that stdout is not a tty.. then setup - // to strip the ANSI sequences.. - isAtty = isatty(fd) != 0; + // If we can detect that stdout is not a tty, then setup + // to strip the ANSI sequences... + isAtty = getCLibrary().isTty(fd) != 0; String term = System.getenv("TERM"); String emacs = System.getenv("INSIDE_EMACS"); if (isAtty && "dumb".equals(term) && emacs != null && !emacs.contains("comint")) { @@ -274,25 +266,26 @@ private static AnsiPrintStream ansiStream(boolean stdout) { installer = uninstaller = null; width = new AnsiOutputStream.ZeroWidthSupplier(); } else if (IS_WINDOWS) { - final long console = GetStdHandle(stdout ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE); + final long console = getKernel32().getStdHandle(stdout); final int[] mode = new int[1]; - final boolean isConsole = GetConsoleMode(console, mode) != 0; - if (isConsole && SetConsoleMode(console, mode[0] | ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0) { - SetConsoleMode(console, mode[0]); // set it back for now, but we know it works + final boolean isConsole = getKernel32().getConsoleMode(console, mode) != 0; + if (isConsole && getKernel32().setConsoleMode(console, mode[0] | ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0) { + // set it back for now, but we know it works + getKernel32().setConsoleMode(console, mode[0]); processor = null; type = AnsiType.VirtualTerminal; installer = new AnsiOutputStream.IoRunnable() { @Override public void run() throws IOException { virtualProcessing++; - SetConsoleMode(console, mode[0] | ENABLE_VIRTUAL_TERMINAL_PROCESSING); + getKernel32().setConsoleMode(console, mode[0] | ENABLE_VIRTUAL_TERMINAL_PROCESSING); } }; uninstaller = new AnsiOutputStream.IoRunnable() { @Override public void run() throws IOException { if (--virtualProcessing == 0) { - SetConsoleMode(console, mode[0]); + getKernel32().setConsoleMode(console, mode[0]); } } }; @@ -308,7 +301,7 @@ public void run() throws IOException { AnsiProcessor proc; AnsiType ttype; try { - proc = new WindowsAnsiProcessor(out, console); + proc = getKernel32().newProcessor(out, console); ttype = AnsiType.Emulation; } catch (Throwable ignore) { // this happens when the stdout is being redirected to a file. @@ -320,14 +313,7 @@ public void run() throws IOException { type = ttype; installer = uninstaller = null; } - width = new AnsiOutputStream.WidthSupplier() { - @Override - public int getTerminalWidth() { - CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); - GetConsoleScreenBufferInfo(console, info); - return info.windowWidth(); - } - }; + width = () -> getKernel32().getTerminalWidth(console); } // We must be on some Unix variant... @@ -336,14 +322,7 @@ public int getTerminalWidth() { processor = null; type = AnsiType.Native; installer = uninstaller = null; - width = new AnsiOutputStream.WidthSupplier() { - @Override - public int getTerminalWidth() { - WinSize sz = new WinSize(); - ioctl(fd, CLibrary.TIOCGWINSZ, sz); - return sz.ws_col; - } - }; + width = () -> getCLibrary().getTerminalWidth(fd); } AnsiMode mode; @@ -553,4 +532,12 @@ static synchronized void initStreams() { initialized = true; } } + + private static AnsiConsoleSupport.Kernel32 getKernel32() { + return AnsiConsoleSupport.getInstance().getKernel32(); + } + + private static AnsiConsoleSupport.CLibrary getCLibrary() { + return AnsiConsoleSupport.getInstance().getCLibrary(); + } } diff --git a/src/main/java/org/fusesource/jansi/AnsiConsoleSupport.java b/src/main/java/org/fusesource/jansi/AnsiConsoleSupport.java new file mode 100644 index 00000000..02907c55 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/AnsiConsoleSupport.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2009-2023 the original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.fusesource.jansi; + +import java.io.IOException; +import java.io.OutputStream; + +import org.fusesource.jansi.io.AnsiProcessor; + +public interface AnsiConsoleSupport { + + interface CLibrary { + + int STDOUT_FILENO = 1; + int STDERR_FILENO = 2; + + short getTerminalWidth(int fd); + + int isTty(int fd); + } + + interface Kernel32 { + + int isTty(long console); + + int getTerminalWidth(long console); + + long getStdHandle(boolean stdout); + + int getConsoleMode(long console, int[] mode); + + int setConsoleMode(long console, int mode); + + int getLastError(); + + String getErrorMessage(int errorCode); + + AnsiProcessor newProcessor(OutputStream os, long console) throws IOException; + } + + String getProviderName(); + + CLibrary getCLibrary(); + + Kernel32 getKernel32(); + + static AnsiConsoleSupport getInstance() { + return AnsiConsoleSupportHolder.get(); + } +} diff --git a/src/main/java/org/fusesource/jansi/AnsiConsoleSupportHolder.java b/src/main/java/org/fusesource/jansi/AnsiConsoleSupportHolder.java new file mode 100644 index 00000000..082f78d0 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/AnsiConsoleSupportHolder.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2009-2023 the original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.fusesource.jansi; + +import org.fusesource.jansi.internal.AnsiConsoleSupportJni; + +import static org.fusesource.jansi.AnsiConsole.JANSI_PROVIDERS; +import static org.fusesource.jansi.AnsiConsole.JANSI_PROVIDERS_DEFAULT; +import static org.fusesource.jansi.AnsiConsole.JANSI_PROVIDER_FFM; +import static org.fusesource.jansi.AnsiConsole.JANSI_PROVIDER_JNI; + +class AnsiConsoleSupportHolder { + static volatile AnsiConsoleSupport instance; + + static AnsiConsoleSupport get() { + if (instance == null) { + synchronized (AnsiConsoleSupportHolder.class) { + if (instance == null) { + instance = doGet(); + } + } + } + return instance; + } + + static AnsiConsoleSupport doGet() { + RuntimeException error = new RuntimeException("Unable to create AnsiConsoleSupport provider"); + String[] providers = + System.getProperty(JANSI_PROVIDERS, JANSI_PROVIDERS_DEFAULT).split(","); + for (String provider : providers) { + try { + if (JANSI_PROVIDER_FFM.equals(provider)) { + return (AnsiConsoleSupport) AnsiConsoleSupport.class + .getClassLoader() + .loadClass("org.fusesource.jansi.ffm.AnsiConsoleSupportFfm") + .getConstructor() + .newInstance(); + } else if (JANSI_PROVIDER_JNI.equals(provider)) { + return new AnsiConsoleSupportJni(); + } + } catch (Throwable t) { + error.addSuppressed(t); + } + } + throw error; + } +} diff --git a/src/main/java/org/fusesource/jansi/AnsiMain.java b/src/main/java/org/fusesource/jansi/AnsiMain.java index c2a845d6..f36c7c52 100644 --- a/src/main/java/org/fusesource/jansi/AnsiMain.java +++ b/src/main/java/org/fusesource/jansi/AnsiMain.java @@ -26,7 +26,6 @@ import java.util.Properties; import org.fusesource.jansi.Ansi.Attribute; -import org.fusesource.jansi.internal.CLibrary; import org.fusesource.jansi.internal.JansiLoader; import static org.fusesource.jansi.Ansi.ansi; @@ -54,27 +53,34 @@ public static void main(String... args) throws IOException { System.out.println(); - // info on native library - System.out.println("library.jansi.path= " + System.getProperty("library.jansi.path", "")); - System.out.println("library.jansi.version= " + System.getProperty("library.jansi.version", "")); - boolean loaded = JansiLoader.initialize(); - if (loaded) { - System.out.println("Jansi native library loaded from " + JansiLoader.getNativeLibraryPath()); - if (JansiLoader.getNativeLibrarySourceUrl() != null) { - System.out.println(" which was auto-extracted from " + JansiLoader.getNativeLibrarySourceUrl()); - } - } else { - String prev = System.getProperty(AnsiConsole.JANSI_GRACEFUL); - try { - System.setProperty(AnsiConsole.JANSI_GRACEFUL, "false"); - JansiLoader.initialize(); - } catch (Throwable e) { - e.printStackTrace(System.out); - } finally { - if (prev != null) { - System.setProperty(AnsiConsole.JANSI_GRACEFUL, prev); - } else { - System.clearProperty(AnsiConsole.JANSI_GRACEFUL); + System.out.println("jansi.providers= " + + System.getProperty(AnsiConsole.JANSI_PROVIDERS, AnsiConsole.JANSI_PROVIDERS_DEFAULT)); + String provider = AnsiConsoleSupport.getInstance().getProviderName(); + System.out.println("Selected provider: " + provider); + + if (AnsiConsole.JANSI_PROVIDER_JNI.equals(provider)) { + // info on native library + System.out.println("library.jansi.path= " + System.getProperty("library.jansi.path", "")); + System.out.println("library.jansi.version= " + System.getProperty("library.jansi.version", "")); + boolean loaded = JansiLoader.initialize(); + if (loaded) { + System.out.println("Jansi native library loaded from " + JansiLoader.getNativeLibraryPath()); + if (JansiLoader.getNativeLibrarySourceUrl() != null) { + System.out.println(" which was auto-extracted from " + JansiLoader.getNativeLibrarySourceUrl()); + } + } else { + String prev = System.getProperty(AnsiConsole.JANSI_GRACEFUL); + try { + System.setProperty(AnsiConsole.JANSI_GRACEFUL, "false"); + JansiLoader.initialize(); + } catch (Throwable e) { + e.printStackTrace(System.out); + } finally { + if (prev != null) { + System.setProperty(AnsiConsole.JANSI_GRACEFUL, prev); + } else { + System.clearProperty(AnsiConsole.JANSI_GRACEFUL); + } } } } @@ -188,11 +194,21 @@ private static String getJansiVersion() { } private static void diagnoseTty(boolean stderr) { - int fd = stderr ? CLibrary.STDERR_FILENO : CLibrary.STDOUT_FILENO; - int isatty = CLibrary.LOADED ? CLibrary.isatty(fd) : 0; + int isatty; + int width; + if (AnsiConsole.IS_WINDOWS) { + long console = AnsiConsoleSupport.getInstance().getKernel32().getStdHandle(!stderr); + isatty = AnsiConsoleSupport.getInstance().getKernel32().isTty(console); + width = AnsiConsoleSupport.getInstance().getKernel32().getTerminalWidth(console); + } else { + int fd = stderr ? AnsiConsoleSupport.CLibrary.STDERR_FILENO : AnsiConsoleSupport.CLibrary.STDOUT_FILENO; + isatty = AnsiConsoleSupport.getInstance().getCLibrary().isTty(fd); + width = AnsiConsoleSupport.getInstance().getCLibrary().getTerminalWidth(fd); + } System.out.println("isatty(STD" + (stderr ? "ERR" : "OUT") + "_FILENO): " + isatty + ", System." + (stderr ? "err" : "out") + " " + ((isatty == 0) ? "is *NOT*" : "is") + " a terminal"); + System.out.println("width(STD" + (stderr ? "ERR" : "OUT") + "_FILENO): " + width); } private static void testAnsi(boolean stderr) { diff --git a/src/main/java/org/fusesource/jansi/WindowsSupport.java b/src/main/java/org/fusesource/jansi/WindowsSupport.java index 010f527e..e14854cd 100644 --- a/src/main/java/org/fusesource/jansi/WindowsSupport.java +++ b/src/main/java/org/fusesource/jansi/WindowsSupport.java @@ -15,27 +15,18 @@ */ package org.fusesource.jansi; -import java.io.UnsupportedEncodingException; - -import static org.fusesource.jansi.internal.Kernel32.FORMAT_MESSAGE_FROM_SYSTEM; -import static org.fusesource.jansi.internal.Kernel32.FormatMessageW; -import static org.fusesource.jansi.internal.Kernel32.GetLastError; - public class WindowsSupport { public static String getLastErrorMessage() { - int errorCode = GetLastError(); + int errorCode = getKernel32().getLastError(); return getErrorMessage(errorCode); } public static String getErrorMessage(int errorCode) { - int bufferSize = 160; - byte data[] = new byte[bufferSize]; - FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, 0, errorCode, 0, data, bufferSize, null); - try { - return new String(data, "UTF-16LE").trim(); - } catch (UnsupportedEncodingException e) { - throw new IllegalStateException(e); - } + return getKernel32().getErrorMessage(errorCode); + } + + private static AnsiConsoleSupport.Kernel32 getKernel32() { + return AnsiConsoleSupport.getInstance().getKernel32(); } } diff --git a/src/main/java/org/fusesource/jansi/ffm/AnsiConsoleSupportFfm.java b/src/main/java/org/fusesource/jansi/ffm/AnsiConsoleSupportFfm.java new file mode 100644 index 00000000..b1af763f --- /dev/null +++ b/src/main/java/org/fusesource/jansi/ffm/AnsiConsoleSupportFfm.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2009-2023 the original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.fusesource.jansi.ffm; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.GroupLayout; +import java.lang.foreign.Linker; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.VarHandle; + +import org.fusesource.jansi.AnsiConsoleSupport; +import org.fusesource.jansi.io.AnsiProcessor; + +import static org.fusesource.jansi.ffm.Kernel32.*; + +public class AnsiConsoleSupportFfm implements AnsiConsoleSupport { + static GroupLayout wsLayout; + static MethodHandle ioctl; + static VarHandle ws_col; + static MethodHandle isatty; + + static { + wsLayout = MemoryLayout.structLayout( + ValueLayout.JAVA_SHORT.withName("ws_row"), + ValueLayout.JAVA_SHORT.withName("ws_col"), + ValueLayout.JAVA_SHORT, + ValueLayout.JAVA_SHORT); + ws_col = wsLayout.varHandle(MemoryLayout.PathElement.groupElement("ws_col")); + Linker linker = Linker.nativeLinker(); + ioctl = linker.downcallHandle( + linker.defaultLookup().find("ioctl").get(), + FunctionDescriptor.of( + ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS), + Linker.Option.firstVariadicArg(2)); + isatty = linker.downcallHandle( + linker.defaultLookup().find("isatty").get(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT)); + } + + @Override + public String getProviderName() { + return "ffm"; + } + + @Override + public CLibrary getCLibrary() { + return new CLibrary() { + static final int TIOCGWINSZ; + + static { + String osName = System.getProperty("os.name"); + if (osName.startsWith("Linux")) { + String arch = System.getProperty("os.arch"); + boolean isMipsPpcOrSparc = + arch.startsWith("mips") || arch.startsWith("ppc") || arch.startsWith("sparc"); + TIOCGWINSZ = isMipsPpcOrSparc ? 0x40087468 : 0x00005413; + } else if (osName.startsWith("Solaris") || osName.startsWith("SunOS")) { + int _TIOC = ('T' << 8); + TIOCGWINSZ = (_TIOC | 104); + } else if (osName.startsWith("Mac") || osName.startsWith("Darwin")) { + TIOCGWINSZ = 0x40087468; + } else if (osName.startsWith("FreeBSD")) { + TIOCGWINSZ = 0x40087468; + } else { + throw new UnsupportedOperationException(); + } + } + + @Override + public short getTerminalWidth(int fd) { + MemorySegment segment = Arena.ofAuto().allocate(wsLayout); + try { + int res = (int) ioctl.invoke(fd, (long) TIOCGWINSZ, segment); + return (short) ws_col.get(segment); + } catch (Throwable e) { + throw new RuntimeException("Unable to ioctl(TIOCGWINSZ)", e); + } + } + + @Override + public int isTty(int fd) { + try { + return (int) isatty.invoke(fd); + } catch (Throwable e) { + throw new RuntimeException("Unable to call isatty", e); + } + } + }; + } + + @Override + public Kernel32 getKernel32() { + return new Kernel32() { + @Override + public int isTty(long console) { + int[] mode = new int[1]; + return getConsoleMode(console, mode); + } + + @Override + public int getTerminalWidth(long console) { + CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); + GetConsoleScreenBufferInfo(MemorySegment.ofAddress(console), info); + return info.windowWidth(); + } + + @Override + public long getStdHandle(boolean stdout) { + return GetStdHandle(stdout ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE) + .address(); + } + + @Override + public int getConsoleMode(long console, int[] mode) { + try (Arena session = Arena.ofConfined()) { + MemorySegment written = session.allocate(ValueLayout.JAVA_INT); + int res = GetConsoleMode(MemorySegment.ofAddress(console), written); + mode[0] = written.getAtIndex(ValueLayout.JAVA_INT, 0); + return res; + } + } + + @Override + public int setConsoleMode(long console, int mode) { + return SetConsoleMode(MemorySegment.ofAddress(console), mode); + } + + @Override + public int getLastError() { + return GetLastError(); + } + + @Override + public String getErrorMessage(int errorCode) { + int bufferSize = 160; + MemorySegment data = Arena.ofAuto().allocate(bufferSize); + FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, null, errorCode, 0, data, bufferSize, null); + return data.getUtf8String(0).trim(); + } + + @Override + public AnsiProcessor newProcessor(OutputStream os, long console) throws IOException { + return new WindowsAnsiProcessor(os, MemorySegment.ofAddress(console)); + } + }; + } +} diff --git a/src/main/java/org/fusesource/jansi/ffm/Kernel32.java b/src/main/java/org/fusesource/jansi/ffm/Kernel32.java new file mode 100644 index 00000000..fc17db68 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/ffm/Kernel32.java @@ -0,0 +1,858 @@ +/* + * Copyright (C) 2009-2023 the original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.fusesource.jansi.ffm; + +import java.io.IOException; +import java.lang.foreign.AddressLayout; +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.GroupLayout; +import java.lang.foreign.Linker; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.SymbolLookup; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.VarHandle; +import java.util.Objects; + +import static java.lang.foreign.ValueLayout.JAVA_INT; +import static java.lang.foreign.ValueLayout.OfBoolean; +import static java.lang.foreign.ValueLayout.OfByte; +import static java.lang.foreign.ValueLayout.OfChar; +import static java.lang.foreign.ValueLayout.OfDouble; +import static java.lang.foreign.ValueLayout.OfFloat; +import static java.lang.foreign.ValueLayout.OfInt; +import static java.lang.foreign.ValueLayout.OfLong; +import static java.lang.foreign.ValueLayout.OfShort; + +@SuppressWarnings({"unused", "CopyConstructorMissesField"}) +class Kernel32 { + + public static final int FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000; + + public static final int INVALID_HANDLE_VALUE = -1; + public static final int STD_INPUT_HANDLE = -10; + public static final int STD_OUTPUT_HANDLE = -11; + public static final int STD_ERROR_HANDLE = -12; + + public static final int ENABLE_PROCESSED_INPUT = 0x0001; + public static final int ENABLE_LINE_INPUT = 0x0002; + public static final int ENABLE_ECHO_INPUT = 0x0004; + public static final int ENABLE_WINDOW_INPUT = 0x0008; + public static final int ENABLE_MOUSE_INPUT = 0x0010; + public static final int ENABLE_INSERT_MODE = 0x0020; + public static final int ENABLE_QUICK_EDIT_MODE = 0x0040; + public static final int ENABLE_EXTENDED_FLAGS = 0x0080; + + public static final int RIGHT_ALT_PRESSED = 0x0001; + public static final int LEFT_ALT_PRESSED = 0x0002; + public static final int RIGHT_CTRL_PRESSED = 0x0004; + public static final int LEFT_CTRL_PRESSED = 0x0008; + public static final int SHIFT_PRESSED = 0x0010; + + public static final int FOREGROUND_BLUE = 0x0001; + public static final int FOREGROUND_GREEN = 0x0002; + public static final int FOREGROUND_RED = 0x0004; + public static final int FOREGROUND_INTENSITY = 0x0008; + public static final int BACKGROUND_BLUE = 0x0010; + public static final int BACKGROUND_GREEN = 0x0020; + public static final int BACKGROUND_RED = 0x0040; + public static final int BACKGROUND_INTENSITY = 0x0080; + + // Button state + public static final int FROM_LEFT_1ST_BUTTON_PRESSED = 0x0001; + public static final int RIGHTMOST_BUTTON_PRESSED = 0x0002; + public static final int FROM_LEFT_2ND_BUTTON_PRESSED = 0x0004; + public static final int FROM_LEFT_3RD_BUTTON_PRESSED = 0x0008; + public static final int FROM_LEFT_4TH_BUTTON_PRESSED = 0x0010; + + // Event flags + public static final int MOUSE_MOVED = 0x0001; + public static final int DOUBLE_CLICK = 0x0002; + public static final int MOUSE_WHEELED = 0x0004; + public static final int MOUSE_HWHEELED = 0x0008; + + // Event types + public static final short KEY_EVENT = 0x0001; + public static final short MOUSE_EVENT = 0x0002; + public static final short WINDOW_BUFFER_SIZE_EVENT = 0x0004; + public static final short MENU_EVENT = 0x0008; + public static final short FOCUS_EVENT = 0x0010; + + public static int WaitForSingleObject(MemorySegment hHandle, int dwMilliseconds) { + MethodHandle mh$ = requireNonNull(WaitForSingleObject$MH, "WaitForSingleObject"); + try { + return (int) mh$.invokeExact(hHandle, dwMilliseconds); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static MemorySegment GetStdHandle(int nStdHandle) { + MethodHandle mh$ = requireNonNull(GetStdHandle$MH, "GetStdHandle"); + try { + return MemorySegment.ofAddress((long) mh$.invokeExact(nStdHandle)); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int FormatMessageW( + int dwFlags, + MemorySegment lpSource, + int dwMessageId, + int dwLanguageId, + MemorySegment lpBuffer, + int nSize, + MemorySegment Arguments) { + MethodHandle mh$ = requireNonNull(FormatMessageW$MH, "FormatMessageW"); + try { + return (int) mh$.invokeExact( + dwFlags, + lpSource.address(), + dwMessageId, + dwLanguageId, + lpBuffer.address(), + nSize, + Arguments.address()); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int SetConsoleTextAttribute(MemorySegment hConsoleOutput, short wAttributes) { + MethodHandle mh$ = requireNonNull(SetConsoleTextAttribute$MH, "SetConsoleTextAttribute"); + try { + return (int) mh$.invokeExact(hConsoleOutput, wAttributes); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int SetConsoleMode(MemorySegment hConsoleHandle, int dwMode) { + MethodHandle mh$ = requireNonNull(SetConsoleMode$MH, "SetConsoleMode"); + try { + return (int) mh$.invokeExact(hConsoleHandle.address(), dwMode); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int GetConsoleMode(MemorySegment hConsoleHandle, MemorySegment lpMode) { + MethodHandle mh$ = requireNonNull(GetConsoleMode$MH, "GetConsoleMode"); + try { + return (int) mh$.invokeExact(hConsoleHandle.address(), lpMode.address()); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int SetConsoleTitleW(MemorySegment lpConsoleTitle) { + MethodHandle mh$ = requireNonNull(SetConsoleTitleW$MH, "SetConsoleTitleW"); + try { + return (int) mh$.invokeExact(lpConsoleTitle.address()); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int SetConsoleCursorPosition(MemorySegment hConsoleOutput, COORD dwCursorPosition) { + MethodHandle mh$ = requireNonNull(SetConsoleCursorPosition$MH, "SetConsoleCursorPosition"); + try { + return (int) mh$.invokeExact(hConsoleOutput, dwCursorPosition.seg); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int FillConsoleOutputCharacterW( + MemorySegment hConsoleOutput, + char cCharacter, + int nLength, + COORD dwWriteCoord, + MemorySegment lpNumberOfCharsWritten) { + MethodHandle mh$ = requireNonNull(FillConsoleOutputCharacterW$MH, "FillConsoleOutputCharacterW"); + try { + return (int) mh$.invokeExact( + hConsoleOutput.address(), cCharacter, nLength, dwWriteCoord.seg, lpNumberOfCharsWritten.address()); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int FillConsoleOutputAttribute( + MemorySegment hConsoleOutput, + short wAttribute, + int nLength, + COORD dwWriteCoord, + MemorySegment lpNumberOfAttrsWritten) { + MethodHandle mh$ = requireNonNull(FillConsoleOutputAttribute$MH, "FillConsoleOutputAttribute"); + try { + return (int) mh$.invokeExact( + hConsoleOutput, wAttribute, nLength, dwWriteCoord.seg, lpNumberOfAttrsWritten.address()); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int WriteConsoleW( + MemorySegment hConsoleOutput, + MemorySegment lpBuffer, + int nNumberOfCharsToWrite, + MemorySegment lpNumberOfCharsWritten, + MemorySegment lpReserved) { + MethodHandle mh$ = requireNonNull(WriteConsoleW$MH, "WriteConsoleW"); + try { + return (int) mh$.invokeExact( + hConsoleOutput, lpBuffer, nNumberOfCharsToWrite, lpNumberOfCharsWritten, lpReserved); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int ReadConsoleInputW( + MemorySegment hConsoleInput, MemorySegment lpBuffer, int nLength, MemorySegment lpNumberOfEventsRead) { + MethodHandle mh$ = requireNonNull(ReadConsoleInputW$MH, "ReadConsoleInputW"); + try { + return (int) mh$.invokeExact( + hConsoleInput.address(), lpBuffer.address(), nLength, lpNumberOfEventsRead.address()); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int PeekConsoleInputW( + MemorySegment hConsoleInput, MemorySegment lpBuffer, int nLength, MemorySegment lpNumberOfEventsRead) { + MethodHandle mh$ = requireNonNull(PeekConsoleInputW$MH, "PeekConsoleInputW"); + try { + return (int) mh$.invokeExact( + hConsoleInput.address(), lpBuffer.address(), nLength, lpNumberOfEventsRead.address()); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int GetConsoleScreenBufferInfo( + MemorySegment hConsoleOutput, CONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo) { + MethodHandle mh$ = requireNonNull(GetConsoleScreenBufferInfo$MH, "GetConsoleScreenBufferInfo"); + try { + return (int) mh$.invokeExact(hConsoleOutput.address(), lpConsoleScreenBufferInfo.seg); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int ScrollConsoleScreenBuffer( + MemorySegment hConsoleOutput, + SMALL_RECT lpScrollRectangle, + SMALL_RECT lpClipRectangle, + COORD dwDestinationOrigin, + CHAR_INFO lpFill) { + MethodHandle mh$ = requireNonNull(ScrollConsoleScreenBuffer$MH, "ScrollConsoleScreenBuffer"); + try { + return (int) + mh$.invokeExact(hConsoleOutput, lpScrollRectangle, lpClipRectangle, dwDestinationOrigin, lpFill); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int GetLastError(Object... x0) { + MethodHandle mh$ = requireNonNull(GetLastError$MH, "GetLastError"); + try { + return (int) mh$.invokeExact(x0); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static INPUT_RECORD[] readConsoleInputHelper(MemorySegment handle, int count, boolean peek) + throws IOException { + try (Arena session = Arena.ofConfined()) { + MemorySegment inputRecordPtr = session.allocateArray(INPUT_RECORD.LAYOUT, count); + MemorySegment length = session.allocate(JAVA_INT, 0); + int res = peek + ? PeekConsoleInputW(handle, inputRecordPtr, count, length) + : ReadConsoleInputW(handle, inputRecordPtr, count, length); + if (res == 0) { + throw new IOException("ReadConsoleInputW failed: " + getLastErrorMessage()); + } + int len = length.get(JAVA_INT, 0); + return inputRecordPtr + .elements(INPUT_RECORD.LAYOUT) + .map(INPUT_RECORD::new) + .limit(len) + .toArray(INPUT_RECORD[]::new); + } + } + + public static String getLastErrorMessage() { + int errorCode = GetLastError(); + return getErrorMessage(errorCode); + } + + public static String getErrorMessage(int errorCode) { + int bufferSize = 160; + MemorySegment data = Arena.ofAuto().allocate(bufferSize); + FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, null, errorCode, 0, data, bufferSize, null); + return data.getUtf8String(0).trim(); + } + + static final OfBoolean C_BOOL$LAYOUT = ValueLayout.JAVA_BOOLEAN; + static final OfByte C_CHAR$LAYOUT = ValueLayout.JAVA_BYTE; + static final OfChar C_WCHAR$LAYOUT = ValueLayout.JAVA_CHAR.withByteAlignment(16); + static final OfShort C_SHORT$LAYOUT = ValueLayout.JAVA_SHORT.withByteAlignment(16); + static final OfShort C_WORD$LAYOUT = ValueLayout.JAVA_SHORT.withByteAlignment(16); + static final OfInt C_DWORD$LAYOUT = ValueLayout.JAVA_INT.withByteAlignment(32); + static final OfInt C_INT$LAYOUT = JAVA_INT.withByteAlignment(32); + static final OfLong C_LONG$LAYOUT = ValueLayout.JAVA_LONG.withByteAlignment(64); + static final OfLong C_LONG_LONG$LAYOUT = ValueLayout.JAVA_LONG.withByteAlignment(64); + static final OfFloat C_FLOAT$LAYOUT = ValueLayout.JAVA_FLOAT.withByteAlignment(32); + static final OfDouble C_DOUBLE$LAYOUT = ValueLayout.JAVA_DOUBLE.withByteAlignment(64); + static final AddressLayout C_POINTER$LAYOUT = ValueLayout.ADDRESS.withByteAlignment(64); + + static final MethodHandle WaitForSingleObject$MH = + downcallHandle("WaitForSingleObject", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT)); + static final MethodHandle GetStdHandle$MH = + downcallHandle("GetStdHandle", FunctionDescriptor.of(C_POINTER$LAYOUT, C_INT$LAYOUT)); + static final MethodHandle FormatMessageW$MH = downcallHandle( + "FormatMessageW", + FunctionDescriptor.of( + C_INT$LAYOUT, + C_INT$LAYOUT, + C_POINTER$LAYOUT, + C_INT$LAYOUT, + C_INT$LAYOUT, + C_POINTER$LAYOUT, + C_INT$LAYOUT, + C_POINTER$LAYOUT)); + static final MethodHandle SetConsoleTextAttribute$MH = downcallHandle( + "SetConsoleTextAttribute", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_SHORT$LAYOUT)); + static final MethodHandle SetConsoleMode$MH = + downcallHandle("SetConsoleMode", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT)); + static final MethodHandle GetConsoleMode$MH = + downcallHandle("GetConsoleMode", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT)); + + static final MethodHandle SetConsoleTitleW$MH = + downcallHandle("SetConsoleTitleW", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT)); + static final MethodHandle SetConsoleCursorPosition$MH = downcallHandle( + "SetConsoleCursorPosition", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, COORD.LAYOUT)); + static final MethodHandle FillConsoleOutputCharacterW$MH = downcallHandle( + "FillConsoleOutputCharacterW", + FunctionDescriptor.of( + C_INT$LAYOUT, C_POINTER$LAYOUT, C_SHORT$LAYOUT, C_INT$LAYOUT, COORD.LAYOUT, C_POINTER$LAYOUT)); + static final MethodHandle FillConsoleOutputAttribute$MH = downcallHandle( + "FillConsoleOutputAttribute", + FunctionDescriptor.of( + C_INT$LAYOUT, C_POINTER$LAYOUT, C_SHORT$LAYOUT, C_INT$LAYOUT, COORD.LAYOUT, C_POINTER$LAYOUT)); + static final MethodHandle WriteConsoleW$MH = downcallHandle( + "WriteConsoleW", + FunctionDescriptor.of( + C_INT$LAYOUT, + C_POINTER$LAYOUT, + C_POINTER$LAYOUT, + C_INT$LAYOUT, + C_POINTER$LAYOUT, + C_POINTER$LAYOUT)); + + static final MethodHandle ReadConsoleInputW$MH = downcallHandle( + "ReadConsoleInputW", + FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT, C_POINTER$LAYOUT)); + static final MethodHandle PeekConsoleInputW$MH = downcallHandle( + "PeekConsoleInputW", + FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT, C_POINTER$LAYOUT)); + + static final MethodHandle GetConsoleScreenBufferInfo$MH = downcallHandle( + "GetConsoleScreenBufferInfo", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT)); + + static final MethodHandle ScrollConsoleScreenBuffer$MH = downcallHandle( + "ScrollConsoleScreenBuffer", + FunctionDescriptor.of( + C_INT$LAYOUT, + C_POINTER$LAYOUT, + C_POINTER$LAYOUT, + C_POINTER$LAYOUT, + COORD.LAYOUT, + C_POINTER$LAYOUT)); + static final MethodHandle GetLastError$MH = downcallHandle("GetLastError", FunctionDescriptor.of(C_INT$LAYOUT)); + + public static class INPUT_RECORD { + static final MemoryLayout LAYOUT = MemoryLayout.structLayout( + ValueLayout.JAVA_SHORT.withName("EventType"), + MemoryLayout.unionLayout( + KEY_EVENT_RECORD.LAYOUT.withName("KeyEvent"), + MOUSE_EVENT_RECORD.LAYOUT.withName("MouseEvent"), + WINDOW_BUFFER_SIZE_RECORD.LAYOUT.withName("WindowBufferSizeEvent"), + MENU_EVENT_RECORD.LAYOUT.withName("MenuEvent"), + FOCUS_EVENT_RECORD.LAYOUT.withName("FocusEvent")) + .withName("Event")); + static final VarHandle EventType$VH = varHandle(LAYOUT, "EventType"); + static final long Event$OFFSET = byteOffset(LAYOUT, "Event"); + + private final MemorySegment seg; + + public INPUT_RECORD() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + INPUT_RECORD(MemorySegment seg) { + this.seg = seg; + } + + public short eventType() { + return (short) EventType$VH.get(seg); + } + + public KEY_EVENT_RECORD keyEvent() { + return new KEY_EVENT_RECORD(seg, Event$OFFSET); + } + + public MOUSE_EVENT_RECORD mouseEvent() { + return new MOUSE_EVENT_RECORD(seg, Event$OFFSET); + } + + public FOCUS_EVENT_RECORD focusEvent() { + return new FOCUS_EVENT_RECORD(seg, Event$OFFSET); + } + } + + public static class MENU_EVENT_RECORD { + + static final GroupLayout LAYOUT = MemoryLayout.structLayout(C_DWORD$LAYOUT.withName("dwCommandId")); + static final VarHandle COMMAND_ID = varHandle(LAYOUT, "dwCommandId"); + + private final MemorySegment seg; + + public MENU_EVENT_RECORD() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + MENU_EVENT_RECORD(MemorySegment seg) { + this.seg = seg; + } + + public int commandId() { + return (int) MENU_EVENT_RECORD.COMMAND_ID.get(seg); + } + + public void commandId(int commandId) { + MENU_EVENT_RECORD.COMMAND_ID.set(seg, commandId); + } + } + + public static class FOCUS_EVENT_RECORD { + + static final GroupLayout LAYOUT = MemoryLayout.structLayout(C_BOOL$LAYOUT.withName("bSetFocus")); + static final VarHandle SET_FOCUS = varHandle(LAYOUT, "bSetFocus"); + + private final MemorySegment seg; + + public FOCUS_EVENT_RECORD() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + FOCUS_EVENT_RECORD(MemorySegment seg) { + this.seg = Objects.requireNonNull(seg); + } + + FOCUS_EVENT_RECORD(MemorySegment seg, long offset) { + this.seg = Objects.requireNonNull(seg).asSlice(offset, LAYOUT.byteSize()); + } + + public boolean setFocus() { + return (boolean) FOCUS_EVENT_RECORD.SET_FOCUS.get(seg); + } + + public void setFocus(boolean setFocus) { + FOCUS_EVENT_RECORD.SET_FOCUS.set(seg, setFocus); + } + } + + public static class WINDOW_BUFFER_SIZE_RECORD { + + static final GroupLayout LAYOUT = MemoryLayout.structLayout(COORD.LAYOUT.withName("size")); + static final long SIZE_OFFSET = byteOffset(LAYOUT, "size"); + + private final MemorySegment seg; + + public WINDOW_BUFFER_SIZE_RECORD() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + WINDOW_BUFFER_SIZE_RECORD(MemorySegment seg) { + this.seg = seg; + } + + public COORD size() { + return new COORD(seg, SIZE_OFFSET); + } + + public String toString() { + return "WINDOW_BUFFER_SIZE_RECORD{size=" + this.size() + '}'; + } + } + + public static class MOUSE_EVENT_RECORD { + + private static final MemoryLayout LAYOUT = MemoryLayout.structLayout( + COORD.LAYOUT.withName("dwMousePosition"), + C_DWORD$LAYOUT.withName("dwButtonState"), + C_DWORD$LAYOUT.withName("dwControlKeyState"), + C_DWORD$LAYOUT.withName("dwEventFlags")); + private static final long MOUSE_POSITION_OFFSET = byteOffset(LAYOUT, "dwMousePosition"); + private static final VarHandle BUTTON_STATE = varHandle(LAYOUT, "dwButtonState"); + private static final VarHandle CONTROL_KEY_STATE = varHandle(LAYOUT, "dwControlKeyState"); + private static final VarHandle EVENT_FLAGS = varHandle(LAYOUT, "dwEventFlags"); + + private final MemorySegment seg; + + public MOUSE_EVENT_RECORD() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + MOUSE_EVENT_RECORD(MemorySegment seg) { + this.seg = Objects.requireNonNull(seg); + } + + MOUSE_EVENT_RECORD(MemorySegment seg, long offset) { + this.seg = Objects.requireNonNull(seg).asSlice(offset, LAYOUT.byteSize()); + } + + public COORD mousePosition() { + return new COORD(seg, MOUSE_POSITION_OFFSET); + } + + public int buttonState() { + return (int) BUTTON_STATE.get(seg); + } + + public int controlKeyState() { + return (int) CONTROL_KEY_STATE.get(seg); + } + + public int eventFlags() { + return (int) EVENT_FLAGS.get(seg); + } + + public String toString() { + return "MOUSE_EVENT_RECORD{mousePosition=" + mousePosition() + ", buttonState=" + buttonState() + + ", controlKeyState=" + controlKeyState() + ", eventFlags=" + eventFlags() + '}'; + } + } + + public static class KEY_EVENT_RECORD { + + static final MemoryLayout LAYOUT = MemoryLayout.structLayout( + JAVA_INT.withName("bKeyDown"), + ValueLayout.JAVA_SHORT.withName("wRepeatCount"), + ValueLayout.JAVA_SHORT.withName("wVirtualKeyCode"), + ValueLayout.JAVA_SHORT.withName("wVirtualScanCode"), + MemoryLayout.unionLayout( + ValueLayout.JAVA_CHAR.withName("UnicodeChar"), + ValueLayout.JAVA_BYTE.withName("AsciiChar")) + .withName("uChar"), + JAVA_INT.withName("dwControlKeyState")); + static final VarHandle bKeyDown$VH = varHandle(LAYOUT, "bKeyDown"); + static final VarHandle wRepeatCount$VH = varHandle(LAYOUT, "wRepeatCount"); + static final VarHandle wVirtualKeyCode$VH = varHandle(LAYOUT, "wVirtualKeyCode"); + static final VarHandle wVirtualScanCode$VH = varHandle(LAYOUT, "wVirtualScanCode"); + static final VarHandle UnicodeChar$VH = varHandle(LAYOUT, "uChar", "UnicodeChar"); + static final VarHandle AsciiChar$VH = varHandle(LAYOUT, "uChar", "AsciiChar"); + static final VarHandle dwControlKeyState$VH = varHandle(LAYOUT, "dwControlKeyState"); + + final MemorySegment seg; + + public KEY_EVENT_RECORD() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + KEY_EVENT_RECORD(MemorySegment seg) { + this.seg = seg; + } + + KEY_EVENT_RECORD(MemorySegment seg, long offset) { + this.seg = Objects.requireNonNull(seg).asSlice(offset, LAYOUT.byteSize()); + } + + public boolean keyDown() { + return (boolean) bKeyDown$VH.get(seg); + } + + public int repeatCount() { + return (int) wRepeatCount$VH.get(seg); + } + + public short keyCode() { + return (short) wVirtualKeyCode$VH.get(seg); + } + + public short scanCode() { + return (short) wVirtualScanCode$VH.get(seg); + } + + public char uchar() { + return (char) UnicodeChar$VH.get(seg); + } + + public int controlKeyState() { + return (int) dwControlKeyState$VH.get(seg); + } + + public String toString() { + return "KEY_EVENT_RECORD{keyDown=" + this.keyDown() + ", repeatCount=" + this.repeatCount() + ", keyCode=" + + this.keyCode() + ", scanCode=" + this.scanCode() + ", uchar=" + this.uchar() + + ", controlKeyState=" + + this.controlKeyState() + '}'; + } + } + + public static class CHAR_INFO { + + static final GroupLayout LAYOUT = MemoryLayout.structLayout( + MemoryLayout.unionLayout(C_WCHAR$LAYOUT.withName("UnicodeChar"), C_CHAR$LAYOUT.withName("AsciiChar")) + .withName("Char"), + C_WORD$LAYOUT.withName("Attributes")); + static final VarHandle UnicodeChar$VH = varHandle(LAYOUT, "Char", "UnicodeChar"); + static final VarHandle Attributes$VH = varHandle(LAYOUT, "Attributes"); + + final MemorySegment seg; + + public CHAR_INFO() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + public CHAR_INFO(char c, short a) { + this(); + UnicodeChar$VH.set(seg, c); + Attributes$VH.set(seg, a); + } + + CHAR_INFO(MemorySegment seg) { + this.seg = seg; + } + + public char unicodeChar() { + return (char) UnicodeChar$VH.get(seg); + } + } + + public static class CONSOLE_SCREEN_BUFFER_INFO { + static final GroupLayout LAYOUT = MemoryLayout.structLayout( + COORD.LAYOUT.withName("dwSize"), + COORD.LAYOUT.withName("dwCursorPosition"), + C_WORD$LAYOUT.withName("wAttributes"), + SMALL_RECT.LAYOUT.withName("srWindow"), + COORD.LAYOUT.withName("dwMaximumWindowSize")); + static final long dwSize$OFFSET = byteOffset(LAYOUT, "dwSize"); + static final long dwCursorPosition$OFFSET = byteOffset(LAYOUT, "dwCursorPosition"); + static final VarHandle wAttributes$VH = varHandle(LAYOUT, "wAttributes"); + static final long srWindow$OFFSET = byteOffset(LAYOUT, "srWindow"); + + private final MemorySegment seg; + + public CONSOLE_SCREEN_BUFFER_INFO() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + CONSOLE_SCREEN_BUFFER_INFO(MemorySegment seg) { + this.seg = seg; + } + + public COORD size() { + return new COORD(seg, dwSize$OFFSET); + } + + public COORD cursorPosition() { + return new COORD(seg, dwCursorPosition$OFFSET); + } + + public short attributes() { + return (short) wAttributes$VH.get(seg); + } + + public SMALL_RECT window() { + return new SMALL_RECT(seg, srWindow$OFFSET); + } + + public int windowWidth() { + return this.window().width() + 1; + } + + public int windowHeight() { + return this.window().height() + 1; + } + + public void attributes(short attr) { + wAttributes$VH.set(seg, attr); + } + } + + public static class COORD { + + static final GroupLayout LAYOUT = + MemoryLayout.structLayout(C_SHORT$LAYOUT.withName("x"), C_SHORT$LAYOUT.withName("y")); + static final VarHandle x$VH = varHandle(LAYOUT, "x"); + static final VarHandle y$VH = varHandle(LAYOUT, "y"); + + private final MemorySegment seg; + + public COORD() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + public COORD(short x, short y) { + this(Arena.ofAuto().allocate(LAYOUT)); + x(x); + y(y); + } + + public COORD(COORD from) { + this(Arena.ofAuto().allocate(LAYOUT).copyFrom(Objects.requireNonNull(from).seg)); + } + + COORD(MemorySegment seg) { + this.seg = seg; + } + + COORD(MemorySegment seg, long offset) { + this.seg = Objects.requireNonNull(seg).asSlice(offset, LAYOUT.byteSize()); + } + + public short x() { + return (short) COORD.x$VH.get(seg); + } + + public void x(short x) { + COORD.x$VH.set(seg, x); + } + + public short y() { + return (short) COORD.y$VH.get(seg); + } + + public void y(short y) { + COORD.y$VH.set(seg, y); + } + + public COORD copy() { + return new COORD(this); + } + } + + public static class SMALL_RECT { + + static final GroupLayout LAYOUT = MemoryLayout.structLayout( + C_SHORT$LAYOUT.withName("Left"), + C_SHORT$LAYOUT.withName("Top"), + C_SHORT$LAYOUT.withName("Right"), + C_SHORT$LAYOUT.withName("Bottom")); + static final VarHandle Left$VH = varHandle(LAYOUT, "Left"); + static final VarHandle Top$VH = varHandle(LAYOUT, "Top"); + static final VarHandle Right$VH = varHandle(LAYOUT, "Right"); + static final VarHandle Bottom$VH = varHandle(LAYOUT, "Bottom"); + + private final MemorySegment seg; + + public SMALL_RECT() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + public SMALL_RECT(SMALL_RECT from) { + this(Arena.ofAuto().allocate(LAYOUT).copyFrom(from.seg)); + } + + SMALL_RECT(MemorySegment seg, long offset) { + this(seg.asSlice(offset, LAYOUT.byteSize())); + } + + SMALL_RECT(MemorySegment seg) { + this.seg = seg; + } + + public short left() { + return (short) Left$VH.get(seg); + } + + public short top() { + return (short) Top$VH.get(seg); + } + + public short right() { + return (short) Right$VH.get(seg); + } + + public short bottom() { + return (short) Bottom$VH.get(seg); + } + + public short width() { + return (short) (this.right() - this.left()); + } + + public short height() { + return (short) (this.bottom() - this.top()); + } + + public void left(short l) { + Left$VH.set(seg, l); + } + + public void top(short t) { + Top$VH.set(seg, t); + } + + public SMALL_RECT copy() { + return new SMALL_RECT(this); + } + } + + private static final Linker LINKER = Linker.nativeLinker(); + + private static final SymbolLookup SYMBOL_LOOKUP; + + static { + SymbolLookup loaderLookup = SymbolLookup.loaderLookup(); + SYMBOL_LOOKUP = + name -> loaderLookup.find(name).or(() -> LINKER.defaultLookup().find(name)); + } + + static MethodHandle downcallHandle(String name, FunctionDescriptor fdesc) { + return SYMBOL_LOOKUP + .find(name) + .map(addr -> LINKER.downcallHandle(addr, fdesc)) + .orElse(null); + } + + static T requireNonNull(T obj, String symbolName) { + if (obj == null) { + throw new UnsatisfiedLinkError("unresolved symbol: " + symbolName); + } + return obj; + } + + static VarHandle varHandle(MemoryLayout layout, String e1) { + return layout.varHandle(MemoryLayout.PathElement.groupElement(e1)); + } + + static VarHandle varHandle(MemoryLayout layout, String e1, String e2) { + return layout.varHandle(MemoryLayout.PathElement.groupElement(e1), MemoryLayout.PathElement.groupElement(e2)); + } + + static long byteOffset(MemoryLayout layout, String e1) { + return layout.byteOffset(MemoryLayout.PathElement.groupElement(e1)); + } +} diff --git a/src/main/java/org/fusesource/jansi/ffm/WindowsAnsiProcessor.java b/src/main/java/org/fusesource/jansi/ffm/WindowsAnsiProcessor.java new file mode 100644 index 00000000..25d20030 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/ffm/WindowsAnsiProcessor.java @@ -0,0 +1,439 @@ +/* + * Copyright (C) 2009-2023 the original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.fusesource.jansi.ffm; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; + +import org.fusesource.jansi.WindowsSupport; +import org.fusesource.jansi.io.AnsiProcessor; +import org.fusesource.jansi.io.Colors; + +import static org.fusesource.jansi.ffm.Kernel32.*; + +/** + * A Windows ANSI escape processor, that uses JNA to access native platform + * API's to change the console attributes (see + * Jansi native Kernel32). + *

The native library used is named jansi and is loaded using HawtJNI Runtime + * Library + * + * @since 1.19 + * @author Hiram Chirino + * @author Joris Kuipers + */ +public class WindowsAnsiProcessor extends AnsiProcessor { + + private final MemorySegment console; + + private static final short FOREGROUND_BLACK = 0; + private static final short FOREGROUND_YELLOW = (short) (FOREGROUND_RED | FOREGROUND_GREEN); + private static final short FOREGROUND_MAGENTA = (short) (FOREGROUND_BLUE | FOREGROUND_RED); + private static final short FOREGROUND_CYAN = (short) (FOREGROUND_BLUE | FOREGROUND_GREEN); + private static final short FOREGROUND_WHITE = (short) (FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE); + + private static final short BACKGROUND_BLACK = 0; + private static final short BACKGROUND_YELLOW = (short) (BACKGROUND_RED | BACKGROUND_GREEN); + private static final short BACKGROUND_MAGENTA = (short) (BACKGROUND_BLUE | BACKGROUND_RED); + private static final short BACKGROUND_CYAN = (short) (BACKGROUND_BLUE | BACKGROUND_GREEN); + private static final short BACKGROUND_WHITE = (short) (BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE); + + private static final short[] ANSI_FOREGROUND_COLOR_MAP = { + FOREGROUND_BLACK, + FOREGROUND_RED, + FOREGROUND_GREEN, + FOREGROUND_YELLOW, + FOREGROUND_BLUE, + FOREGROUND_MAGENTA, + FOREGROUND_CYAN, + FOREGROUND_WHITE, + }; + + private static final short[] ANSI_BACKGROUND_COLOR_MAP = { + BACKGROUND_BLACK, + BACKGROUND_RED, + BACKGROUND_GREEN, + BACKGROUND_YELLOW, + BACKGROUND_BLUE, + BACKGROUND_MAGENTA, + BACKGROUND_CYAN, + BACKGROUND_WHITE, + }; + + private final CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); + private final short originalColors; + + private boolean negative; + private short savedX = -1; + private short savedY = -1; + + public WindowsAnsiProcessor(OutputStream ps, MemorySegment console) throws IOException { + super(ps); + this.console = console; + getConsoleInfo(); + originalColors = info.attributes(); + } + + public WindowsAnsiProcessor(OutputStream ps, boolean stdout) throws IOException { + this(ps, GetStdHandle(stdout ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE)); + } + + public WindowsAnsiProcessor(OutputStream ps) throws IOException { + this(ps, true); + } + + private void getConsoleInfo() throws IOException { + os.flush(); + if (GetConsoleScreenBufferInfo(console, info) == 0) { + throw new IOException("Could not get the screen info: " + WindowsSupport.getLastErrorMessage()); + } + if (negative) { + info.attributes(invertAttributeColors(info.attributes())); + } + } + + private void applyAttribute() throws IOException { + os.flush(); + short attributes = info.attributes(); + if (negative) { + attributes = invertAttributeColors(attributes); + } + if (SetConsoleTextAttribute(console, attributes) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + + private short invertAttributeColors(short attributes) { + // Swap the the Foreground and Background bits. + int fg = 0x000F & attributes; + fg <<= 4; + int bg = 0X00F0 & attributes; + bg >>= 4; + attributes = (short) ((attributes & 0xFF00) | fg | bg); + return attributes; + } + + private void applyCursorPosition() throws IOException { + if (SetConsoleCursorPosition(console, info.cursorPosition().copy()) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + + @Override + protected void processEraseScreen(int eraseOption) throws IOException { + getConsoleInfo(); + try (Arena session = Arena.ofConfined()) { + MemorySegment written = session.allocate(ValueLayout.JAVA_INT); + switch (eraseOption) { + case ERASE_SCREEN: + COORD topLeft = new COORD(); + topLeft.x((short) 0); + topLeft.y(info.window().top()); + int screenLength = info.window().height() * info.size().x(); + FillConsoleOutputAttribute(console, info.attributes(), screenLength, topLeft, written); + FillConsoleOutputCharacterW(console, ' ', screenLength, topLeft, written); + break; + case ERASE_SCREEN_TO_BEGINING: + COORD topLeft2 = new COORD(); + topLeft2.x((short) 0); + topLeft2.y(info.window().top()); + int lengthToCursor = + (info.cursorPosition().y() - info.window().top()) + * info.size().x() + + info.cursorPosition().x(); + FillConsoleOutputAttribute(console, info.attributes(), lengthToCursor, topLeft2, written); + FillConsoleOutputCharacterW(console, ' ', lengthToCursor, topLeft2, written); + break; + case ERASE_SCREEN_TO_END: + int lengthToEnd = + (info.window().bottom() - info.cursorPosition().y()) + * info.size().x() + + (info.size().x() - info.cursorPosition().x()); + FillConsoleOutputAttribute( + console, + info.attributes(), + lengthToEnd, + info.cursorPosition().copy(), + written); + FillConsoleOutputCharacterW( + console, ' ', lengthToEnd, info.cursorPosition().copy(), written); + break; + default: + break; + } + } + } + + @Override + protected void processEraseLine(int eraseOption) throws IOException { + getConsoleInfo(); + try (Arena session = Arena.ofConfined()) { + MemorySegment written = session.allocate(ValueLayout.JAVA_INT); + switch (eraseOption) { + case ERASE_LINE: + COORD leftColCurrRow = info.cursorPosition().copy(); + leftColCurrRow.x((short) 0); + FillConsoleOutputAttribute( + console, info.attributes(), info.size().x(), leftColCurrRow, written); + FillConsoleOutputCharacterW(console, ' ', info.size().x(), leftColCurrRow, written); + break; + case ERASE_LINE_TO_BEGINING: + COORD leftColCurrRow2 = info.cursorPosition().copy(); + leftColCurrRow2.x((short) 0); + FillConsoleOutputAttribute( + console, info.attributes(), info.cursorPosition().x(), leftColCurrRow2, written); + FillConsoleOutputCharacterW( + console, ' ', info.cursorPosition().x(), leftColCurrRow2, written); + break; + case ERASE_LINE_TO_END: + int lengthToLastCol = + info.size().x() - info.cursorPosition().x(); + FillConsoleOutputAttribute( + console, + info.attributes(), + lengthToLastCol, + info.cursorPosition().copy(), + written); + FillConsoleOutputCharacterW( + console, ' ', lengthToLastCol, info.cursorPosition().copy(), written); + break; + default: + break; + } + } + } + + @Override + protected void processCursorLeft(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition().x((short) Math.max(0, info.cursorPosition().x() - count)); + applyCursorPosition(); + } + + @Override + protected void processCursorRight(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition() + .x((short) Math.min(info.window().width(), info.cursorPosition().x() + count)); + applyCursorPosition(); + } + + @Override + protected void processCursorDown(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition().y((short) + Math.min(Math.max(0, info.size().y() - 1), info.cursorPosition().y() + count)); + applyCursorPosition(); + } + + @Override + protected void processCursorUp(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition() + .y((short) Math.max(info.window().top(), info.cursorPosition().y() - count)); + applyCursorPosition(); + } + + @Override + protected void processCursorTo(int row, int col) throws IOException { + getConsoleInfo(); + info.cursorPosition().y((short) Math.max( + info.window().top(), Math.min(info.size().y(), info.window().top() + row - 1))); + info.cursorPosition().x((short) Math.max(0, Math.min(info.window().width(), col - 1))); + applyCursorPosition(); + } + + @Override + protected void processCursorToColumn(int x) throws IOException { + getConsoleInfo(); + info.cursorPosition().x((short) Math.max(0, Math.min(info.window().width(), x - 1))); + applyCursorPosition(); + } + + @Override + protected void processCursorUpLine(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition().x((short) 0); + info.cursorPosition() + .y((short) Math.max(info.window().top(), info.cursorPosition().y() - count)); + applyCursorPosition(); + } + + @Override + protected void processCursorDownLine(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition().x((short) 0); + info.cursorPosition() + .y((short) Math.max(info.window().top(), info.cursorPosition().y() + count)); + applyCursorPosition(); + } + + @Override + protected void processSetForegroundColor(int color, boolean bright) throws IOException { + info.attributes((short) ((info.attributes() & ~0x0007) | ANSI_FOREGROUND_COLOR_MAP[color])); + if (bright) { + info.attributes((short) (info.attributes() | FOREGROUND_INTENSITY)); + } + applyAttribute(); + } + + @Override + protected void processSetForegroundColorExt(int paletteIndex) throws IOException { + int round = Colors.roundColor(paletteIndex, 16); + processSetForegroundColor(round >= 8 ? round - 8 : round, round >= 8); + } + + @Override + protected void processSetForegroundColorExt(int r, int g, int b) throws IOException { + int round = Colors.roundRgbColor(r, g, b, 16); + processSetForegroundColor(round >= 8 ? round - 8 : round, round >= 8); + } + + @Override + protected void processSetBackgroundColor(int color, boolean bright) throws IOException { + info.attributes((short) ((info.attributes() & ~0x0070) | ANSI_BACKGROUND_COLOR_MAP[color])); + if (bright) { + info.attributes((short) (info.attributes() | BACKGROUND_INTENSITY)); + } + applyAttribute(); + } + + @Override + protected void processSetBackgroundColorExt(int paletteIndex) throws IOException { + int round = Colors.roundColor(paletteIndex, 16); + processSetBackgroundColor(round >= 8 ? round - 8 : round, round >= 8); + } + + @Override + protected void processSetBackgroundColorExt(int r, int g, int b) throws IOException { + int round = Colors.roundRgbColor(r, g, b, 16); + processSetBackgroundColor(round >= 8 ? round - 8 : round, round >= 8); + } + + @Override + protected void processDefaultTextColor() throws IOException { + info.attributes((short) ((info.attributes() & ~0x000F) | (originalColors & 0xF))); + info.attributes((short) (info.attributes() & ~FOREGROUND_INTENSITY)); + applyAttribute(); + } + + @Override + protected void processDefaultBackgroundColor() throws IOException { + info.attributes((short) ((info.attributes() & ~0x00F0) | (originalColors & 0xF0))); + info.attributes((short) (info.attributes() & ~BACKGROUND_INTENSITY)); + applyAttribute(); + } + + @Override + protected void processAttributeReset() throws IOException { + info.attributes((short) ((info.attributes() & ~0x00FF) | originalColors)); + this.negative = false; + applyAttribute(); + } + + @Override + protected void processSetAttribute(int attribute) throws IOException { + switch (attribute) { + case ATTRIBUTE_INTENSITY_BOLD: + info.attributes((short) (info.attributes() | FOREGROUND_INTENSITY)); + applyAttribute(); + break; + case ATTRIBUTE_INTENSITY_NORMAL: + info.attributes((short) (info.attributes() & ~FOREGROUND_INTENSITY)); + applyAttribute(); + break; + + // Yeah, setting the background intensity is not underlining.. but it's best we can do + // using the Windows console API + case ATTRIBUTE_UNDERLINE: + info.attributes((short) (info.attributes() | BACKGROUND_INTENSITY)); + applyAttribute(); + break; + case ATTRIBUTE_UNDERLINE_OFF: + info.attributes((short) (info.attributes() & ~BACKGROUND_INTENSITY)); + applyAttribute(); + break; + + case ATTRIBUTE_NEGATIVE_ON: + negative = true; + applyAttribute(); + break; + case ATTRIBUTE_NEGATIVE_OFF: + negative = false; + applyAttribute(); + break; + default: + break; + } + } + + @Override + protected void processSaveCursorPosition() throws IOException { + getConsoleInfo(); + savedX = info.cursorPosition().x(); + savedY = info.cursorPosition().y(); + } + + @Override + protected void processRestoreCursorPosition() throws IOException { + // restore only if there was a save operation first + if (savedX != -1 && savedY != -1) { + os.flush(); + info.cursorPosition().x(savedX); + info.cursorPosition().y(savedY); + applyCursorPosition(); + } + } + + @Override + protected void processInsertLine(int optionInt) throws IOException { + getConsoleInfo(); + SMALL_RECT scroll = info.window().copy(); + scroll.top(info.cursorPosition().y()); + COORD org = new COORD(); + org.x((short) 0); + org.y((short) (info.cursorPosition().y() + optionInt)); + CHAR_INFO info = new CHAR_INFO(' ', originalColors); + if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + + @Override + protected void processDeleteLine(int optionInt) throws IOException { + getConsoleInfo(); + SMALL_RECT scroll = info.window().copy(); + scroll.top(info.cursorPosition().y()); + COORD org = new COORD(); + org.x((short) 0); + org.y((short) (info.cursorPosition().y() - optionInt)); + CHAR_INFO info = new CHAR_INFO(' ', originalColors); + if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + + @Override + protected void processChangeWindowTitle(String title) { + try (Arena session = Arena.ofConfined()) { + MemorySegment str = session.allocateUtf8String(title); + SetConsoleTitleW(str); + } + } +} diff --git a/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportJni.java b/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportJni.java new file mode 100644 index 00000000..83f1a31e --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportJni.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2009-2023 the original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.fusesource.jansi.internal; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +import org.fusesource.jansi.AnsiConsoleSupport; +import org.fusesource.jansi.io.AnsiProcessor; +import org.fusesource.jansi.io.WindowsAnsiProcessor; + +import static org.fusesource.jansi.internal.Kernel32.FORMAT_MESSAGE_FROM_SYSTEM; +import static org.fusesource.jansi.internal.Kernel32.FormatMessageW; +import static org.fusesource.jansi.internal.Kernel32.GetConsoleMode; +import static org.fusesource.jansi.internal.Kernel32.GetConsoleScreenBufferInfo; +import static org.fusesource.jansi.internal.Kernel32.GetLastError; +import static org.fusesource.jansi.internal.Kernel32.GetStdHandle; +import static org.fusesource.jansi.internal.Kernel32.STD_ERROR_HANDLE; +import static org.fusesource.jansi.internal.Kernel32.STD_OUTPUT_HANDLE; +import static org.fusesource.jansi.internal.Kernel32.SetConsoleMode; + +public class AnsiConsoleSupportJni implements AnsiConsoleSupport { + + @Override + public String getProviderName() { + return "jni"; + } + + @Override + public CLibrary getCLibrary() { + return new CLibrary() { + @Override + public short getTerminalWidth(int fd) { + return org.fusesource.jansi.internal.CLibrary.getTerminalWidth(fd); + } + + @Override + public int isTty(int fd) { + return org.fusesource.jansi.internal.CLibrary.isatty(fd); + } + }; + } + + @Override + public Kernel32 getKernel32() { + return new Kernel32() { + @Override + public int isTty(long console) { + int[] mode = new int[1]; + return GetConsoleMode(console, mode); + } + + @Override + public int getTerminalWidth(long console) { + org.fusesource.jansi.internal.Kernel32.CONSOLE_SCREEN_BUFFER_INFO info = + new org.fusesource.jansi.internal.Kernel32.CONSOLE_SCREEN_BUFFER_INFO(); + GetConsoleScreenBufferInfo(console, info); + return info.windowWidth(); + } + + public long getStdHandle(boolean stdout) { + return GetStdHandle(stdout ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE); + } + + @Override + public int getConsoleMode(long console, int[] mode) { + return GetConsoleMode(console, mode); + } + + @Override + public int setConsoleMode(long console, int mode) { + return SetConsoleMode(console, mode); + } + + @Override + public int getLastError() { + return GetLastError(); + } + + @Override + public String getErrorMessage(int errorCode) { + int bufferSize = 160; + byte[] data = new byte[bufferSize]; + FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, 0, errorCode, 0, data, bufferSize, null); + return new String(data, StandardCharsets.UTF_16LE).trim(); + } + + @Override + public AnsiProcessor newProcessor(OutputStream os, long console) throws IOException { + return new WindowsAnsiProcessor(os, console); + } + }; + } +} diff --git a/src/main/java/org/fusesource/jansi/internal/CLibrary.java b/src/main/java/org/fusesource/jansi/internal/CLibrary.java index 2e2285c3..24e6ddfb 100644 --- a/src/main/java/org/fusesource/jansi/internal/CLibrary.java +++ b/src/main/java/org/fusesource/jansi/internal/CLibrary.java @@ -44,10 +44,6 @@ public class CLibrary { // Constants // - public static int STDOUT_FILENO = 1; - - public static int STDERR_FILENO = 2; - public static boolean HAVE_ISATTY; public static boolean HAVE_TTYNAME; @@ -103,6 +99,12 @@ public class CLibrary { public static native int ioctl(int filedes, long request, WinSize params); + public static short getTerminalWidth(int fd) { + WinSize sz = new WinSize(); + ioctl(fd, TIOCGWINSZ, sz); + return sz.ws_col; + } + /** * Window sizes. * diff --git a/src/main/java/org/fusesource/jansi/internal/WindowsAnsiProcessor.java b/src/main/java/org/fusesource/jansi/internal/WindowsAnsiProcessor.java new file mode 100644 index 00000000..e8e64aa9 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/WindowsAnsiProcessor.java @@ -0,0 +1,407 @@ +/* + * Copyright (C) 2009-2023 the original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.fusesource.jansi.internal; + +import java.io.IOException; +import java.io.OutputStream; + +import org.fusesource.jansi.WindowsSupport; +import org.fusesource.jansi.io.AnsiProcessor; +import org.fusesource.jansi.io.Colors; + +import static org.fusesource.jansi.internal.Kernel32.*; + +/** + * A Windows ANSI escape processor, that uses JNA to access native platform + * API's to change the console attributes (see + * Jansi native Kernel32). + *

The native library used is named jansi and is loaded using HawtJNI Runtime + * Library + * + * @since 1.19 + * @author Hiram Chirino + * @author Joris Kuipers + */ +public class WindowsAnsiProcessor extends AnsiProcessor { + + private final long console; + + private static final short FOREGROUND_BLACK = 0; + private static final short FOREGROUND_YELLOW = (short) (FOREGROUND_RED | FOREGROUND_GREEN); + private static final short FOREGROUND_MAGENTA = (short) (FOREGROUND_BLUE | FOREGROUND_RED); + private static final short FOREGROUND_CYAN = (short) (FOREGROUND_BLUE | FOREGROUND_GREEN); + private static final short FOREGROUND_WHITE = (short) (FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE); + + private static final short BACKGROUND_BLACK = 0; + private static final short BACKGROUND_YELLOW = (short) (BACKGROUND_RED | BACKGROUND_GREEN); + private static final short BACKGROUND_MAGENTA = (short) (BACKGROUND_BLUE | BACKGROUND_RED); + private static final short BACKGROUND_CYAN = (short) (BACKGROUND_BLUE | BACKGROUND_GREEN); + private static final short BACKGROUND_WHITE = (short) (BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE); + + private static final short[] ANSI_FOREGROUND_COLOR_MAP = { + FOREGROUND_BLACK, + FOREGROUND_RED, + FOREGROUND_GREEN, + FOREGROUND_YELLOW, + FOREGROUND_BLUE, + FOREGROUND_MAGENTA, + FOREGROUND_CYAN, + FOREGROUND_WHITE, + }; + + private static final short[] ANSI_BACKGROUND_COLOR_MAP = { + BACKGROUND_BLACK, + BACKGROUND_RED, + BACKGROUND_GREEN, + BACKGROUND_YELLOW, + BACKGROUND_BLUE, + BACKGROUND_MAGENTA, + BACKGROUND_CYAN, + BACKGROUND_WHITE, + }; + + private final CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); + private final short originalColors; + + private boolean negative; + private short savedX = -1; + private short savedY = -1; + + public WindowsAnsiProcessor(OutputStream ps, long console) throws IOException { + super(ps); + this.console = console; + getConsoleInfo(); + originalColors = info.attributes; + } + + public WindowsAnsiProcessor(OutputStream ps, boolean stdout) throws IOException { + this(ps, GetStdHandle(stdout ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE)); + } + + public WindowsAnsiProcessor(OutputStream ps) throws IOException { + this(ps, true); + } + + private void getConsoleInfo() throws IOException { + os.flush(); + if (GetConsoleScreenBufferInfo(console, info) == 0) { + throw new IOException("Could not get the screen info: " + WindowsSupport.getLastErrorMessage()); + } + if (negative) { + info.attributes = invertAttributeColors(info.attributes); + } + } + + private void applyAttribute() throws IOException { + os.flush(); + short attributes = info.attributes; + if (negative) { + attributes = invertAttributeColors(attributes); + } + if (SetConsoleTextAttribute(console, attributes) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + + private short invertAttributeColors(short attributes) { + // Swap the the Foreground and Background bits. + int fg = 0x000F & attributes; + fg <<= 4; + int bg = 0X00F0 & attributes; + bg >>= 4; + attributes = (short) ((attributes & 0xFF00) | fg | bg); + return attributes; + } + + private void applyCursorPosition() throws IOException { + if (SetConsoleCursorPosition(console, info.cursorPosition.copy()) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + + @Override + protected void processEraseScreen(int eraseOption) throws IOException { + getConsoleInfo(); + int[] written = new int[1]; + switch (eraseOption) { + case ERASE_SCREEN: + COORD topLeft = new COORD(); + topLeft.x = 0; + topLeft.y = info.window.top; + int screenLength = info.window.height() * info.size.x; + FillConsoleOutputAttribute(console, info.attributes, screenLength, topLeft, written); + FillConsoleOutputCharacterW(console, ' ', screenLength, topLeft, written); + break; + case ERASE_SCREEN_TO_BEGINING: + COORD topLeft2 = new COORD(); + topLeft2.x = 0; + topLeft2.y = info.window.top; + int lengthToCursor = (info.cursorPosition.y - info.window.top) * info.size.x + info.cursorPosition.x; + FillConsoleOutputAttribute(console, info.attributes, lengthToCursor, topLeft2, written); + FillConsoleOutputCharacterW(console, ' ', lengthToCursor, topLeft2, written); + break; + case ERASE_SCREEN_TO_END: + int lengthToEnd = (info.window.bottom - info.cursorPosition.y) * info.size.x + + (info.size.x - info.cursorPosition.x); + FillConsoleOutputAttribute(console, info.attributes, lengthToEnd, info.cursorPosition.copy(), written); + FillConsoleOutputCharacterW(console, ' ', lengthToEnd, info.cursorPosition.copy(), written); + break; + default: + break; + } + } + + @Override + protected void processEraseLine(int eraseOption) throws IOException { + getConsoleInfo(); + int[] written = new int[1]; + switch (eraseOption) { + case ERASE_LINE: + COORD leftColCurrRow = info.cursorPosition.copy(); + leftColCurrRow.x = 0; + FillConsoleOutputAttribute(console, info.attributes, info.size.x, leftColCurrRow, written); + FillConsoleOutputCharacterW(console, ' ', info.size.x, leftColCurrRow, written); + break; + case ERASE_LINE_TO_BEGINING: + COORD leftColCurrRow2 = info.cursorPosition.copy(); + leftColCurrRow2.x = 0; + FillConsoleOutputAttribute(console, info.attributes, info.cursorPosition.x, leftColCurrRow2, written); + FillConsoleOutputCharacterW(console, ' ', info.cursorPosition.x, leftColCurrRow2, written); + break; + case ERASE_LINE_TO_END: + int lengthToLastCol = info.size.x - info.cursorPosition.x; + FillConsoleOutputAttribute( + console, info.attributes, lengthToLastCol, info.cursorPosition.copy(), written); + FillConsoleOutputCharacterW(console, ' ', lengthToLastCol, info.cursorPosition.copy(), written); + break; + default: + break; + } + } + + @Override + protected void processCursorLeft(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition.x = (short) Math.max(0, info.cursorPosition.x - count); + applyCursorPosition(); + } + + @Override + protected void processCursorRight(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition.x = (short) Math.min(info.window.width(), info.cursorPosition.x + count); + applyCursorPosition(); + } + + @Override + protected void processCursorDown(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition.y = (short) Math.min(Math.max(0, info.size.y - 1), info.cursorPosition.y + count); + applyCursorPosition(); + } + + @Override + protected void processCursorUp(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition.y = (short) Math.max(info.window.top, info.cursorPosition.y - count); + applyCursorPosition(); + } + + @Override + protected void processCursorTo(int row, int col) throws IOException { + getConsoleInfo(); + info.cursorPosition.y = (short) Math.max(info.window.top, Math.min(info.size.y, info.window.top + row - 1)); + info.cursorPosition.x = (short) Math.max(0, Math.min(info.window.width(), col - 1)); + applyCursorPosition(); + } + + @Override + protected void processCursorToColumn(int x) throws IOException { + getConsoleInfo(); + info.cursorPosition.x = (short) Math.max(0, Math.min(info.window.width(), x - 1)); + applyCursorPosition(); + } + + @Override + protected void processCursorUpLine(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition.x = 0; + info.cursorPosition.y = (short) Math.max(info.window.top, info.cursorPosition.y - count); + applyCursorPosition(); + } + + @Override + protected void processCursorDownLine(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition.x = 0; + info.cursorPosition.y = (short) Math.max(info.window.top, info.cursorPosition.y + count); + applyCursorPosition(); + } + + @Override + protected void processSetForegroundColor(int color, boolean bright) throws IOException { + info.attributes = (short) ((info.attributes & ~0x0007) | ANSI_FOREGROUND_COLOR_MAP[color]); + if (bright) { + info.attributes |= FOREGROUND_INTENSITY; + } + applyAttribute(); + } + + @Override + protected void processSetForegroundColorExt(int paletteIndex) throws IOException { + int round = Colors.roundColor(paletteIndex, 16); + processSetForegroundColor(round >= 8 ? round - 8 : round, round >= 8); + } + + @Override + protected void processSetForegroundColorExt(int r, int g, int b) throws IOException { + int round = Colors.roundRgbColor(r, g, b, 16); + processSetForegroundColor(round >= 8 ? round - 8 : round, round >= 8); + } + + @Override + protected void processSetBackgroundColor(int color, boolean bright) throws IOException { + info.attributes = (short) ((info.attributes & ~0x0070) | ANSI_BACKGROUND_COLOR_MAP[color]); + if (bright) { + info.attributes |= BACKGROUND_INTENSITY; + } + applyAttribute(); + } + + @Override + protected void processSetBackgroundColorExt(int paletteIndex) throws IOException { + int round = Colors.roundColor(paletteIndex, 16); + processSetBackgroundColor(round >= 8 ? round - 8 : round, round >= 8); + } + + @Override + protected void processSetBackgroundColorExt(int r, int g, int b) throws IOException { + int round = Colors.roundRgbColor(r, g, b, 16); + processSetBackgroundColor(round >= 8 ? round - 8 : round, round >= 8); + } + + @Override + protected void processDefaultTextColor() throws IOException { + info.attributes = (short) ((info.attributes & ~0x000F) | (originalColors & 0xF)); + info.attributes = (short) (info.attributes & ~FOREGROUND_INTENSITY); + applyAttribute(); + } + + @Override + protected void processDefaultBackgroundColor() throws IOException { + info.attributes = (short) ((info.attributes & ~0x00F0) | (originalColors & 0xF0)); + info.attributes = (short) (info.attributes & ~BACKGROUND_INTENSITY); + applyAttribute(); + } + + @Override + protected void processAttributeReset() throws IOException { + info.attributes = (short) ((info.attributes & ~0x00FF) | originalColors); + this.negative = false; + applyAttribute(); + } + + @Override + protected void processSetAttribute(int attribute) throws IOException { + switch (attribute) { + case ATTRIBUTE_INTENSITY_BOLD: + info.attributes = (short) (info.attributes | FOREGROUND_INTENSITY); + applyAttribute(); + break; + case ATTRIBUTE_INTENSITY_NORMAL: + info.attributes = (short) (info.attributes & ~FOREGROUND_INTENSITY); + applyAttribute(); + break; + + // Yeah, setting the background intensity is not underlining.. but it's best we can do + // using the Windows console API + case ATTRIBUTE_UNDERLINE: + info.attributes = (short) (info.attributes | BACKGROUND_INTENSITY); + applyAttribute(); + break; + case ATTRIBUTE_UNDERLINE_OFF: + info.attributes = (short) (info.attributes & ~BACKGROUND_INTENSITY); + applyAttribute(); + break; + + case ATTRIBUTE_NEGATIVE_ON: + negative = true; + applyAttribute(); + break; + case ATTRIBUTE_NEGATIVE_OFF: + negative = false; + applyAttribute(); + break; + default: + break; + } + } + + @Override + protected void processSaveCursorPosition() throws IOException { + getConsoleInfo(); + savedX = info.cursorPosition.x; + savedY = info.cursorPosition.y; + } + + @Override + protected void processRestoreCursorPosition() throws IOException { + // restore only if there was a save operation first + if (savedX != -1 && savedY != -1) { + os.flush(); + info.cursorPosition.x = savedX; + info.cursorPosition.y = savedY; + applyCursorPosition(); + } + } + + @Override + protected void processInsertLine(int optionInt) throws IOException { + getConsoleInfo(); + SMALL_RECT scroll = info.window.copy(); + scroll.top = info.cursorPosition.y; + COORD org = new COORD(); + org.x = 0; + org.y = (short) (info.cursorPosition.y + optionInt); + CHAR_INFO info = new CHAR_INFO(); + info.attributes = originalColors; + info.unicodeChar = ' '; + if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + + @Override + protected void processDeleteLine(int optionInt) throws IOException { + getConsoleInfo(); + SMALL_RECT scroll = info.window.copy(); + scroll.top = info.cursorPosition.y; + COORD org = new COORD(); + org.x = 0; + org.y = (short) (info.cursorPosition.y - optionInt); + CHAR_INFO info = new CHAR_INFO(); + info.attributes = originalColors; + info.unicodeChar = ' '; + if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + + @Override + protected void processChangeWindowTitle(String label) { + SetConsoleTitle(label); + } +} diff --git a/src/main/java/org/fusesource/jansi/io/WindowsAnsiProcessor.java b/src/main/java/org/fusesource/jansi/io/WindowsAnsiProcessor.java index 74a178a5..dccb8403 100644 --- a/src/main/java/org/fusesource/jansi/io/WindowsAnsiProcessor.java +++ b/src/main/java/org/fusesource/jansi/io/WindowsAnsiProcessor.java @@ -18,31 +18,6 @@ import java.io.IOException; import java.io.OutputStream; -import org.fusesource.jansi.WindowsSupport; -import org.fusesource.jansi.internal.Kernel32.CONSOLE_SCREEN_BUFFER_INFO; -import org.fusesource.jansi.internal.Kernel32.COORD; - -import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_BLUE; -import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_GREEN; -import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_INTENSITY; -import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_RED; -import static org.fusesource.jansi.internal.Kernel32.CHAR_INFO; -import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_BLUE; -import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_GREEN; -import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_INTENSITY; -import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_RED; -import static org.fusesource.jansi.internal.Kernel32.FillConsoleOutputAttribute; -import static org.fusesource.jansi.internal.Kernel32.FillConsoleOutputCharacterW; -import static org.fusesource.jansi.internal.Kernel32.GetConsoleScreenBufferInfo; -import static org.fusesource.jansi.internal.Kernel32.GetStdHandle; -import static org.fusesource.jansi.internal.Kernel32.SMALL_RECT; -import static org.fusesource.jansi.internal.Kernel32.STD_ERROR_HANDLE; -import static org.fusesource.jansi.internal.Kernel32.STD_OUTPUT_HANDLE; -import static org.fusesource.jansi.internal.Kernel32.ScrollConsoleScreenBuffer; -import static org.fusesource.jansi.internal.Kernel32.SetConsoleCursorPosition; -import static org.fusesource.jansi.internal.Kernel32.SetConsoleTextAttribute; -import static org.fusesource.jansi.internal.Kernel32.SetConsoleTitle; - /** * A Windows ANSI escape processor, that uses JNA to access native platform * API's to change the console attributes (see @@ -51,374 +26,20 @@ * Library * * @since 1.19 + * @author Hiram Chirino + * @author Joris Kuipers */ -public final class WindowsAnsiProcessor extends AnsiProcessor { - - private final long console; - - private static final short FOREGROUND_BLACK = 0; - private static final short FOREGROUND_YELLOW = (short) (FOREGROUND_RED | FOREGROUND_GREEN); - private static final short FOREGROUND_MAGENTA = (short) (FOREGROUND_BLUE | FOREGROUND_RED); - private static final short FOREGROUND_CYAN = (short) (FOREGROUND_BLUE | FOREGROUND_GREEN); - private static final short FOREGROUND_WHITE = (short) (FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE); - - private static final short BACKGROUND_BLACK = 0; - private static final short BACKGROUND_YELLOW = (short) (BACKGROUND_RED | BACKGROUND_GREEN); - private static final short BACKGROUND_MAGENTA = (short) (BACKGROUND_BLUE | BACKGROUND_RED); - private static final short BACKGROUND_CYAN = (short) (BACKGROUND_BLUE | BACKGROUND_GREEN); - private static final short BACKGROUND_WHITE = (short) (BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE); - - private static final short[] ANSI_FOREGROUND_COLOR_MAP = { - FOREGROUND_BLACK, - FOREGROUND_RED, - FOREGROUND_GREEN, - FOREGROUND_YELLOW, - FOREGROUND_BLUE, - FOREGROUND_MAGENTA, - FOREGROUND_CYAN, - FOREGROUND_WHITE, - }; - - private static final short[] ANSI_BACKGROUND_COLOR_MAP = { - BACKGROUND_BLACK, - BACKGROUND_RED, - BACKGROUND_GREEN, - BACKGROUND_YELLOW, - BACKGROUND_BLUE, - BACKGROUND_MAGENTA, - BACKGROUND_CYAN, - BACKGROUND_WHITE, - }; - - private final CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); - private final short originalColors; - - private boolean negative; - private short savedX = -1; - private short savedY = -1; +public final class WindowsAnsiProcessor extends org.fusesource.jansi.internal.WindowsAnsiProcessor { public WindowsAnsiProcessor(OutputStream ps, long console) throws IOException { - super(ps); - this.console = console; - getConsoleInfo(); - originalColors = info.attributes; + super(ps, console); } public WindowsAnsiProcessor(OutputStream ps, boolean stdout) throws IOException { - this(ps, GetStdHandle(stdout ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE)); + super(ps, stdout); } public WindowsAnsiProcessor(OutputStream ps) throws IOException { - this(ps, true); - } - - private void getConsoleInfo() throws IOException { - os.flush(); - if (GetConsoleScreenBufferInfo(console, info) == 0) { - throw new IOException("Could not get the screen info: " + WindowsSupport.getLastErrorMessage()); - } - if (negative) { - info.attributes = invertAttributeColors(info.attributes); - } - } - - private void applyAttribute() throws IOException { - os.flush(); - short attributes = info.attributes; - if (negative) { - attributes = invertAttributeColors(attributes); - } - if (SetConsoleTextAttribute(console, attributes) == 0) { - throw new IOException(WindowsSupport.getLastErrorMessage()); - } - } - - private short invertAttributeColors(short attributes) { - // Swap the the Foreground and Background bits. - int fg = 0x000F & attributes; - fg <<= 4; - int bg = 0X00F0 & attributes; - bg >>= 4; - attributes = (short) ((attributes & 0xFF00) | fg | bg); - return attributes; - } - - private void applyCursorPosition() throws IOException { - if (SetConsoleCursorPosition(console, info.cursorPosition.copy()) == 0) { - throw new IOException(WindowsSupport.getLastErrorMessage()); - } - } - - @Override - protected void processEraseScreen(int eraseOption) throws IOException { - getConsoleInfo(); - int[] written = new int[1]; - switch (eraseOption) { - case ERASE_SCREEN: - COORD topLeft = new COORD(); - topLeft.x = 0; - topLeft.y = info.window.top; - int screenLength = info.window.height() * info.size.x; - FillConsoleOutputAttribute(console, info.attributes, screenLength, topLeft, written); - FillConsoleOutputCharacterW(console, ' ', screenLength, topLeft, written); - break; - case ERASE_SCREEN_TO_BEGINING: - COORD topLeft2 = new COORD(); - topLeft2.x = 0; - topLeft2.y = info.window.top; - int lengthToCursor = (info.cursorPosition.y - info.window.top) * info.size.x + info.cursorPosition.x; - FillConsoleOutputAttribute(console, info.attributes, lengthToCursor, topLeft2, written); - FillConsoleOutputCharacterW(console, ' ', lengthToCursor, topLeft2, written); - break; - case ERASE_SCREEN_TO_END: - int lengthToEnd = (info.window.bottom - info.cursorPosition.y) * info.size.x - + (info.size.x - info.cursorPosition.x); - FillConsoleOutputAttribute(console, info.attributes, lengthToEnd, info.cursorPosition.copy(), written); - FillConsoleOutputCharacterW(console, ' ', lengthToEnd, info.cursorPosition.copy(), written); - break; - default: - break; - } - } - - @Override - protected void processEraseLine(int eraseOption) throws IOException { - getConsoleInfo(); - int[] written = new int[1]; - switch (eraseOption) { - case ERASE_LINE: - COORD leftColCurrRow = info.cursorPosition.copy(); - leftColCurrRow.x = 0; - FillConsoleOutputAttribute(console, info.attributes, info.size.x, leftColCurrRow, written); - FillConsoleOutputCharacterW(console, ' ', info.size.x, leftColCurrRow, written); - break; - case ERASE_LINE_TO_BEGINING: - COORD leftColCurrRow2 = info.cursorPosition.copy(); - leftColCurrRow2.x = 0; - FillConsoleOutputAttribute(console, info.attributes, info.cursorPosition.x, leftColCurrRow2, written); - FillConsoleOutputCharacterW(console, ' ', info.cursorPosition.x, leftColCurrRow2, written); - break; - case ERASE_LINE_TO_END: - int lengthToLastCol = info.size.x - info.cursorPosition.x; - FillConsoleOutputAttribute( - console, info.attributes, lengthToLastCol, info.cursorPosition.copy(), written); - FillConsoleOutputCharacterW(console, ' ', lengthToLastCol, info.cursorPosition.copy(), written); - break; - default: - break; - } - } - - @Override - protected void processCursorLeft(int count) throws IOException { - getConsoleInfo(); - info.cursorPosition.x = (short) Math.max(0, info.cursorPosition.x - count); - applyCursorPosition(); - } - - @Override - protected void processCursorRight(int count) throws IOException { - getConsoleInfo(); - info.cursorPosition.x = (short) Math.min(info.window.width(), info.cursorPosition.x + count); - applyCursorPosition(); - } - - @Override - protected void processCursorDown(int count) throws IOException { - getConsoleInfo(); - info.cursorPosition.y = (short) Math.min(Math.max(0, info.size.y - 1), info.cursorPosition.y + count); - applyCursorPosition(); - } - - @Override - protected void processCursorUp(int count) throws IOException { - getConsoleInfo(); - info.cursorPosition.y = (short) Math.max(info.window.top, info.cursorPosition.y - count); - applyCursorPosition(); - } - - @Override - protected void processCursorTo(int row, int col) throws IOException { - getConsoleInfo(); - info.cursorPosition.y = (short) Math.max(info.window.top, Math.min(info.size.y, info.window.top + row - 1)); - info.cursorPosition.x = (short) Math.max(0, Math.min(info.window.width(), col - 1)); - applyCursorPosition(); - } - - @Override - protected void processCursorToColumn(int x) throws IOException { - getConsoleInfo(); - info.cursorPosition.x = (short) Math.max(0, Math.min(info.window.width(), x - 1)); - applyCursorPosition(); - } - - @Override - protected void processCursorUpLine(int count) throws IOException { - getConsoleInfo(); - info.cursorPosition.x = 0; - info.cursorPosition.y = (short) Math.max(info.window.top, info.cursorPosition.y - count); - applyCursorPosition(); - } - - @Override - protected void processCursorDownLine(int count) throws IOException { - getConsoleInfo(); - info.cursorPosition.x = 0; - info.cursorPosition.y = (short) Math.max(info.window.top, info.cursorPosition.y + count); - applyCursorPosition(); - } - - @Override - protected void processSetForegroundColor(int color, boolean bright) throws IOException { - info.attributes = (short) ((info.attributes & ~0x0007) | ANSI_FOREGROUND_COLOR_MAP[color]); - if (bright) { - info.attributes |= FOREGROUND_INTENSITY; - } - applyAttribute(); - } - - @Override - protected void processSetForegroundColorExt(int paletteIndex) throws IOException { - int round = Colors.roundColor(paletteIndex, 16); - processSetForegroundColor(round >= 8 ? round - 8 : round, round >= 8); - } - - @Override - protected void processSetForegroundColorExt(int r, int g, int b) throws IOException { - int round = Colors.roundRgbColor(r, g, b, 16); - processSetForegroundColor(round >= 8 ? round - 8 : round, round >= 8); - } - - @Override - protected void processSetBackgroundColor(int color, boolean bright) throws IOException { - info.attributes = (short) ((info.attributes & ~0x0070) | ANSI_BACKGROUND_COLOR_MAP[color]); - if (bright) { - info.attributes |= BACKGROUND_INTENSITY; - } - applyAttribute(); - } - - @Override - protected void processSetBackgroundColorExt(int paletteIndex) throws IOException { - int round = Colors.roundColor(paletteIndex, 16); - processSetBackgroundColor(round >= 8 ? round - 8 : round, round >= 8); - } - - @Override - protected void processSetBackgroundColorExt(int r, int g, int b) throws IOException { - int round = Colors.roundRgbColor(r, g, b, 16); - processSetBackgroundColor(round >= 8 ? round - 8 : round, round >= 8); - } - - @Override - protected void processDefaultTextColor() throws IOException { - info.attributes = (short) ((info.attributes & ~0x000F) | (originalColors & 0xF)); - info.attributes = (short) (info.attributes & ~FOREGROUND_INTENSITY); - applyAttribute(); - } - - @Override - protected void processDefaultBackgroundColor() throws IOException { - info.attributes = (short) ((info.attributes & ~0x00F0) | (originalColors & 0xF0)); - info.attributes = (short) (info.attributes & ~BACKGROUND_INTENSITY); - applyAttribute(); - } - - @Override - protected void processAttributeReset() throws IOException { - info.attributes = (short) ((info.attributes & ~0x00FF) | originalColors); - this.negative = false; - applyAttribute(); - } - - @Override - protected void processSetAttribute(int attribute) throws IOException { - switch (attribute) { - case ATTRIBUTE_INTENSITY_BOLD: - info.attributes = (short) (info.attributes | FOREGROUND_INTENSITY); - applyAttribute(); - break; - case ATTRIBUTE_INTENSITY_NORMAL: - info.attributes = (short) (info.attributes & ~FOREGROUND_INTENSITY); - applyAttribute(); - break; - - // Yeah, setting the background intensity is not underlining.. but it's best we can do - // using the Windows console API - case ATTRIBUTE_UNDERLINE: - info.attributes = (short) (info.attributes | BACKGROUND_INTENSITY); - applyAttribute(); - break; - case ATTRIBUTE_UNDERLINE_OFF: - info.attributes = (short) (info.attributes & ~BACKGROUND_INTENSITY); - applyAttribute(); - break; - - case ATTRIBUTE_NEGATIVE_ON: - negative = true; - applyAttribute(); - break; - case ATTRIBUTE_NEGATIVE_OFF: - negative = false; - applyAttribute(); - break; - default: - break; - } - } - - @Override - protected void processSaveCursorPosition() throws IOException { - getConsoleInfo(); - savedX = info.cursorPosition.x; - savedY = info.cursorPosition.y; - } - - @Override - protected void processRestoreCursorPosition() throws IOException { - // restore only if there was a save operation first - if (savedX != -1 && savedY != -1) { - os.flush(); - info.cursorPosition.x = savedX; - info.cursorPosition.y = savedY; - applyCursorPosition(); - } - } - - @Override - protected void processInsertLine(int optionInt) throws IOException { - getConsoleInfo(); - SMALL_RECT scroll = info.window.copy(); - scroll.top = info.cursorPosition.y; - COORD org = new COORD(); - org.x = 0; - org.y = (short) (info.cursorPosition.y + optionInt); - CHAR_INFO info = new CHAR_INFO(); - info.attributes = originalColors; - info.unicodeChar = ' '; - if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { - throw new IOException(WindowsSupport.getLastErrorMessage()); - } - } - - @Override - protected void processDeleteLine(int optionInt) throws IOException { - getConsoleInfo(); - SMALL_RECT scroll = info.window.copy(); - scroll.top = info.cursorPosition.y; - COORD org = new COORD(); - org.x = 0; - org.y = (short) (info.cursorPosition.y - optionInt); - CHAR_INFO info = new CHAR_INFO(); - info.attributes = originalColors; - info.unicodeChar = ' '; - if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { - throw new IOException(WindowsSupport.getLastErrorMessage()); - } - } - - @Override - protected void processChangeWindowTitle(String label) { - SetConsoleTitle(label); + super(ps); } }