diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 169a1924..891f5f24 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: @@ -46,4 +46,4 @@ jobs: key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-m2 - name: mvn verify - run: mvn test + run: mvn verify -Dnosign diff --git a/Makefile b/Makefile index 4e2b2c85..cb5c1049 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,7 @@ include Makefile.common linux-armv6-digest:=@sha256:7bad6ab302af34bdf6634c8c2b02c8dc6ac932c67da9ecc199c549ab405e971e linux-x86-digest:=@sha256:7a8fda5ff1bb436ac1f2e7d40043deb630800fce33d123d04779d48f85702dcd +linux-riscv64-digest:=@sha256:e10e1d3588cffffaf4d0721825e4f952710ad29d4b6630ea76d353914ffdc415 windows-static-x86-digest:=@sha256:896bd4a43bbc89502904afdc8d00e6f2422f8f35852cc59777d6426bfc8491e8 windows-static-x64-digest:=@sha256:f159861bc80b29e5dafb223477167bec53ecec6cdacb051d31e90c5823542100 windows-arm64-digest:=@sha256:f4b3c1a49ec8b53418cef1499dc3f9a54a5570b7a3ecdf42fc8c83eb94b01b7d @@ -121,6 +122,12 @@ linux-ppc64: download-includes docker run -it --rm -v $$PWD:/workdir --user $$(id -u):$$(id -g) \ -e CROSS_TRIPLE=powerpc64le-linux-gnu multiarch/crossbuild$(cross-build-digest) make clean-native native OS_NAME=Linux OS_ARCH=ppc64 +target/dockcross/dockcross-linux-riscv64: dockcross + docker run --rm dockcross/linux-riscv64$(linux-riscv64-digest) > target/dockcross/dockcross-linux-riscv64 + chmod +x target/dockcross/dockcross-linux-riscv64 +linux-riscv64: download-includes target/dockcross/dockcross-linux-riscv64 + target/dockcross/dockcross-linux-riscv64 bash -c 'make clean-native native CROSS_PREFIX=riscv64-unknown-linux-gnu- OS_NAME=Linux OS_ARCH=riscv64' + target/dockcross/dockcross-windows-static-x86: dockcross docker run --rm dockcross/windows-static-x86$(windows-static-x86-digest) > target/dockcross/dockcross-windows-static-x86 chmod +x target/dockcross/dockcross-windows-static-x86 diff --git a/Makefile.common b/Makefile.common index d67eb0e6..5f24f473 100644 --- a/Makefile.common +++ b/Makefile.common @@ -80,6 +80,13 @@ Linux-ppc64_LINKFLAGS := -shared -static-libgcc Linux-ppc64_LIBNAME := libjansi.so Linux-ppc64_JANSI_FLAGS := +Linux-riscv64_CC := $(CROSS_PREFIX)gcc +Linux-riscv64_STRIP := $(CROSS_PREFIX)strip +Linux-riscv64_CCFLAGS := -I$(JAVA_HOME)/include -Itarget/inc -Itarget/inc/unix -Os -fPIC -fvisibility=hidden +Linux-riscv64_LINKFLAGS := -shared -static-libgcc +Linux-riscv64_LIBNAME := libjansi.so +Linux-riscv64_JANSI_FLAGS := + DragonFly-x86_64_CC := $(CROSS_PREFIX)cc DragonFly-x86_64_STRIP := $(CROSS_PREFIX)strip DragonFly-x86_64_CCFLAGS := -I$(JAVA_HOME)/include -Itarget/inc -Itarget/inc/unix -O2 -fPIC -fvisibility=hidden diff --git a/pom.xml b/pom.xml index 9cf9578f..2c79c2db 100644 --- a/pom.xml +++ b/pom.xml @@ -109,6 +109,13 @@ + + org.graalvm.sdk + nativeimage + 23.1.0 + provided + true + org.junit.jupiter junit-jupiter @@ -160,13 +167,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 @@ -206,12 +267,14 @@ 9 + org.fusesource.jansi.AnsiMain org.fusesource.jansi org.fusesource.jansi; org.fusesource.jansi.io; + true @@ -324,20 +387,6 @@ maven-release-plugin 3.0.0-M1 - - org.apache.maven.plugins - maven-gpg-plugin - 1.6 - - - sign-artifacts - - sign - - verify - - - org.sonatype.plugins nexus-staging-maven-plugin @@ -351,12 +400,12 @@ com.diffplug.spotless spotless-maven-plugin - 2.38.0 + 2.39.0 - 2.35.0 + 2.38.0 java|javax,org,,\# @@ -410,4 +459,33 @@ + + + sign + + + !nosign + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + sign-artifacts + + sign + + verify + + + + + + + + diff --git a/src/main/java/org/fusesource/jansi/Ansi.java b/src/main/java/org/fusesource/jansi/Ansi.java index c1054742..576d8d53 100644 --- a/src/main/java/org/fusesource/jansi/Ansi.java +++ b/src/main/java/org/fusesource/jansi/Ansi.java @@ -149,17 +149,14 @@ public int value() { } } + @FunctionalInterface public interface Consumer { void apply(Ansi ansi); } public static final String DISABLE = Ansi.class.getName() + ".disable"; - private static Callable detector = new Callable() { - public Boolean call() throws Exception { - return !Boolean.getBoolean(DISABLE); - } - }; + private static Callable detector = () -> !Boolean.getBoolean(DISABLE); public static void setDetector(final Callable detector) { if (detector == null) throw new IllegalArgumentException(); @@ -374,7 +371,7 @@ public Ansi reset() { } private final StringBuilder builder; - private final ArrayList attributeOptions = new ArrayList(5); + private final ArrayList attributeOptions = new ArrayList<>(5); public Ansi() { this(new StringBuilder(80)); @@ -716,19 +713,45 @@ public Ansi scrollDown(final int rows) { return rows > 0 ? appendEscapeSequence('T', rows) : rows < 0 ? scrollUp(-rows) : this; } + @Deprecated + public Ansi restorCursorPosition() { + return restoreCursorPosition(); + } + public Ansi saveCursorPosition() { + saveCursorPositionSCO(); + return saveCursorPositionDEC(); + } + + // SCO command + public Ansi saveCursorPositionSCO() { return appendEscapeSequence('s'); } - @Deprecated - public Ansi restorCursorPosition() { - return appendEscapeSequence('u'); + // DEC command + public Ansi saveCursorPositionDEC() { + builder.append(FIRST_ESC_CHAR); + builder.append('7'); + return this; } public Ansi restoreCursorPosition() { + restoreCursorPositionSCO(); + return restoreCursorPositionDEC(); + } + + // SCO command + public Ansi restoreCursorPositionSCO() { return appendEscapeSequence('u'); } + // DEC command + public Ansi restoreCursorPositionDEC() { + builder.append(FIRST_ESC_CHAR); + builder.append('8'); + return this; + } + public Ansi reset() { return a(Attribute.RESET); } diff --git a/src/main/java/org/fusesource/jansi/AnsiConsole.java b/src/main/java/org/fusesource/jansi/AnsiConsole.java index 3b7b0032..edc52429 100644 --- a/src/main/java/org/fusesource/jansi/AnsiConsole.java +++ b/src/main/java/org/fusesource/jansi/AnsiConsole.java @@ -24,24 +24,15 @@ import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; 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.internal.MingwSupport; +import org.fusesource.jansi.internal.OSInfo; 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; +import static org.fusesource.jansi.internal.AnsiConsoleSupportHolder.getCLibrary; +import static org.fusesource.jansi.internal.AnsiConsoleSupportHolder.getKernel32; /** * Provides consistent access to an ANSI aware console PrintStream or an ANSI codes stripping PrintStream @@ -167,6 +158,22 @@ public class AnsiConsole { */ public static final String JANSI_GRACEFUL = "jansi.graceful"; + /** + * The {@code jansi.providers} system property can be set to control which internal provider + * will be used. If this property is not set, the {@code ffm} provider will be used if available, + * else the {@code jni} one will be used. If set, this property is interpreted as a comma + * separated list of provider names to try in order. + */ + public static final String JANSI_PROVIDERS = "jansi.providers"; + /** + * The name of the {@code jni} provider. + */ + public static final String JANSI_PROVIDER_JNI = "jni"; + /** + * The name of the {@code ffm} provider. + */ + public static final String JANSI_PROVIDER_FFM = "ffm"; + /** * @deprecated this field will be made private in a future release, use {@link #sysOut()} instead */ @@ -202,8 +209,7 @@ public static int getTerminalWidth() { return w; } - static final boolean IS_WINDOWS = - System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("win"); + static final boolean IS_WINDOWS = OSInfo.isWindows(); static final boolean IS_CYGWIN = IS_WINDOWS && System.getenv("PWD") != null && System.getenv("PWD").startsWith("/"); @@ -227,9 +233,9 @@ public static int getTerminalWidth() { } } - private static boolean initialized; - private static int installed; - private static int virtualProcessing; + private static volatile boolean initialized; + private static volatile int installed; + private static volatile int virtualProcessing; private AnsiConsole() {} @@ -237,7 +243,10 @@ private static AnsiPrintStream ansiStream(boolean stdout) { FileDescriptor descriptor = stdout ? FileDescriptor.out : FileDescriptor.err; final OutputStream out = new FastBufferedOutputStream(new FileOutputStream(descriptor)); - String enc = System.getProperty(stdout ? "sun.stdout.encoding" : "sun.stderr.encoding"); + String enc = System.getProperty(stdout ? "stdout.encoding" : "stderr.encoding"); + if (enc == null) { + enc = System.getProperty(stdout ? "sun.stdout.encoding" : "sun.stderr.encoding"); + } final boolean isatty; boolean isAtty; @@ -246,9 +255,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,33 +283,40 @@ 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 { + installer = () -> { + synchronized (AnsiConsole.class) { 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 { + uninstaller = () -> { + synchronized (AnsiConsole.class) { if (--virtualProcessing == 0) { - SetConsoleMode(console, mode[0]); + getKernel32().setConsoleMode(console, mode[0]); } } }; + width = () -> getKernel32().getTerminalWidth(console); } else if ((IS_CONEMU || IS_CYGWIN || IS_MSYSTEM) && !isConsole) { // ANSI-enabled ConEmu, Cygwin or MSYS(2) on Windows... processor = null; type = AnsiType.Native; installer = uninstaller = null; + MingwSupport mingw = new MingwSupport(); + String name = mingw.getConsoleName(stdout); + if (name != null && !name.isEmpty()) { + width = () -> mingw.getTerminalWidth(name); + } else { + width = () -> -1; + } } else { // On Windows, when no ANSI-capable terminal is used, we know the console does not natively interpret // ANSI @@ -308,7 +324,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. @@ -319,15 +335,8 @@ public void run() throws IOException { processor = proc; type = ttype; installer = uninstaller = null; + width = () -> getKernel32().getTerminalWidth(console); } - width = new AnsiOutputStream.WidthSupplier() { - @Override - public int getTerminalWidth() { - CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); - GetConsoleScreenBufferInfo(console, info); - return info.windowWidth(); - } - }; } // We must be on some Unix variant... @@ -336,14 +345,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; @@ -446,8 +448,7 @@ static boolean getBoolean(String name) { try { String val = System.getProperty(name); result = val.isEmpty() || Boolean.parseBoolean(val); - } catch (IllegalArgumentException e) { - } catch (NullPointerException e) { + } catch (IllegalArgumentException | NullPointerException ignored) { } return result; } diff --git a/src/main/java/org/fusesource/jansi/AnsiMain.java b/src/main/java/org/fusesource/jansi/AnsiMain.java index c2a845d6..dace3a4d 100644 --- a/src/main/java/org/fusesource/jansi/AnsiMain.java +++ b/src/main/java/org/fusesource/jansi/AnsiMain.java @@ -26,9 +26,12 @@ import java.util.Properties; import org.fusesource.jansi.Ansi.Attribute; -import org.fusesource.jansi.internal.CLibrary; +import org.fusesource.jansi.internal.AnsiConsoleSupport; +import org.fusesource.jansi.internal.AnsiConsoleSupportHolder; import org.fusesource.jansi.internal.JansiLoader; +import org.fusesource.jansi.internal.MingwSupport; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.fusesource.jansi.Ansi.ansi; /** @@ -54,27 +57,33 @@ 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, "")); + String provider = AnsiConsoleSupportHolder.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); + } } } } @@ -85,9 +94,14 @@ public static void main(String... args) throws IOException { + "os.version= " + System.getProperty("os.version") + ", " + "os.arch= " + System.getProperty("os.arch")); System.out.println("file.encoding= " + System.getProperty("file.encoding")); + System.out.println("sun.stdout.encoding= " + System.getProperty("sun.stdout.encoding") + ", " + + "sun.stderr.encoding= " + System.getProperty("sun.stderr.encoding")); + System.out.println("stdout.encoding= " + System.getProperty("stdout.encoding") + ", " + "stderr.encoding= " + + System.getProperty("stderr.encoding")); System.out.println("java.version= " + System.getProperty("java.version") + ", " + "java.vendor= " + System.getProperty("java.vendor") + "," + " java.home= " + System.getProperty("java.home")); + System.out.println("Console: " + System.console()); System.out.println(); @@ -188,11 +202,33 @@ 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 = AnsiConsoleSupportHolder.getKernel32().getStdHandle(!stderr); + isatty = AnsiConsoleSupportHolder.getKernel32().isTty(console); + if ((AnsiConsole.IS_CONEMU || AnsiConsole.IS_CYGWIN || AnsiConsole.IS_MSYSTEM) && isatty == 0) { + MingwSupport mingw = new MingwSupport(); + String name = mingw.getConsoleName(!stderr); + if (name != null && !name.isEmpty()) { + isatty = 1; + width = mingw.getTerminalWidth(name); + } else { + isatty = 0; + width = 0; + } + } else { + width = AnsiConsoleSupportHolder.getKernel32().getTerminalWidth(console); + } + } else { + int fd = stderr ? AnsiConsoleSupport.CLibrary.STDERR_FILENO : AnsiConsoleSupport.CLibrary.STDOUT_FILENO; + isatty = AnsiConsoleSupportHolder.getCLibrary().isTty(fd); + width = AnsiConsoleSupportHolder.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) { @@ -274,7 +310,7 @@ private static String getPomPropertiesVersion(String path) throws IOException { private static void printJansiLogoDemo() throws IOException { BufferedReader in = - new BufferedReader(new InputStreamReader(AnsiMain.class.getResourceAsStream("jansi.txt"), "UTF-8")); + new BufferedReader(new InputStreamReader(AnsiMain.class.getResourceAsStream("jansi.txt"), UTF_8)); try { String l; while ((l = in.readLine()) != null) { diff --git a/src/main/java/org/fusesource/jansi/WindowsSupport.java b/src/main/java/org/fusesource/jansi/WindowsSupport.java index 010f527e..cfc0f9bc 100644 --- a/src/main/java/org/fusesource/jansi/WindowsSupport.java +++ b/src/main/java/org/fusesource/jansi/WindowsSupport.java @@ -15,27 +15,16 @@ */ 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; +import org.fusesource.jansi.internal.AnsiConsoleSupportHolder; public class WindowsSupport { public static String getLastErrorMessage() { - int errorCode = GetLastError(); + int errorCode = AnsiConsoleSupportHolder.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 AnsiConsoleSupportHolder.getKernel32().getErrorMessage(errorCode); } } diff --git a/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupport.java b/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupport.java new file mode 100644 index 00000000..64087fa1 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupport.java @@ -0,0 +1,89 @@ +/* + * 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.io.AnsiProcessor; + +public abstract class AnsiConsoleSupport { + + public interface CLibrary { + + int STDOUT_FILENO = 1; + int STDERR_FILENO = 2; + + short getTerminalWidth(int fd); + + int isTty(int fd); + } + + public 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; + } + + private final String providerName; + private CLibrary cLibrary; + private Kernel32 kernel32; + + protected AnsiConsoleSupport(String providerName) { + this.providerName = providerName; + } + + public final String getProviderName() { + return providerName; + } + + protected abstract CLibrary createCLibrary(); + + protected abstract Kernel32 createKernel32(); + + public final CLibrary getCLibrary() { + if (cLibrary == null) { + cLibrary = createCLibrary(); + } + + return cLibrary; + } + + public final Kernel32 getKernel32() { + if (kernel32 == null) { + if (!OSInfo.isWindows()) { + throw new RuntimeException("Kernel32 is not available on this platform"); + } + + kernel32 = createKernel32(); + } + + return kernel32; + } +} diff --git a/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportHolder.java b/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportHolder.java new file mode 100644 index 00000000..9e66e579 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportHolder.java @@ -0,0 +1,107 @@ +/* + * 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 static org.fusesource.jansi.AnsiConsole.JANSI_PROVIDERS; + +public final class AnsiConsoleSupportHolder { + + static final AnsiConsoleSupport PROVIDER; + + static final Throwable ERR; + + private static AnsiConsoleSupport getDefaultProvider() { + if (!OSInfo.isInImageCode()) { + try { + // Call the specialized constructor to check whether the module has native access enabled + // If not, fallback to JNI to avoid the JDK printing warnings in stderr + return (AnsiConsoleSupport) Class.forName("org.fusesource.jansi.internal.ffm.AnsiConsoleSupportImpl") + .getConstructor(boolean.class) + .newInstance(true); + } catch (Throwable ignored) { + } + } + + return new org.fusesource.jansi.internal.jni.AnsiConsoleSupportImpl(); + } + + private static AnsiConsoleSupport findProvider(String providerList) { + String[] providers = providerList.split(","); + + RuntimeException error = null; + + for (String provider : providers) { + try { + return (AnsiConsoleSupport) + Class.forName("org.fusesource.jansi.internal." + provider + ".AnsiConsoleSupportImpl") + .getConstructor() + .newInstance(); + } catch (Throwable t) { + if (error == null) { + error = new RuntimeException("Unable to create AnsiConsoleSupport provider"); + } + + error.addSuppressed(t); + } + } + + // User does not specify any provider, falling back to the default + if (error == null) { + return getDefaultProvider(); + } + + throw error; + } + + static { + String providerList = System.getProperty(JANSI_PROVIDERS); + + AnsiConsoleSupport ansiConsoleSupport = null; + Throwable err = null; + + try { + if (providerList == null) { + ansiConsoleSupport = getDefaultProvider(); + } else { + ansiConsoleSupport = findProvider(providerList); + } + } catch (Throwable e) { + err = e; + } + + PROVIDER = ansiConsoleSupport; + ERR = err; + } + + public static AnsiConsoleSupport getProvider() { + if (PROVIDER == null) { + throw new RuntimeException("No provider available", ERR); + } + return PROVIDER; + } + + public static String getProviderName() { + return getProvider().getProviderName(); + } + + public static AnsiConsoleSupport.CLibrary getCLibrary() { + return getProvider().getCLibrary(); + } + + public static AnsiConsoleSupport.Kernel32 getKernel32() { + return getProvider().getKernel32(); + } +} 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/JansiLoader.java b/src/main/java/org/fusesource/jansi/internal/JansiLoader.java index f705620c..1b4494f3 100644 --- a/src/main/java/org/fusesource/jansi/internal/JansiLoader.java +++ b/src/main/java/org/fusesource/jansi/internal/JansiLoader.java @@ -37,8 +37,9 @@ import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.util.Arrays; import java.util.LinkedList; import java.util.List; @@ -198,9 +199,7 @@ private static boolean extractAndLoadLibraryFile( if (!extractedLckFile.exists()) { new FileOutputStream(extractedLckFile).close(); } - try (OutputStream out = new FileOutputStream(extractedLibFile)) { - copy(in, out); - } + Files.copy(in, extractedLibFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } finally { // Delete the extracted lib file on JVM exit. extractedLibFile.deleteOnExit(); @@ -239,14 +238,6 @@ private static String randomUUID() { return Long.toHexString(new Random().nextLong()); } - private static void copy(InputStream in, OutputStream out) throws IOException { - byte[] buf = new byte[8192]; - int n; - while ((n = in.read(buf)) > 0) { - out.write(buf, 0, n); - } - } - /** * Loads native library using the given path and name of the library. * @@ -358,7 +349,7 @@ private static void loadJansiNativeLibrary() throws Exception { throw new Exception(String.format( "No native library found for os.name=%s, os.arch=%s, paths=[%s]", - OSInfo.getOSName(), OSInfo.getArchName(), join(triedPaths, File.pathSeparator))); + OSInfo.getOSName(), OSInfo.getArchName(), String.join(File.pathSeparator, triedPaths))); } private static boolean hasResource(String path) { @@ -401,16 +392,4 @@ public static String getVersion() { } return version; } - - private static String join(List list, String separator) { - StringBuilder sb = new StringBuilder(); - boolean first = true; - for (String item : list) { - if (first) first = false; - else sb.append(separator); - - sb.append(item); - } - return sb.toString(); - } } diff --git a/src/main/java/org/fusesource/jansi/internal/MingwSupport.java b/src/main/java/org/fusesource/jansi/internal/MingwSupport.java new file mode 100644 index 00000000..be0c54a2 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/MingwSupport.java @@ -0,0 +1,137 @@ +/* + * 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.ByteArrayOutputStream; +import java.io.File; +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Support for MINGW terminals. + * Those terminals do not use the underlying windows terminal and there's no CLibrary available + * in these environments. We have to rely on calling {@code stty.exe} and {@code tty.exe} to + * obtain the terminal name and width. + */ +public class MingwSupport { + + private final String sttyCommand; + private final String ttyCommand; + private final Pattern columnsPatterns; + + public MingwSupport() { + String tty = null; + String stty = null; + String path = System.getenv("PATH"); + if (path != null) { + String[] paths = path.split(File.pathSeparator); + for (String p : paths) { + File ttyFile = new File(p, "tty.exe"); + if (tty == null && ttyFile.canExecute()) { + tty = ttyFile.getAbsolutePath(); + } + File sttyFile = new File(p, "stty.exe"); + if (stty == null && sttyFile.canExecute()) { + stty = sttyFile.getAbsolutePath(); + } + } + } + if (tty == null) { + tty = "tty.exe"; + } + if (stty == null) { + stty = "stty.exe"; + } + ttyCommand = tty; + sttyCommand = stty; + // Compute patterns + columnsPatterns = Pattern.compile("\\b" + "columns" + "\\s+(\\d+)\\b"); + } + + public String getConsoleName(boolean stdout) { + try { + Process p = new ProcessBuilder(ttyCommand) + .redirectInput(getRedirect(stdout ? FileDescriptor.out : FileDescriptor.err)) + .start(); + String result = waitAndCapture(p); + if (p.exitValue() == 0) { + return result.trim(); + } + } catch (Throwable t) { + if ("java.lang.reflect.InaccessibleObjectException" + .equals(t.getClass().getName())) { + System.err.println("MINGW support requires --add-opens java.base/java.lang=ALL-UNNAMED"); + } + // ignore + } + return null; + } + + public int getTerminalWidth(String name) { + try { + Process p = new ProcessBuilder(sttyCommand, "-F", name, "-a").start(); + String result = waitAndCapture(p); + if (p.exitValue() != 0) { + throw new IOException("Error executing '" + sttyCommand + "': " + result); + } + Matcher matcher = columnsPatterns.matcher(result); + if (matcher.find()) { + return Integer.parseInt(matcher.group(1)); + } + throw new IOException("Unable to parse columns"); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static String waitAndCapture(Process p) throws IOException, InterruptedException { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + try (InputStream in = p.getInputStream(); + InputStream err = p.getErrorStream()) { + int c; + while ((c = in.read()) != -1) { + bout.write(c); + } + while ((c = err.read()) != -1) { + bout.write(c); + } + p.waitFor(); + } + return bout.toString(); + } + + /** + * This requires --add-opens java.base/java.lang=ALL-UNNAMED + */ + private ProcessBuilder.Redirect getRedirect(FileDescriptor fd) throws ReflectiveOperationException { + // This is not really allowed, but this is the only way to redirect the output or error stream + // to the input. This is definitely not something you'd usually want to do, but in the case of + // the `tty` utility, it provides a way to get + Class rpi = Class.forName("java.lang.ProcessBuilder$RedirectPipeImpl"); + Constructor cns = rpi.getDeclaredConstructor(); + cns.setAccessible(true); + ProcessBuilder.Redirect input = (ProcessBuilder.Redirect) cns.newInstance(); + Field f = rpi.getDeclaredField("fd"); + f.setAccessible(true); + f.set(input, fd); + return input; + } +} diff --git a/src/main/java/org/fusesource/jansi/internal/NativeImageFeature.java b/src/main/java/org/fusesource/jansi/internal/NativeImageFeature.java new file mode 100644 index 00000000..27063a11 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/NativeImageFeature.java @@ -0,0 +1,84 @@ +/* + * 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.util.Objects; + +import org.fusesource.jansi.AnsiConsole; +import org.graalvm.nativeimage.hosted.Feature; +import org.graalvm.nativeimage.hosted.RuntimeClassInitialization; +import org.graalvm.nativeimage.hosted.RuntimeSystemProperties; + +public class NativeImageFeature implements Feature { + @Override + public String getURL() { + return "https://github.com/fusesource/jansi"; + } + + @Override + public void duringSetup(DuringSetupAccess access) { + RuntimeClassInitialization.initializeAtBuildTime(AnsiConsoleSupportHolder.class); + + String providers = System.getProperty(AnsiConsole.JANSI_PROVIDERS); + if (providers != null) { + try { + RuntimeSystemProperties.register(AnsiConsole.JANSI_PROVIDERS, providers); + } catch (Throwable ignored) { + // GraalVM version < 23.0 + // No need to worry as we select the provider at build time + } + } + + String provider = Objects.requireNonNull(AnsiConsoleSupportHolder.getProviderName(), "No provider available"); + if (provider.equals(AnsiConsole.JANSI_PROVIDER_JNI)) { + String jansiNativeLibraryName = System.mapLibraryName("jansi"); + if (jansiNativeLibraryName.endsWith(".dylib")) { + jansiNativeLibraryName = jansiNativeLibraryName.replace(".dylib", ".jnilib"); + } + + String packagePath = JansiLoader.class.getPackage().getName().replace('.', '/'); + + try { + Class moduleClass = Class.forName("java.lang.Module"); + Class rraClass = Class.forName("org.graalvm.nativeimage.hosted.RuntimeResourceAccess"); + + Object module = Class.class.getMethod("getModule").invoke(JansiLoader.class); + rraClass.getMethod("addResource", moduleClass, String.class) + .invoke( + null, + module, + String.format( + "%s/native/%s/%s", + packagePath, + OSInfo.getNativeLibFolderPathForCurrentOS(), + jansiNativeLibraryName)); + + } catch (Throwable ignored) { + // GraalVM version < 22.3 + // Users need to manually add the JNI library as resources + } + } else if (provider.equals(AnsiConsole.JANSI_PROVIDER_FFM)) { + try { + // FFM is only available in JDK 21+, so we need to compile it separately + Class.forName("org.fusesource.jansi.internal.ffm.NativeImageDowncallRegister") + .getMethod("registerForDowncall") + .invoke(null); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/src/main/java/org/fusesource/jansi/internal/OSInfo.java b/src/main/java/org/fusesource/jansi/internal/OSInfo.java index 8c9999ca..2d0ce235 100644 --- a/src/main/java/org/fusesource/jansi/internal/OSInfo.java +++ b/src/main/java/org/fusesource/jansi/internal/OSInfo.java @@ -34,12 +34,12 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.util.HashMap; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.Locale; /** * Provides OS name and architecture name. - * */ public class OSInfo { @@ -50,49 +50,7 @@ public class OSInfo { public static final String PPC = "ppc"; public static final String PPC64 = "ppc64"; public static final String ARM64 = "arm64"; - - private static final HashMap archMapping = new HashMap(); - - static { - // x86 mappings - archMapping.put(X86, X86); - archMapping.put("i386", X86); - archMapping.put("i486", X86); - archMapping.put("i586", X86); - archMapping.put("i686", X86); - archMapping.put("pentium", X86); - - // x86_64 mappings - archMapping.put(X86_64, X86_64); - archMapping.put("amd64", X86_64); - archMapping.put("em64t", X86_64); - archMapping.put("universal", X86_64); // Needed for openjdk7 in Mac - - // Itenium 64-bit mappings - archMapping.put(IA64, IA64); - archMapping.put("ia64w", IA64); - - // Itenium 32-bit mappings, usually an HP-UX construct - archMapping.put(IA64_32, IA64_32); - archMapping.put("ia64n", IA64_32); - - // PowerPC mappings - archMapping.put(PPC, PPC); - archMapping.put("power", PPC); - archMapping.put("powerpc", PPC); - archMapping.put("power_pc", PPC); - archMapping.put("power_rs", PPC); - - // TODO: PowerPC 64bit mappings - archMapping.put(PPC64, PPC64); - archMapping.put("power64", PPC64); - archMapping.put("powerpc64", PPC64); - archMapping.put("power_pc64", PPC64); - archMapping.put("power_rs64", PPC64); - - // aarch64 mappings - archMapping.put("aarch64", ARM64); - } + public static final String RISCV64 = "riscv64"; public static void main(String[] args) { if (args.length >= 1) { @@ -108,6 +66,63 @@ public static void main(String[] args) { System.out.print(getNativeLibFolderPathForCurrentOS()); } + private static String mapArchName(String arch) { + switch (arch.toLowerCase(Locale.ROOT)) { + // x86 mappings + case X86: + case "i386": + case "i486": + case "i586": + case "i686": + case "pentium": + return X86; + + // x86_64 mappings + case X86_64: + case "amd64": + case "em64t": + case "universal": // Needed for openjdk7 in Mac + return X86_64; + + // Itenium 64-bit mappings + case IA64: + case "ia64w": + return IA64; + + // Itenium 32-bit mappings, usually an HP-UX construct + case IA64_32: + case "ia64n": + return IA64_32; + + // PowerPC mappings + case PPC: + case "power": + case "powerpc": + case "power_pc": + case "power_rs": + return PPC; + + // TODO: PowerPC 64bit mappings + case PPC64: + case "power64": + case "powerpc64": + case "power_pc64": + case "power_rs64": + return PPC64; + + // aarch64 mappings + case "aarch64": + return ARM64; + + // riscv64 mappings + case RISCV64: + return RISCV64; + + default: + return null; + } + } + public static String getNativeLibFolderPathForCurrentOS() { return getOSName() + "/" + getArchName(); } @@ -116,25 +131,31 @@ public static String getOSName() { return translateOSNameToFolderName(System.getProperty("os.name")); } + public static boolean isWindows() { + return System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win"); + } + public static boolean isAndroid() { - return System.getProperty("java.runtime.name", "").toLowerCase().contains("android"); + return System.getProperty("java.runtime.name", "") + .toLowerCase(Locale.ROOT) + .contains("android"); } public static boolean isAlpine() { try { - Process p = Runtime.getRuntime().exec("cat /etc/os-release | grep ^ID"); - p.waitFor(); - - InputStream in = p.getInputStream(); - try { - return readFully(in).toLowerCase().contains("alpine"); - } finally { - in.close(); + for (String line : Files.readAllLines(Paths.get("/etc/os-release"))) { + if (line.startsWith("ID") && line.toLowerCase(Locale.ROOT).contains("alpine")) { + return true; + } } - - } catch (Throwable e) { - return false; + } catch (Throwable ignored) { } + + return false; + } + + public static boolean isInImageCode() { + return System.getProperty("org.graalvm.nativeimage.imagecode") != null; } static String getHardwareName() { @@ -161,7 +182,7 @@ private static String readFully(InputStream in) throws IOException { while ((readLen = in.read(buf, 0, buf.length)) >= 0) { b.write(buf, 0, readLen); } - return b.toString(); + return b.toString("UTF-8"); } static String resolveArmArchType() { @@ -203,8 +224,10 @@ public static String getArchName() { if (osArch.startsWith("arm")) { osArch = resolveArmArchType(); } else { - String lc = osArch.toLowerCase(Locale.US); - if (archMapping.containsKey(lc)) return archMapping.get(lc); + String arch = mapArchName(osArch); + if (arch != null) { + return arch; + } } return translateArchNameToFolderName(osArch); } 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/internal/ffm/AnsiConsoleSupportImpl.java b/src/main/java/org/fusesource/jansi/internal/ffm/AnsiConsoleSupportImpl.java new file mode 100644 index 00000000..b36bc692 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/ffm/AnsiConsoleSupportImpl.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.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.internal.AnsiConsoleSupport; +import org.fusesource.jansi.internal.OSInfo; +import org.fusesource.jansi.io.AnsiProcessor; + +import static org.fusesource.jansi.internal.ffm.Kernel32.*; + +public final class AnsiConsoleSupportImpl extends AnsiConsoleSupport { + + public AnsiConsoleSupportImpl() { + super("ffm"); + } + + public AnsiConsoleSupportImpl(boolean checkNativeAccess) { + this(); + if (checkNativeAccess && !AnsiConsoleSupportImpl.class.getModule().isNativeAccessEnabled()) { + throw new UnsupportedOperationException( + "Native access is not enabled for the current module: " + AnsiConsoleSupportImpl.class.getModule()); + } + } + + @Override + protected CLibrary createCLibrary() { + if (OSInfo.isWindows()) { + return new WindowsCLibrary(); + } else { + return new PosixCLibrary(); + } + } + + @Override + protected Kernel32 createKernel32() { + return new Kernel32() { + @Override + public int isTty(long console) { + int[] mode = new int[1]; + return getConsoleMode(console, mode); + } + + @Override + public int getTerminalWidth(long console) { + try (Arena arena = Arena.ofConfined()) { + CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(arena); + 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 arena = Arena.ofConfined()) { + MemorySegment written = arena.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) { + return org.fusesource.jansi.internal.ffm.Kernel32.getErrorMessage(errorCode); + } + + @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/internal/ffm/Kernel32.java b/src/main/java/org/fusesource/jansi/internal/ffm/Kernel32.java new file mode 100644 index 00000000..bed95e19 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/ffm/Kernel32.java @@ -0,0 +1,827 @@ +/* + * 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.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.nio.charset.StandardCharsets; +import java.util.Objects; + +import static java.lang.foreign.ValueLayout.*; + +@SuppressWarnings("unused") +final 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) 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, dwMessageId, dwLanguageId, lpBuffer, nSize, Arguments); + } 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, 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, lpMode); + } 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); + } 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, cCharacter, nLength, dwWriteCoord.seg, lpNumberOfCharsWritten); + } 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); + } 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, lpBuffer, nLength, lpNumberOfEventsRead); + } 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, lpBuffer, nLength, lpNumberOfEventsRead); + } 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, 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(ScrollConsoleScreenBufferW$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() { + MethodHandle mh$ = requireNonNull(GetLastError$MH, "GetLastError"); + try { + return (int) mh$.invokeExact(); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int GetFileType(MemorySegment hFile) { + MethodHandle mh$ = requireNonNull(GetFileType$MH, "GetFileType"); + try { + return (int) mh$.invokeExact(hFile); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static MemorySegment _get_osfhandle(int fd) { + MethodHandle mh$ = requireNonNull(_get_osfhandle$MH, "_get_osfhandle"); + try { + return (MemorySegment) mh$.invokeExact(fd); + } 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 arena = Arena.ofConfined()) { + MemorySegment inputRecordPtr = arena.allocateArray(INPUT_RECORD.LAYOUT, count); + MemorySegment length = arena.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; + try (Arena arena = Arena.ofConfined()) { + MemorySegment data = arena.allocate(bufferSize); + FormatMessageW( + FORMAT_MESSAGE_FROM_SYSTEM, MemorySegment.NULL, errorCode, 0, data, bufferSize, MemorySegment.NULL); + return new String(data.toArray(JAVA_BYTE), StandardCharsets.UTF_16LE).trim(); + } + } + + private static final SymbolLookup SYMBOL_LOOKUP; + + static { + System.loadLibrary("msvcrt"); + System.loadLibrary("Kernel32"); + SYMBOL_LOOKUP = SymbolLookup.loaderLookup(); + } + + static MethodHandle downcallHandle(String name, FunctionDescriptor fdesc) { + return SYMBOL_LOOKUP + .find(name) + .map(addr -> Linker.nativeLinker().downcallHandle(addr, fdesc)) + .orElse(null); + } + + static final OfBoolean C_BOOL$LAYOUT = JAVA_BOOLEAN; + static final OfByte C_CHAR$LAYOUT = JAVA_BYTE; + static final OfChar C_WCHAR$LAYOUT = JAVA_CHAR; + static final OfShort C_SHORT$LAYOUT = JAVA_SHORT; + static final OfShort C_WORD$LAYOUT = JAVA_SHORT; + static final OfInt C_DWORD$LAYOUT = JAVA_INT; + static final OfInt C_INT$LAYOUT = JAVA_INT; + static final OfLong C_LONG$LAYOUT = JAVA_LONG; + static final OfLong C_LONG_LONG$LAYOUT = JAVA_LONG; + static final OfFloat C_FLOAT$LAYOUT = JAVA_FLOAT; + static final OfDouble C_DOUBLE$LAYOUT = JAVA_DOUBLE; + static final AddressLayout C_POINTER$LAYOUT = ADDRESS; + + 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 ScrollConsoleScreenBufferW$MH = downcallHandle( + "ScrollConsoleScreenBufferW", + 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)); + static final MethodHandle GetFileType$MH = + downcallHandle("GetFileType", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT)); + static final MethodHandle _get_osfhandle$MH = + downcallHandle("_get_osfhandle", FunctionDescriptor.of(C_POINTER$LAYOUT, C_INT$LAYOUT)); + + public static final 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; + + 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 final 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; + + 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 final 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; + + 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 final 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; + + 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 final 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; + + 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 final 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; + + 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 final 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(Arena arena) { + this(arena.allocate(LAYOUT)); + } + + public CHAR_INFO(Arena arena, char c, short a) { + this(arena); + 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 final 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(Arena arena) { + this(arena.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 final 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(Arena arena) { + this(arena.allocate(LAYOUT)); + } + + public COORD(Arena arena, short x, short y) { + this(arena.allocate(LAYOUT)); + x(x); + y(y); + } + + 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(Arena arena) { + return new COORD(arena.allocate(LAYOUT).copyFrom(seg)); + } + } + + public static final 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; + + 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(Arena arena) { + return new SMALL_RECT(arena.allocate(LAYOUT).copyFrom(seg)); + } + } + + 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/internal/ffm/NativeImageDowncallRegister.java b/src/main/java/org/fusesource/jansi/internal/ffm/NativeImageDowncallRegister.java new file mode 100644 index 00000000..3052b40f --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/ffm/NativeImageDowncallRegister.java @@ -0,0 +1,103 @@ +/* + * 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.ffm; + +import java.lang.foreign.AddressLayout; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.StructLayout; +import java.lang.foreign.ValueLayout; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.hosted.RuntimeForeignAccess; + +import static java.lang.foreign.ValueLayout.*; + +public final class NativeImageDowncallRegister { + + private static void registerForDowncall(MemoryLayout resLayout, MemoryLayout... argLayouts) { + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(resLayout, argLayouts)); + } + + public static void registerForDowncall() { + if (Platform.includedIn(Platform.WINDOWS.class)) { + final OfShort C_SHORT$LAYOUT = JAVA_SHORT; + final OfInt C_INT$LAYOUT = JAVA_INT; + final AddressLayout C_POINTER$LAYOUT = ADDRESS; + + StructLayout COORD$LAYOUT = + MemoryLayout.structLayout(C_SHORT$LAYOUT.withName("x"), C_SHORT$LAYOUT.withName("y")); + + // WaitForSingleObject + registerForDowncall(C_INT$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT); + // GetStdHandle + registerForDowncall(C_POINTER$LAYOUT, C_INT$LAYOUT); + // FormatMessageW + registerForDowncall( + C_INT$LAYOUT, + C_INT$LAYOUT, + C_POINTER$LAYOUT, + C_INT$LAYOUT, + C_INT$LAYOUT, + C_POINTER$LAYOUT, + C_INT$LAYOUT, + C_POINTER$LAYOUT); + // SetConsoleTextAttribute + registerForDowncall(C_INT$LAYOUT, C_POINTER$LAYOUT, C_SHORT$LAYOUT); + // SetConsoleMode + registerForDowncall(C_INT$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT); + // GetConsoleMode + registerForDowncall(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT); + // SetConsoleTitleW + registerForDowncall(C_INT$LAYOUT, C_POINTER$LAYOUT); + // SetConsoleCursorPosition + registerForDowncall(C_INT$LAYOUT, C_POINTER$LAYOUT, COORD$LAYOUT); + // FillConsoleOutputCharacterW + registerForDowncall( + C_INT$LAYOUT, C_POINTER$LAYOUT, C_SHORT$LAYOUT, C_INT$LAYOUT, COORD$LAYOUT, C_POINTER$LAYOUT); + // FillConsoleOutputAttribute + registerForDowncall( + C_INT$LAYOUT, C_POINTER$LAYOUT, C_SHORT$LAYOUT, C_INT$LAYOUT, COORD$LAYOUT, C_POINTER$LAYOUT); + // WriteConsoleW + registerForDowncall( + C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT); + // ReadConsoleInputW + registerForDowncall(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT, C_POINTER$LAYOUT); + // PeekConsoleInputW + registerForDowncall(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT, C_POINTER$LAYOUT); + // GetConsoleScreenBufferInfo + registerForDowncall(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT); + // ScrollConsoleScreenBuffer + registerForDowncall( + C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT, COORD$LAYOUT, C_POINTER$LAYOUT); + // GetLastError + registerForDowncall(C_INT$LAYOUT); + // GetFileType + registerForDowncall(C_INT$LAYOUT, C_POINTER$LAYOUT); + // _get_osfhandle + registerForDowncall(C_POINTER$LAYOUT, C_INT$LAYOUT); + // NtQueryObject + registerForDowncall(JAVA_INT, ADDRESS, JAVA_INT, ADDRESS, JAVA_LONG, ADDRESS); + } else if (Platform.includedIn(Platform.LINUX.class) || Platform.includedIn(Platform.DARWIN.class)) { + // ioctl + registerForDowncall(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS); + // isatty + registerForDowncall(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT); + } else { + throw new UnsupportedOperationException("Unsupported platform"); + } + } +} diff --git a/src/main/java/org/fusesource/jansi/internal/ffm/PosixCLibrary.java b/src/main/java/org/fusesource/jansi/internal/ffm/PosixCLibrary.java new file mode 100644 index 00000000..817e03e4 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/ffm/PosixCLibrary.java @@ -0,0 +1,91 @@ +/* + * 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.ffm; + +import java.lang.foreign.*; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.VarHandle; + +import org.fusesource.jansi.internal.AnsiConsoleSupport; +import org.fusesource.jansi.internal.OSInfo; + +final class PosixCLibrary implements AnsiConsoleSupport.CLibrary { + private static final int TIOCGWINSZ; + private static final GroupLayout wsLayout; + private static final MethodHandle ioctl; + private static final VarHandle ws_col; + private static final MethodHandle isatty; + + 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(); + } + + 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(); + SymbolLookup lookup; + if (OSInfo.isInImageCode()) { + lookup = SymbolLookup.loaderLookup(); + } else { + lookup = linker.defaultLookup(); + } + + ioctl = linker.downcallHandle( + lookup.find("ioctl").get(), + FunctionDescriptor.of( + ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS), + Linker.Option.firstVariadicArg(2)); + isatty = linker.downcallHandle( + lookup.find("isatty").get(), FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT)); + } + + @Override + public short getTerminalWidth(int fd) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment segment = arena.allocate(wsLayout); + 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); + } + } +} diff --git a/src/main/java/org/fusesource/jansi/internal/ffm/WindowsAnsiProcessor.java b/src/main/java/org/fusesource/jansi/internal/ffm/WindowsAnsiProcessor.java new file mode 100644 index 00000000..cc6789e9 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/ffm/WindowsAnsiProcessor.java @@ -0,0 +1,435 @@ +/* + * 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.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 java.nio.charset.StandardCharsets; + +import org.fusesource.jansi.WindowsSupport; +import org.fusesource.jansi.io.AnsiProcessor; +import org.fusesource.jansi.io.Colors; + +import static org.fusesource.jansi.internal.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(Arena.ofAuto()); + 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()) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + + @Override + protected void processEraseScreen(int eraseOption) throws IOException { + getConsoleInfo(); + try (Arena arena = Arena.ofConfined()) { + MemorySegment written = arena.allocate(ValueLayout.JAVA_INT); + switch (eraseOption) { + case ERASE_SCREEN: + COORD topLeft = new COORD(arena); + 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(arena); + 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(), written); + FillConsoleOutputCharacterW(console, ' ', lengthToEnd, info.cursorPosition(), written); + break; + default: + break; + } + } + } + + @Override + protected void processEraseLine(int eraseOption) throws IOException { + getConsoleInfo(); + try (Arena arena = Arena.ofConfined()) { + MemorySegment written = arena.allocate(ValueLayout.JAVA_INT); + switch (eraseOption) { + case ERASE_LINE: + COORD leftColCurrRow = info.cursorPosition().copy(arena); + 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(arena); + 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(), written); + FillConsoleOutputCharacterW(console, ' ', lengthToLastCol, info.cursorPosition(), 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(); + try (Arena arena = Arena.ofConfined()) { + SMALL_RECT scroll = info.window().copy(arena); + scroll.top(info.cursorPosition().y()); + COORD org = new COORD(arena); + org.x((short) 0); + org.y((short) (info.cursorPosition().y() + optionInt)); + CHAR_INFO info = new CHAR_INFO(arena, ' ', originalColors); + if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + } + + @Override + protected void processDeleteLine(int optionInt) throws IOException { + getConsoleInfo(); + try (Arena arena = Arena.ofConfined()) { + SMALL_RECT scroll = info.window().copy(arena); + scroll.top(info.cursorPosition().y()); + COORD org = new COORD(arena); + org.x((short) 0); + org.y((short) (info.cursorPosition().y() - optionInt)); + CHAR_INFO info = new CHAR_INFO(arena, ' ', originalColors); + if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + } + + @Override + protected void processChangeWindowTitle(String title) { + try (Arena arena = Arena.ofConfined()) { + byte[] bytes = title.getBytes(StandardCharsets.UTF_16LE); + MemorySegment str = arena.allocate(bytes.length + 2); + MemorySegment.copy(bytes, 0, str, ValueLayout.JAVA_BYTE, 0, bytes.length); + SetConsoleTitleW(str); + } + } +} diff --git a/src/main/java/org/fusesource/jansi/internal/ffm/WindowsCLibrary.java b/src/main/java/org/fusesource/jansi/internal/ffm/WindowsCLibrary.java new file mode 100644 index 00000000..2acfedb3 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/ffm/WindowsCLibrary.java @@ -0,0 +1,116 @@ +/* + * 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.ffm; + +import java.lang.foreign.*; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.VarHandle; +import java.nio.charset.StandardCharsets; + +import org.fusesource.jansi.internal.AnsiConsoleSupport; + +import static java.lang.foreign.ValueLayout.*; + +final class WindowsCLibrary implements AnsiConsoleSupport.CLibrary { + + private static final int FILE_TYPE_CHAR = 0x0002; + + private static final int ObjectNameInformation = 1; + + private static final MethodHandle NtQueryObject; + private static final VarHandle UNICODE_STRING_LENGTH; + private static final VarHandle UNICODE_STRING_BUFFER; + + static { + MethodHandle ntQueryObjectHandle = null; + try { + SymbolLookup ntDll = SymbolLookup.libraryLookup("ntdll", Arena.ofAuto()); + + ntQueryObjectHandle = ntDll.find("NtQueryObject") + .map(addr -> Linker.nativeLinker() + .downcallHandle( + addr, + FunctionDescriptor.of(JAVA_INT, ADDRESS, JAVA_INT, ADDRESS, JAVA_LONG, ADDRESS))) + .orElse(null); + } catch (Throwable ignored) { + } + + NtQueryObject = ntQueryObjectHandle; + + StructLayout unicodeStringLayout; + if (ADDRESS.byteSize() == 8) { + unicodeStringLayout = MemoryLayout.structLayout( + JAVA_SHORT.withName("Length"), + JAVA_SHORT.withName("MaximumLength"), + MemoryLayout.paddingLayout(4), + ADDRESS.withTargetLayout(JAVA_BYTE).withName("Buffer")); + } else { + // 32 Bit + unicodeStringLayout = MemoryLayout.structLayout( + JAVA_SHORT.withName("Length"), + JAVA_SHORT.withName("MaximumLength"), + ADDRESS.withTargetLayout(JAVA_BYTE).withName("Buffer")); + } + + UNICODE_STRING_LENGTH = unicodeStringLayout.varHandle(PathElement.groupElement("Length")); + UNICODE_STRING_BUFFER = unicodeStringLayout.varHandle(PathElement.groupElement("Buffer")); + } + + @Override + public short getTerminalWidth(int fd) { + throw new UnsupportedOperationException("Windows does not support ioctl"); + } + + @Override + public int isTty(int fd) { + try (Arena arena = Arena.ofConfined()) { + // check if fd is a pipe + MemorySegment h = Kernel32._get_osfhandle(fd); + int t = Kernel32.GetFileType(h); + if (t == FILE_TYPE_CHAR) { + // check that this is a real tty because the /dev/null + // and /dev/zero streams are also of type FILE_TYPE_CHAR + return Kernel32.GetConsoleMode(h, arena.allocate(JAVA_INT)); + } + + if (NtQueryObject == null) { + return 0; + } + + final int BUFFER_SIZE = 1024; + + MemorySegment buffer = arena.allocate(BUFFER_SIZE); + MemorySegment result = arena.allocate(JAVA_LONG); + + int res = (int) NtQueryObject.invokeExact(h, ObjectNameInformation, buffer, BUFFER_SIZE - 2, result); + if (res != 0) { + return 0; + } + + int stringLength = Short.toUnsignedInt((Short) UNICODE_STRING_LENGTH.get(buffer)); + MemorySegment stringBuffer = ((MemorySegment) UNICODE_STRING_BUFFER.get(buffer)).reinterpret(stringLength); + + String str = new String(stringBuffer.toArray(JAVA_BYTE), StandardCharsets.UTF_16LE).trim(); + if (str.startsWith("msys-") || str.startsWith("cygwin-") || str.startsWith("-pty")) { + return 1; + } + + return 0; + } catch (Throwable e) { + throw new AssertionError("should not reach here", e); + } + } +} diff --git a/src/main/java/org/fusesource/jansi/internal/jni/AnsiConsoleSupportImpl.java b/src/main/java/org/fusesource/jansi/internal/jni/AnsiConsoleSupportImpl.java new file mode 100644 index 00000000..e3ae4d67 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/jni/AnsiConsoleSupportImpl.java @@ -0,0 +1,107 @@ +/* + * 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.jni; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +import org.fusesource.jansi.internal.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 final class AnsiConsoleSupportImpl extends AnsiConsoleSupport { + + public AnsiConsoleSupportImpl() { + super("jni"); + } + + @Override + protected CLibrary createCLibrary() { + 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 + protected Kernel32 createKernel32() { + 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/io/AnsiOutputStream.java b/src/main/java/org/fusesource/jansi/io/AnsiOutputStream.java index d925d728..9c45afef 100644 --- a/src/main/java/org/fusesource/jansi/io/AnsiOutputStream.java +++ b/src/main/java/org/fusesource/jansi/io/AnsiOutputStream.java @@ -25,6 +25,8 @@ import org.fusesource.jansi.AnsiMode; import org.fusesource.jansi.AnsiType; +import static java.nio.charset.StandardCharsets.US_ASCII; + /** * A ANSI print stream extracts ANSI escape codes written to * an output stream and calls corresponding AnsiProcessor.process* methods. @@ -38,12 +40,14 @@ */ public class AnsiOutputStream extends FilterOutputStream { - public static final byte[] RESET_CODE = "\033[0m".getBytes(); + public static final byte[] RESET_CODE = "\033[0m".getBytes(US_ASCII); + @FunctionalInterface public interface IoRunnable { void run() throws IOException; } + @FunctionalInterface public interface WidthSupplier { int getTerminalWidth(); } @@ -79,7 +83,7 @@ public int getTerminalWidth() { private final byte[] buffer = new byte[MAX_ESCAPE_SEQUENCE_LENGTH]; private int pos = 0; private int startOfValue; - private final ArrayList options = new ArrayList(); + private final ArrayList options = new ArrayList<>(); private int state = LOOKING_FOR_FIRST_ESC_CHAR; private final Charset cs; diff --git a/src/main/java/org/fusesource/jansi/io/FastBufferedOutputStream.java b/src/main/java/org/fusesource/jansi/io/FastBufferedOutputStream.java index 823e8019..e436c35e 100644 --- a/src/main/java/org/fusesource/jansi/io/FastBufferedOutputStream.java +++ b/src/main/java/org/fusesource/jansi/io/FastBufferedOutputStream.java @@ -24,7 +24,7 @@ */ public class FastBufferedOutputStream extends FilterOutputStream { - protected final byte buf[] = new byte[8192]; + protected final byte[] buf = new byte[8192]; protected int count; public FastBufferedOutputStream(OutputStream out) { 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); } } diff --git a/src/main/native/jansi_isatty.c b/src/main/native/jansi_isatty.c index 0fc7d348..807cbe65 100644 --- a/src/main/native/jansi_isatty.c +++ b/src/main/native/jansi_isatty.c @@ -57,8 +57,8 @@ JNIEXPORT jint JNICALL CLibrary_NATIVE(isatty) /* check if fd is a pipe */ HANDLE h = (HANDLE) _get_osfhandle(arg0); - DWORD t = GetFileType(h); - if (t == FILE_TYPE_CHAR) { + DWORD t = h != NULL ? GetFileType(h) : 0; + if (h != NULL && t == FILE_TYPE_CHAR) { // check that this is a real tty because the /dev/null // and /dev/zero streams are also of type FILE_TYPE_CHAR rc = GetConsoleMode(h, &mode) != 0; @@ -84,20 +84,25 @@ JNIEXPORT jint JNICALL CLibrary_NATIVE(isatty) else { name = nameinfo->Name.Buffer; - name[nameinfo->Name.Length / 2] = 0; - - //fprintf( stderr, "Standard stream %d: pipe name: %S\n", arg0, name); - - /* - * Check if this could be a MSYS2 pty pipe ('msys-XXXX-ptyN-XX') - * or a cygwin pty pipe ('cygwin-XXXX-ptyN-XX') - */ - if ((wcsstr(name, L"msys-") || wcsstr(name, L"cygwin-")) && wcsstr(name, L"-pty")) { - rc = 1; - } else { - // This is definitely not a tty - rc = 0; + if (name == NULL) { + rc = 0; } + else { + name[nameinfo->Name.Length / 2] = 0; + + //fprintf( stderr, "Standard stream %d: pipe name: %S\n", arg0, name); + + /* + * Check if this could be a MSYS2 pty pipe ('msys-XXXX-ptyN-XX') + * or a cygwin pty pipe ('cygwin-XXXX-ptyN-XX') + */ + if ((wcsstr(name, L"msys-") || wcsstr(name, L"cygwin-")) && wcsstr(name, L"-pty")) { + rc = 1; + } else { + // This is definitely not a tty + rc = 0; + } + } } } } diff --git a/src/main/resources/META-INF/native-image/jansi/native-image.properties b/src/main/resources/META-INF/native-image/jansi/native-image.properties new file mode 100644 index 00000000..6cc8f4e4 --- /dev/null +++ b/src/main/resources/META-INF/native-image/jansi/native-image.properties @@ -0,0 +1 @@ +Args=--features=org.fusesource.jansi.internal.NativeImageFeature \ No newline at end of file diff --git a/src/main/resources/META-INF/native-image/jansi/resource-config.json b/src/main/resources/META-INF/native-image/jansi/resource-config.json index e062c81e..794d8996 100644 --- a/src/main/resources/META-INF/native-image/jansi/resource-config.json +++ b/src/main/resources/META-INF/native-image/jansi/resource-config.json @@ -1,7 +1,6 @@ { "resources": [ {"pattern": "org/fusesource/jansi/jansi.properties"}, - {"pattern": "org/fusesource/jansi/jansi.txt"}, - {"pattern": "org/fusesource/jansi/internal/native/.*"} + {"pattern": "org/fusesource/jansi/jansi.txt"} ] } \ No newline at end of file diff --git a/src/main/resources/org/fusesource/jansi/internal/native/Linux/armv6/libjansi.so b/src/main/resources/org/fusesource/jansi/internal/native/Linux/armv6/libjansi.so index 3d9631eb..9a240b95 100755 Binary files a/src/main/resources/org/fusesource/jansi/internal/native/Linux/armv6/libjansi.so and b/src/main/resources/org/fusesource/jansi/internal/native/Linux/armv6/libjansi.so differ diff --git a/src/main/resources/org/fusesource/jansi/internal/native/Windows/arm64/libjansi.so b/src/main/resources/org/fusesource/jansi/internal/native/Windows/arm64/libjansi.so index b7e3527b..cce0178d 100755 Binary files a/src/main/resources/org/fusesource/jansi/internal/native/Windows/arm64/libjansi.so and b/src/main/resources/org/fusesource/jansi/internal/native/Windows/arm64/libjansi.so differ diff --git a/src/main/resources/org/fusesource/jansi/internal/native/Windows/x86/jansi.dll b/src/main/resources/org/fusesource/jansi/internal/native/Windows/x86/jansi.dll index 298e99d2..8843d024 100755 Binary files a/src/main/resources/org/fusesource/jansi/internal/native/Windows/x86/jansi.dll and b/src/main/resources/org/fusesource/jansi/internal/native/Windows/x86/jansi.dll differ diff --git a/src/main/resources/org/fusesource/jansi/internal/native/Windows/x86_64/jansi.dll b/src/main/resources/org/fusesource/jansi/internal/native/Windows/x86_64/jansi.dll index 8649e32c..aeec4e3a 100755 Binary files a/src/main/resources/org/fusesource/jansi/internal/native/Windows/x86_64/jansi.dll and b/src/main/resources/org/fusesource/jansi/internal/native/Windows/x86_64/jansi.dll differ diff --git a/src/main/resources/org/fusesource/jansi/jansi.txt b/src/main/resources/org/fusesource/jansi/jansi.txt index 247afd25..a62a6f40 100644 --- a/src/main/resources/org/fusesource/jansi/jansi.txt +++ b/src/main/resources/org/fusesource/jansi/jansi.txt @@ -1,5 +1,5 @@ -[?7h -┌──┐┌─────┐ ┌─────┐ ┌──────┬──┐ +[?7h +┌──┐┌─────┐ ┌─────┐ ┌──────┬──┐ │██├┘█████└┬┘█████└┬┘██████│▐▌│ ┌──┐ │██│██▄▄▄██│██┌─┐██│██▄▄▄▄ │▄▄│ │▒▒└─┘▒█│▒█┌─┐▒█│▒█│ │▒█│ ▀▀▀▀▒█│▒█│ diff --git a/src/test/java/org/fusesource/jansi/AnsiTest.java b/src/test/java/org/fusesource/jansi/AnsiTest.java index 2e4a6453..1fbd5bc3 100644 --- a/src/test/java/org/fusesource/jansi/AnsiTest.java +++ b/src/test/java/org/fusesource/jansi/AnsiTest.java @@ -15,12 +15,22 @@ */ package org.fusesource.jansi; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.nio.file.Path; +import java.nio.file.Paths; + import org.fusesource.jansi.Ansi.Color; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests for the {@link Ansi} class. @@ -30,20 +40,10 @@ public class AnsiTest { @Test public void testSetEnabled() throws Exception { Ansi.setEnabled(false); - new Thread() { - @Override - public void run() { - assertEquals(false, Ansi.isEnabled()); - } - }.run(); + new Thread(() -> assertFalse(Ansi.isEnabled())).run(); Ansi.setEnabled(true); - new Thread() { - @Override - public void run() { - assertEquals(true, Ansi.isEnabled()); - } - }.run(); + new Thread(() -> assertTrue(Ansi.isEnabled())).run(); } @Test @@ -56,15 +56,7 @@ public void testClone() throws CloneNotSupportedException { @Test public void testApply() { - assertEquals( - "test", - Ansi.ansi() - .apply(new Ansi.Consumer() { - public void apply(Ansi ansi) { - ansi.a("test"); - } - }) - .toString()); + assertEquals("test", Ansi.ansi().apply(ansi -> ansi.a("test")).toString()); } @ParameterizedTest @@ -181,6 +173,34 @@ public void testColorDisabled() { } } + @Test + @EnabledOnOs(OS.WINDOWS) + @Disabled("Does not really fail: launch `javaw -jar jansi-xxx.jar` directly instead") + public void testAnsiMainWithNoConsole() throws Exception { + Path javaHome = Paths.get(System.getProperty("java.home")); + Path java = javaHome.resolve("bin\\javaw.exe"); + String cp = System.getProperty("java.class.path"); + + Process process = new ProcessBuilder() + .command(java.toString(), "-cp", cp, AnsiMain.class.getName()) + .start(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (InputStream in = process.getInputStream()) { + byte[] buffer = new byte[8192]; + while (true) { + int nb = in.read(buffer); + if (nb > 0) { + baos.write(buffer, 0, nb); + } else { + break; + } + } + } + + assertTrue(baos.toString().contains("test on System.out"), baos.toString()); + } + private static void assertAnsi(String expected, Ansi actual) { assertEquals(expected.replace("ESC", "\033"), actual.toString()); } diff --git a/src/test/java/org/fusesource/jansi/EncodingTest.java b/src/test/java/org/fusesource/jansi/EncodingTest.java index 4cccc88f..d6befd2d 100644 --- a/src/test/java/org/fusesource/jansi/EncodingTest.java +++ b/src/test/java/org/fusesource/jansi/EncodingTest.java @@ -18,7 +18,7 @@ import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.io.UnsupportedEncodingException; -import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.concurrent.atomic.AtomicReference; import org.fusesource.jansi.io.AnsiOutputStream; @@ -32,7 +32,7 @@ public class EncodingTest { @Test public void testEncoding8859() throws UnsupportedEncodingException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); - final AtomicReference newLabel = new AtomicReference(); + final AtomicReference newLabel = new AtomicReference<>(); PrintStream ansi = new AnsiPrintStream( new AnsiOutputStream( baos, @@ -46,7 +46,7 @@ protected void processChangeWindowTitle(String label) { }, AnsiType.Emulation, AnsiColors.TrueColor, - Charset.forName("ISO-8859-1"), + StandardCharsets.ISO_8859_1, null, null, false), @@ -61,7 +61,7 @@ protected void processChangeWindowTitle(String label) { @Test public void testEncodingUtf8() throws UnsupportedEncodingException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); - final AtomicReference newLabel = new AtomicReference(); + final AtomicReference newLabel = new AtomicReference<>(); PrintStream ansi = new PrintStream( new AnsiOutputStream( baos, @@ -75,7 +75,7 @@ protected void processChangeWindowTitle(String label) { }, AnsiType.Emulation, AnsiColors.TrueColor, - Charset.forName("UTF-8"), + StandardCharsets.UTF_8, null, null, false), diff --git a/src/test/java/org/fusesource/jansi/io/AnsiOutputStreamTest.java b/src/test/java/org/fusesource/jansi/io/AnsiOutputStreamTest.java index 49e6dae1..96353408 100644 --- a/src/test/java/org/fusesource/jansi/io/AnsiOutputStreamTest.java +++ b/src/test/java/org/fusesource/jansi/io/AnsiOutputStreamTest.java @@ -17,7 +17,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import org.fusesource.jansi.AnsiColors; import org.fusesource.jansi.AnsiMode; @@ -38,7 +38,7 @@ void canHandleSgrsWithMultipleOptions() throws IOException { null, AnsiType.Emulation, AnsiColors.TrueColor, - Charset.forName("UTF-8"), + StandardCharsets.UTF_8, null, null, false);