From 0ec9c83ab1dfd1ca1dc19c1309e381f33ade9f09 Mon Sep 17 00:00:00 2001 From: Glavo Date: Wed, 27 Sep 2023 00:17:09 +0800 Subject: [PATCH 01/12] Support linux-riscv64 Fixes #261 --- Makefile | 7 +++++++ Makefile.common | 7 +++++++ .../org/fusesource/jansi/internal/OSInfo.java | 4 ++++ .../internal/native/Linux/armv6/libjansi.so | Bin 15088 -> 15088 bytes 4 files changed, 18 insertions(+) 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/src/main/java/org/fusesource/jansi/internal/OSInfo.java b/src/main/java/org/fusesource/jansi/internal/OSInfo.java index 8c9999ca..fe53cbb5 100644 --- a/src/main/java/org/fusesource/jansi/internal/OSInfo.java +++ b/src/main/java/org/fusesource/jansi/internal/OSInfo.java @@ -50,6 +50,7 @@ public class OSInfo { public static final String PPC = "ppc"; public static final String PPC64 = "ppc64"; public static final String ARM64 = "arm64"; + public static final String RISCV64 = "riscv64"; private static final HashMap archMapping = new HashMap(); @@ -92,6 +93,9 @@ public class OSInfo { // aarch64 mappings archMapping.put("aarch64", ARM64); + + // riscv64 mappings + archMapping.put(RISCV64, RISCV64); } public static void main(String[] args) { 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 3d9631eb2076f76753d826a253e0345219ac3fe8..9a240b955b4498865b434880d9de675d38a2d9b8 100755 GIT binary patch delta 364 zcmexR`k{0K3!})3s1t@E_g|mSUmCLD+fr7=$@bm`nWt9KxtJd zZ49NEp)?zm7J<@yP?{G?t3YWXD6I~qnV_@;l;(ucYz$l=#^i_6mj{VcHap6mXPjKX z$gx>K-j7klRL?-q$Sgi3$t=y>z*tkk&`{5KaMkp-~r8yup8v`eZG5MkNim_hS?_(KFC9F*S%cO*2h2GD_1_Ff`ONnp~t{x_N~{jRd3KWK~OL# Date: Thu, 28 Sep 2023 07:26:11 +0200 Subject: [PATCH 02/12] Fix wrong output encoding on Windows with JDK >= 19 (fixes #247) (#258) * Fix wrong output encoding on Windows with JDK >= 19 JDK 19 has changed the system properties used for System.out and System.err encoding, see https://www.oracle.com/java/technologies/javase/19-relnote-issues.html#JDK-8283620 * Fix bad background in logo and add output encoding system properties --- src/main/java/org/fusesource/jansi/AnsiConsole.java | 5 ++++- src/main/java/org/fusesource/jansi/AnsiMain.java | 4 ++++ src/main/resources/org/fusesource/jansi/jansi.txt | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/fusesource/jansi/AnsiConsole.java b/src/main/java/org/fusesource/jansi/AnsiConsole.java index 3b7b0032..9ce1b9a3 100644 --- a/src/main/java/org/fusesource/jansi/AnsiConsole.java +++ b/src/main/java/org/fusesource/jansi/AnsiConsole.java @@ -237,7 +237,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; diff --git a/src/main/java/org/fusesource/jansi/AnsiMain.java b/src/main/java/org/fusesource/jansi/AnsiMain.java index c2a845d6..197c7978 100644 --- a/src/main/java/org/fusesource/jansi/AnsiMain.java +++ b/src/main/java/org/fusesource/jansi/AnsiMain.java @@ -85,6 +85,10 @@ 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")); 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 +┌──┐┌─────┐ ┌─────┐ ┌──────┬──┐ │██├┘█████└┬┘█████└┬┘██████│▐▌│ ┌──┐ │██│██▄▄▄██│██┌─┐██│██▄▄▄▄ │▄▄│ │▒▒└─┘▒█│▒█┌─┐▒█│▒█│ │▒█│ ▀▀▀▀▒█│▒█│ From 3b15c26d4b6b00d9d6959e83398f42efd878986d Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 28 Sep 2023 07:35:46 +0200 Subject: [PATCH 03/12] Support for FFM on JDK 21 (fixes #230) (#259) - the minimal build requirement is bumped to JDK 21 - FFM is the default provider if available (JDK >= 21 with --enable-preview flag) --- .github/workflows/build.yml | 2 +- pom.xml | 60 +- .../org/fusesource/jansi/AnsiConsole.java | 65 +- .../fusesource/jansi/AnsiConsoleSupport.java | 63 ++ .../jansi/AnsiConsoleSupportHolder.java | 60 ++ .../java/org/fusesource/jansi/AnsiMain.java | 64 +- .../org/fusesource/jansi/WindowsSupport.java | 21 +- .../jansi/ffm/AnsiConsoleSupportFfm.java | 166 ++++ .../org/fusesource/jansi/ffm/Kernel32.java | 858 ++++++++++++++++++ .../jansi/ffm/WindowsAnsiProcessor.java | 439 +++++++++ .../jansi/internal/AnsiConsoleSupportJni.java | 108 +++ .../fusesource/jansi/internal/CLibrary.java | 10 +- .../jansi/internal/WindowsAnsiProcessor.java | 407 +++++++++ .../jansi/io/WindowsAnsiProcessor.java | 391 +------- 14 files changed, 2243 insertions(+), 471 deletions(-) create mode 100644 src/main/java/org/fusesource/jansi/AnsiConsoleSupport.java create mode 100644 src/main/java/org/fusesource/jansi/AnsiConsoleSupportHolder.java create mode 100644 src/main/java/org/fusesource/jansi/ffm/AnsiConsoleSupportFfm.java create mode 100644 src/main/java/org/fusesource/jansi/ffm/Kernel32.java create mode 100644 src/main/java/org/fusesource/jansi/ffm/WindowsAnsiProcessor.java create mode 100644 src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportJni.java create mode 100644 src/main/java/org/fusesource/jansi/internal/WindowsAnsiProcessor.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 169a1924..e1d5db2a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest, windows-latest, macos-latest ] - java: [ '11', '17' ] + java: [ '21' ] steps: - uses: actions/checkout@v2 with: diff --git a/pom.xml b/pom.xml index 9cf9578f..2b8a8f61 100644 --- a/pom.xml +++ b/pom.xml @@ -160,13 +160,67 @@ + + org.apache.maven.plugins + maven-enforcer-plugin + 3.4.1 + + + enforce-java + + enforce + + + + + 21 + + + + + + org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.11.0 + true + ${jdkTarget} + ${jdkTarget} ${jdkTarget} + + -Xlint:-options + + + + default-compile + + + **/ffm/*.java + + + + + jdk-21 + + compile + + + 21 + + **/ffm/*.java + + + --enable-preview + + + + + default-testCompile + + org.apache.felix @@ -351,12 +405,12 @@ com.diffplug.spotless spotless-maven-plugin - 2.38.0 + 2.39.0 - 2.35.0 + 2.38.0 java|javax,org,,\# diff --git a/src/main/java/org/fusesource/jansi/AnsiConsole.java b/src/main/java/org/fusesource/jansi/AnsiConsole.java index 9ce1b9a3..ff0cc657 100644 --- a/src/main/java/org/fusesource/jansi/AnsiConsole.java +++ b/src/main/java/org/fusesource/jansi/AnsiConsole.java @@ -26,22 +26,9 @@ import java.nio.charset.UnsupportedCharsetException; import java.util.Locale; -import org.fusesource.jansi.internal.CLibrary; -import org.fusesource.jansi.internal.CLibrary.WinSize; -import org.fusesource.jansi.internal.Kernel32.CONSOLE_SCREEN_BUFFER_INFO; import org.fusesource.jansi.io.AnsiOutputStream; import org.fusesource.jansi.io.AnsiProcessor; import org.fusesource.jansi.io.FastBufferedOutputStream; -import org.fusesource.jansi.io.WindowsAnsiProcessor; - -import static org.fusesource.jansi.internal.CLibrary.ioctl; -import static org.fusesource.jansi.internal.CLibrary.isatty; -import static org.fusesource.jansi.internal.Kernel32.GetConsoleMode; -import static org.fusesource.jansi.internal.Kernel32.GetConsoleScreenBufferInfo; -import static org.fusesource.jansi.internal.Kernel32.GetStdHandle; -import static org.fusesource.jansi.internal.Kernel32.STD_ERROR_HANDLE; -import static org.fusesource.jansi.internal.Kernel32.STD_OUTPUT_HANDLE; -import static org.fusesource.jansi.internal.Kernel32.SetConsoleMode; /** * Provides consistent access to an ANSI aware console PrintStream or an ANSI codes stripping PrintStream @@ -167,6 +154,11 @@ public class AnsiConsole { */ public static final String JANSI_GRACEFUL = "jansi.graceful"; + public static final String JANSI_PROVIDERS = "jansi.providers"; + public static final String JANSI_PROVIDER_JNI = "jni"; + public static final String JANSI_PROVIDER_FFM = "ffm"; + public static final String JANSI_PROVIDERS_DEFAULT = JANSI_PROVIDER_FFM + "," + JANSI_PROVIDER_JNI; + /** * @deprecated this field will be made private in a future release, use {@link #sysOut()} instead */ @@ -249,9 +241,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")) { @@ -277,25 +269,26 @@ private static AnsiPrintStream ansiStream(boolean stdout) { installer = uninstaller = null; width = new AnsiOutputStream.ZeroWidthSupplier(); } else if (IS_WINDOWS) { - final long console = GetStdHandle(stdout ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE); + final long console = getKernel32().getStdHandle(stdout); final int[] mode = new int[1]; - final boolean isConsole = GetConsoleMode(console, mode) != 0; - if (isConsole && SetConsoleMode(console, mode[0] | ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0) { - SetConsoleMode(console, mode[0]); // set it back for now, but we know it works + final boolean isConsole = getKernel32().getConsoleMode(console, mode) != 0; + if (isConsole && getKernel32().setConsoleMode(console, mode[0] | ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0) { + // set it back for now, but we know it works + getKernel32().setConsoleMode(console, mode[0]); processor = null; type = AnsiType.VirtualTerminal; installer = new AnsiOutputStream.IoRunnable() { @Override public void run() throws IOException { virtualProcessing++; - SetConsoleMode(console, mode[0] | ENABLE_VIRTUAL_TERMINAL_PROCESSING); + getKernel32().setConsoleMode(console, mode[0] | ENABLE_VIRTUAL_TERMINAL_PROCESSING); } }; uninstaller = new AnsiOutputStream.IoRunnable() { @Override public void run() throws IOException { if (--virtualProcessing == 0) { - SetConsoleMode(console, mode[0]); + getKernel32().setConsoleMode(console, mode[0]); } } }; @@ -311,7 +304,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. @@ -323,14 +316,7 @@ public void run() throws IOException { type = ttype; installer = uninstaller = null; } - width = new AnsiOutputStream.WidthSupplier() { - @Override - public int getTerminalWidth() { - CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); - GetConsoleScreenBufferInfo(console, info); - return info.windowWidth(); - } - }; + width = () -> getKernel32().getTerminalWidth(console); } // We must be on some Unix variant... @@ -339,14 +325,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; @@ -556,4 +535,12 @@ static synchronized void initStreams() { initialized = true; } } + + private static AnsiConsoleSupport.Kernel32 getKernel32() { + return AnsiConsoleSupport.getInstance().getKernel32(); + } + + private static AnsiConsoleSupport.CLibrary getCLibrary() { + return AnsiConsoleSupport.getInstance().getCLibrary(); + } } diff --git a/src/main/java/org/fusesource/jansi/AnsiConsoleSupport.java b/src/main/java/org/fusesource/jansi/AnsiConsoleSupport.java new file mode 100644 index 00000000..02907c55 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/AnsiConsoleSupport.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2009-2023 the original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.fusesource.jansi; + +import java.io.IOException; +import java.io.OutputStream; + +import org.fusesource.jansi.io.AnsiProcessor; + +public interface AnsiConsoleSupport { + + interface CLibrary { + + int STDOUT_FILENO = 1; + int STDERR_FILENO = 2; + + short getTerminalWidth(int fd); + + int isTty(int fd); + } + + interface Kernel32 { + + int isTty(long console); + + int getTerminalWidth(long console); + + long getStdHandle(boolean stdout); + + int getConsoleMode(long console, int[] mode); + + int setConsoleMode(long console, int mode); + + int getLastError(); + + String getErrorMessage(int errorCode); + + AnsiProcessor newProcessor(OutputStream os, long console) throws IOException; + } + + String getProviderName(); + + CLibrary getCLibrary(); + + Kernel32 getKernel32(); + + static AnsiConsoleSupport getInstance() { + return AnsiConsoleSupportHolder.get(); + } +} diff --git a/src/main/java/org/fusesource/jansi/AnsiConsoleSupportHolder.java b/src/main/java/org/fusesource/jansi/AnsiConsoleSupportHolder.java new file mode 100644 index 00000000..082f78d0 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/AnsiConsoleSupportHolder.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2009-2023 the original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.fusesource.jansi; + +import org.fusesource.jansi.internal.AnsiConsoleSupportJni; + +import static org.fusesource.jansi.AnsiConsole.JANSI_PROVIDERS; +import static org.fusesource.jansi.AnsiConsole.JANSI_PROVIDERS_DEFAULT; +import static org.fusesource.jansi.AnsiConsole.JANSI_PROVIDER_FFM; +import static org.fusesource.jansi.AnsiConsole.JANSI_PROVIDER_JNI; + +class AnsiConsoleSupportHolder { + static volatile AnsiConsoleSupport instance; + + static AnsiConsoleSupport get() { + if (instance == null) { + synchronized (AnsiConsoleSupportHolder.class) { + if (instance == null) { + instance = doGet(); + } + } + } + return instance; + } + + static AnsiConsoleSupport doGet() { + RuntimeException error = new RuntimeException("Unable to create AnsiConsoleSupport provider"); + String[] providers = + System.getProperty(JANSI_PROVIDERS, JANSI_PROVIDERS_DEFAULT).split(","); + for (String provider : providers) { + try { + if (JANSI_PROVIDER_FFM.equals(provider)) { + return (AnsiConsoleSupport) AnsiConsoleSupport.class + .getClassLoader() + .loadClass("org.fusesource.jansi.ffm.AnsiConsoleSupportFfm") + .getConstructor() + .newInstance(); + } else if (JANSI_PROVIDER_JNI.equals(provider)) { + return new AnsiConsoleSupportJni(); + } + } catch (Throwable t) { + error.addSuppressed(t); + } + } + throw error; + } +} diff --git a/src/main/java/org/fusesource/jansi/AnsiMain.java b/src/main/java/org/fusesource/jansi/AnsiMain.java index 197c7978..24fe6f2f 100644 --- a/src/main/java/org/fusesource/jansi/AnsiMain.java +++ b/src/main/java/org/fusesource/jansi/AnsiMain.java @@ -26,7 +26,6 @@ import java.util.Properties; import org.fusesource.jansi.Ansi.Attribute; -import org.fusesource.jansi.internal.CLibrary; import org.fusesource.jansi.internal.JansiLoader; import static org.fusesource.jansi.Ansi.ansi; @@ -54,27 +53,34 @@ public static void main(String... args) throws IOException { System.out.println(); - // info on native library - System.out.println("library.jansi.path= " + System.getProperty("library.jansi.path", "")); - System.out.println("library.jansi.version= " + System.getProperty("library.jansi.version", "")); - boolean loaded = JansiLoader.initialize(); - if (loaded) { - System.out.println("Jansi native library loaded from " + JansiLoader.getNativeLibraryPath()); - if (JansiLoader.getNativeLibrarySourceUrl() != null) { - System.out.println(" which was auto-extracted from " + JansiLoader.getNativeLibrarySourceUrl()); - } - } else { - String prev = System.getProperty(AnsiConsole.JANSI_GRACEFUL); - try { - System.setProperty(AnsiConsole.JANSI_GRACEFUL, "false"); - JansiLoader.initialize(); - } catch (Throwable e) { - e.printStackTrace(System.out); - } finally { - if (prev != null) { - System.setProperty(AnsiConsole.JANSI_GRACEFUL, prev); - } else { - System.clearProperty(AnsiConsole.JANSI_GRACEFUL); + System.out.println("jansi.providers= " + + System.getProperty(AnsiConsole.JANSI_PROVIDERS, AnsiConsole.JANSI_PROVIDERS_DEFAULT)); + String provider = AnsiConsoleSupport.getInstance().getProviderName(); + System.out.println("Selected provider: " + provider); + + if (AnsiConsole.JANSI_PROVIDER_JNI.equals(provider)) { + // info on native library + System.out.println("library.jansi.path= " + System.getProperty("library.jansi.path", "")); + System.out.println("library.jansi.version= " + System.getProperty("library.jansi.version", "")); + boolean loaded = JansiLoader.initialize(); + if (loaded) { + System.out.println("Jansi native library loaded from " + JansiLoader.getNativeLibraryPath()); + if (JansiLoader.getNativeLibrarySourceUrl() != null) { + System.out.println(" which was auto-extracted from " + JansiLoader.getNativeLibrarySourceUrl()); + } + } else { + String prev = System.getProperty(AnsiConsole.JANSI_GRACEFUL); + try { + System.setProperty(AnsiConsole.JANSI_GRACEFUL, "false"); + JansiLoader.initialize(); + } catch (Throwable e) { + e.printStackTrace(System.out); + } finally { + if (prev != null) { + System.setProperty(AnsiConsole.JANSI_GRACEFUL, prev); + } else { + System.clearProperty(AnsiConsole.JANSI_GRACEFUL); + } } } } @@ -192,11 +198,21 @@ private static String getJansiVersion() { } private static void diagnoseTty(boolean stderr) { - int fd = stderr ? CLibrary.STDERR_FILENO : CLibrary.STDOUT_FILENO; - int isatty = CLibrary.LOADED ? CLibrary.isatty(fd) : 0; + int isatty; + int width; + if (AnsiConsole.IS_WINDOWS) { + long console = AnsiConsoleSupport.getInstance().getKernel32().getStdHandle(!stderr); + isatty = AnsiConsoleSupport.getInstance().getKernel32().isTty(console); + width = AnsiConsoleSupport.getInstance().getKernel32().getTerminalWidth(console); + } else { + int fd = stderr ? AnsiConsoleSupport.CLibrary.STDERR_FILENO : AnsiConsoleSupport.CLibrary.STDOUT_FILENO; + isatty = AnsiConsoleSupport.getInstance().getCLibrary().isTty(fd); + width = AnsiConsoleSupport.getInstance().getCLibrary().getTerminalWidth(fd); + } System.out.println("isatty(STD" + (stderr ? "ERR" : "OUT") + "_FILENO): " + isatty + ", System." + (stderr ? "err" : "out") + " " + ((isatty == 0) ? "is *NOT*" : "is") + " a terminal"); + System.out.println("width(STD" + (stderr ? "ERR" : "OUT") + "_FILENO): " + width); } private static void testAnsi(boolean stderr) { diff --git a/src/main/java/org/fusesource/jansi/WindowsSupport.java b/src/main/java/org/fusesource/jansi/WindowsSupport.java index 010f527e..e14854cd 100644 --- a/src/main/java/org/fusesource/jansi/WindowsSupport.java +++ b/src/main/java/org/fusesource/jansi/WindowsSupport.java @@ -15,27 +15,18 @@ */ package org.fusesource.jansi; -import java.io.UnsupportedEncodingException; - -import static org.fusesource.jansi.internal.Kernel32.FORMAT_MESSAGE_FROM_SYSTEM; -import static org.fusesource.jansi.internal.Kernel32.FormatMessageW; -import static org.fusesource.jansi.internal.Kernel32.GetLastError; - public class WindowsSupport { public static String getLastErrorMessage() { - int errorCode = GetLastError(); + int errorCode = getKernel32().getLastError(); return getErrorMessage(errorCode); } public static String getErrorMessage(int errorCode) { - int bufferSize = 160; - byte data[] = new byte[bufferSize]; - FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, 0, errorCode, 0, data, bufferSize, null); - try { - return new String(data, "UTF-16LE").trim(); - } catch (UnsupportedEncodingException e) { - throw new IllegalStateException(e); - } + return getKernel32().getErrorMessage(errorCode); + } + + private static AnsiConsoleSupport.Kernel32 getKernel32() { + return AnsiConsoleSupport.getInstance().getKernel32(); } } diff --git a/src/main/java/org/fusesource/jansi/ffm/AnsiConsoleSupportFfm.java b/src/main/java/org/fusesource/jansi/ffm/AnsiConsoleSupportFfm.java new file mode 100644 index 00000000..b1af763f --- /dev/null +++ b/src/main/java/org/fusesource/jansi/ffm/AnsiConsoleSupportFfm.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2009-2023 the original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.fusesource.jansi.ffm; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.GroupLayout; +import java.lang.foreign.Linker; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.VarHandle; + +import org.fusesource.jansi.AnsiConsoleSupport; +import org.fusesource.jansi.io.AnsiProcessor; + +import static org.fusesource.jansi.ffm.Kernel32.*; + +public class AnsiConsoleSupportFfm implements AnsiConsoleSupport { + static GroupLayout wsLayout; + static MethodHandle ioctl; + static VarHandle ws_col; + static MethodHandle isatty; + + static { + wsLayout = MemoryLayout.structLayout( + ValueLayout.JAVA_SHORT.withName("ws_row"), + ValueLayout.JAVA_SHORT.withName("ws_col"), + ValueLayout.JAVA_SHORT, + ValueLayout.JAVA_SHORT); + ws_col = wsLayout.varHandle(MemoryLayout.PathElement.groupElement("ws_col")); + Linker linker = Linker.nativeLinker(); + ioctl = linker.downcallHandle( + linker.defaultLookup().find("ioctl").get(), + FunctionDescriptor.of( + ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS), + Linker.Option.firstVariadicArg(2)); + isatty = linker.downcallHandle( + linker.defaultLookup().find("isatty").get(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT)); + } + + @Override + public String getProviderName() { + return "ffm"; + } + + @Override + public CLibrary getCLibrary() { + return new CLibrary() { + static final int TIOCGWINSZ; + + static { + String osName = System.getProperty("os.name"); + if (osName.startsWith("Linux")) { + String arch = System.getProperty("os.arch"); + boolean isMipsPpcOrSparc = + arch.startsWith("mips") || arch.startsWith("ppc") || arch.startsWith("sparc"); + TIOCGWINSZ = isMipsPpcOrSparc ? 0x40087468 : 0x00005413; + } else if (osName.startsWith("Solaris") || osName.startsWith("SunOS")) { + int _TIOC = ('T' << 8); + TIOCGWINSZ = (_TIOC | 104); + } else if (osName.startsWith("Mac") || osName.startsWith("Darwin")) { + TIOCGWINSZ = 0x40087468; + } else if (osName.startsWith("FreeBSD")) { + TIOCGWINSZ = 0x40087468; + } else { + throw new UnsupportedOperationException(); + } + } + + @Override + public short getTerminalWidth(int fd) { + MemorySegment segment = Arena.ofAuto().allocate(wsLayout); + try { + int res = (int) ioctl.invoke(fd, (long) TIOCGWINSZ, segment); + return (short) ws_col.get(segment); + } catch (Throwable e) { + throw new RuntimeException("Unable to ioctl(TIOCGWINSZ)", e); + } + } + + @Override + public int isTty(int fd) { + try { + return (int) isatty.invoke(fd); + } catch (Throwable e) { + throw new RuntimeException("Unable to call isatty", e); + } + } + }; + } + + @Override + public Kernel32 getKernel32() { + return new Kernel32() { + @Override + public int isTty(long console) { + int[] mode = new int[1]; + return getConsoleMode(console, mode); + } + + @Override + public int getTerminalWidth(long console) { + CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); + GetConsoleScreenBufferInfo(MemorySegment.ofAddress(console), info); + return info.windowWidth(); + } + + @Override + public long getStdHandle(boolean stdout) { + return GetStdHandle(stdout ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE) + .address(); + } + + @Override + public int getConsoleMode(long console, int[] mode) { + try (Arena session = Arena.ofConfined()) { + MemorySegment written = session.allocate(ValueLayout.JAVA_INT); + int res = GetConsoleMode(MemorySegment.ofAddress(console), written); + mode[0] = written.getAtIndex(ValueLayout.JAVA_INT, 0); + return res; + } + } + + @Override + public int setConsoleMode(long console, int mode) { + return SetConsoleMode(MemorySegment.ofAddress(console), mode); + } + + @Override + public int getLastError() { + return GetLastError(); + } + + @Override + public String getErrorMessage(int errorCode) { + int bufferSize = 160; + MemorySegment data = Arena.ofAuto().allocate(bufferSize); + FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, null, errorCode, 0, data, bufferSize, null); + return data.getUtf8String(0).trim(); + } + + @Override + public AnsiProcessor newProcessor(OutputStream os, long console) throws IOException { + return new WindowsAnsiProcessor(os, MemorySegment.ofAddress(console)); + } + }; + } +} diff --git a/src/main/java/org/fusesource/jansi/ffm/Kernel32.java b/src/main/java/org/fusesource/jansi/ffm/Kernel32.java new file mode 100644 index 00000000..fc17db68 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/ffm/Kernel32.java @@ -0,0 +1,858 @@ +/* + * Copyright (C) 2009-2023 the original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.fusesource.jansi.ffm; + +import java.io.IOException; +import java.lang.foreign.AddressLayout; +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.GroupLayout; +import java.lang.foreign.Linker; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.SymbolLookup; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.VarHandle; +import java.util.Objects; + +import static java.lang.foreign.ValueLayout.JAVA_INT; +import static java.lang.foreign.ValueLayout.OfBoolean; +import static java.lang.foreign.ValueLayout.OfByte; +import static java.lang.foreign.ValueLayout.OfChar; +import static java.lang.foreign.ValueLayout.OfDouble; +import static java.lang.foreign.ValueLayout.OfFloat; +import static java.lang.foreign.ValueLayout.OfInt; +import static java.lang.foreign.ValueLayout.OfLong; +import static java.lang.foreign.ValueLayout.OfShort; + +@SuppressWarnings({"unused", "CopyConstructorMissesField"}) +class Kernel32 { + + public static final int FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000; + + public static final int INVALID_HANDLE_VALUE = -1; + public static final int STD_INPUT_HANDLE = -10; + public static final int STD_OUTPUT_HANDLE = -11; + public static final int STD_ERROR_HANDLE = -12; + + public static final int ENABLE_PROCESSED_INPUT = 0x0001; + public static final int ENABLE_LINE_INPUT = 0x0002; + public static final int ENABLE_ECHO_INPUT = 0x0004; + public static final int ENABLE_WINDOW_INPUT = 0x0008; + public static final int ENABLE_MOUSE_INPUT = 0x0010; + public static final int ENABLE_INSERT_MODE = 0x0020; + public static final int ENABLE_QUICK_EDIT_MODE = 0x0040; + public static final int ENABLE_EXTENDED_FLAGS = 0x0080; + + public static final int RIGHT_ALT_PRESSED = 0x0001; + public static final int LEFT_ALT_PRESSED = 0x0002; + public static final int RIGHT_CTRL_PRESSED = 0x0004; + public static final int LEFT_CTRL_PRESSED = 0x0008; + public static final int SHIFT_PRESSED = 0x0010; + + public static final int FOREGROUND_BLUE = 0x0001; + public static final int FOREGROUND_GREEN = 0x0002; + public static final int FOREGROUND_RED = 0x0004; + public static final int FOREGROUND_INTENSITY = 0x0008; + public static final int BACKGROUND_BLUE = 0x0010; + public static final int BACKGROUND_GREEN = 0x0020; + public static final int BACKGROUND_RED = 0x0040; + public static final int BACKGROUND_INTENSITY = 0x0080; + + // Button state + public static final int FROM_LEFT_1ST_BUTTON_PRESSED = 0x0001; + public static final int RIGHTMOST_BUTTON_PRESSED = 0x0002; + public static final int FROM_LEFT_2ND_BUTTON_PRESSED = 0x0004; + public static final int FROM_LEFT_3RD_BUTTON_PRESSED = 0x0008; + public static final int FROM_LEFT_4TH_BUTTON_PRESSED = 0x0010; + + // Event flags + public static final int MOUSE_MOVED = 0x0001; + public static final int DOUBLE_CLICK = 0x0002; + public static final int MOUSE_WHEELED = 0x0004; + public static final int MOUSE_HWHEELED = 0x0008; + + // Event types + public static final short KEY_EVENT = 0x0001; + public static final short MOUSE_EVENT = 0x0002; + public static final short WINDOW_BUFFER_SIZE_EVENT = 0x0004; + public static final short MENU_EVENT = 0x0008; + public static final short FOCUS_EVENT = 0x0010; + + public static int WaitForSingleObject(MemorySegment hHandle, int dwMilliseconds) { + MethodHandle mh$ = requireNonNull(WaitForSingleObject$MH, "WaitForSingleObject"); + try { + return (int) mh$.invokeExact(hHandle, dwMilliseconds); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static MemorySegment GetStdHandle(int nStdHandle) { + MethodHandle mh$ = requireNonNull(GetStdHandle$MH, "GetStdHandle"); + try { + return MemorySegment.ofAddress((long) mh$.invokeExact(nStdHandle)); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int FormatMessageW( + int dwFlags, + MemorySegment lpSource, + int dwMessageId, + int dwLanguageId, + MemorySegment lpBuffer, + int nSize, + MemorySegment Arguments) { + MethodHandle mh$ = requireNonNull(FormatMessageW$MH, "FormatMessageW"); + try { + return (int) mh$.invokeExact( + dwFlags, + lpSource.address(), + dwMessageId, + dwLanguageId, + lpBuffer.address(), + nSize, + Arguments.address()); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int SetConsoleTextAttribute(MemorySegment hConsoleOutput, short wAttributes) { + MethodHandle mh$ = requireNonNull(SetConsoleTextAttribute$MH, "SetConsoleTextAttribute"); + try { + return (int) mh$.invokeExact(hConsoleOutput, wAttributes); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int SetConsoleMode(MemorySegment hConsoleHandle, int dwMode) { + MethodHandle mh$ = requireNonNull(SetConsoleMode$MH, "SetConsoleMode"); + try { + return (int) mh$.invokeExact(hConsoleHandle.address(), dwMode); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int GetConsoleMode(MemorySegment hConsoleHandle, MemorySegment lpMode) { + MethodHandle mh$ = requireNonNull(GetConsoleMode$MH, "GetConsoleMode"); + try { + return (int) mh$.invokeExact(hConsoleHandle.address(), lpMode.address()); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int SetConsoleTitleW(MemorySegment lpConsoleTitle) { + MethodHandle mh$ = requireNonNull(SetConsoleTitleW$MH, "SetConsoleTitleW"); + try { + return (int) mh$.invokeExact(lpConsoleTitle.address()); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int SetConsoleCursorPosition(MemorySegment hConsoleOutput, COORD dwCursorPosition) { + MethodHandle mh$ = requireNonNull(SetConsoleCursorPosition$MH, "SetConsoleCursorPosition"); + try { + return (int) mh$.invokeExact(hConsoleOutput, dwCursorPosition.seg); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int FillConsoleOutputCharacterW( + MemorySegment hConsoleOutput, + char cCharacter, + int nLength, + COORD dwWriteCoord, + MemorySegment lpNumberOfCharsWritten) { + MethodHandle mh$ = requireNonNull(FillConsoleOutputCharacterW$MH, "FillConsoleOutputCharacterW"); + try { + return (int) mh$.invokeExact( + hConsoleOutput.address(), cCharacter, nLength, dwWriteCoord.seg, lpNumberOfCharsWritten.address()); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int FillConsoleOutputAttribute( + MemorySegment hConsoleOutput, + short wAttribute, + int nLength, + COORD dwWriteCoord, + MemorySegment lpNumberOfAttrsWritten) { + MethodHandle mh$ = requireNonNull(FillConsoleOutputAttribute$MH, "FillConsoleOutputAttribute"); + try { + return (int) mh$.invokeExact( + hConsoleOutput, wAttribute, nLength, dwWriteCoord.seg, lpNumberOfAttrsWritten.address()); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int WriteConsoleW( + MemorySegment hConsoleOutput, + MemorySegment lpBuffer, + int nNumberOfCharsToWrite, + MemorySegment lpNumberOfCharsWritten, + MemorySegment lpReserved) { + MethodHandle mh$ = requireNonNull(WriteConsoleW$MH, "WriteConsoleW"); + try { + return (int) mh$.invokeExact( + hConsoleOutput, lpBuffer, nNumberOfCharsToWrite, lpNumberOfCharsWritten, lpReserved); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int ReadConsoleInputW( + MemorySegment hConsoleInput, MemorySegment lpBuffer, int nLength, MemorySegment lpNumberOfEventsRead) { + MethodHandle mh$ = requireNonNull(ReadConsoleInputW$MH, "ReadConsoleInputW"); + try { + return (int) mh$.invokeExact( + hConsoleInput.address(), lpBuffer.address(), nLength, lpNumberOfEventsRead.address()); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int PeekConsoleInputW( + MemorySegment hConsoleInput, MemorySegment lpBuffer, int nLength, MemorySegment lpNumberOfEventsRead) { + MethodHandle mh$ = requireNonNull(PeekConsoleInputW$MH, "PeekConsoleInputW"); + try { + return (int) mh$.invokeExact( + hConsoleInput.address(), lpBuffer.address(), nLength, lpNumberOfEventsRead.address()); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int GetConsoleScreenBufferInfo( + MemorySegment hConsoleOutput, CONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo) { + MethodHandle mh$ = requireNonNull(GetConsoleScreenBufferInfo$MH, "GetConsoleScreenBufferInfo"); + try { + return (int) mh$.invokeExact(hConsoleOutput.address(), lpConsoleScreenBufferInfo.seg); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int ScrollConsoleScreenBuffer( + MemorySegment hConsoleOutput, + SMALL_RECT lpScrollRectangle, + SMALL_RECT lpClipRectangle, + COORD dwDestinationOrigin, + CHAR_INFO lpFill) { + MethodHandle mh$ = requireNonNull(ScrollConsoleScreenBuffer$MH, "ScrollConsoleScreenBuffer"); + try { + return (int) + mh$.invokeExact(hConsoleOutput, lpScrollRectangle, lpClipRectangle, dwDestinationOrigin, lpFill); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int GetLastError(Object... x0) { + MethodHandle mh$ = requireNonNull(GetLastError$MH, "GetLastError"); + try { + return (int) mh$.invokeExact(x0); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static INPUT_RECORD[] readConsoleInputHelper(MemorySegment handle, int count, boolean peek) + throws IOException { + try (Arena session = Arena.ofConfined()) { + MemorySegment inputRecordPtr = session.allocateArray(INPUT_RECORD.LAYOUT, count); + MemorySegment length = session.allocate(JAVA_INT, 0); + int res = peek + ? PeekConsoleInputW(handle, inputRecordPtr, count, length) + : ReadConsoleInputW(handle, inputRecordPtr, count, length); + if (res == 0) { + throw new IOException("ReadConsoleInputW failed: " + getLastErrorMessage()); + } + int len = length.get(JAVA_INT, 0); + return inputRecordPtr + .elements(INPUT_RECORD.LAYOUT) + .map(INPUT_RECORD::new) + .limit(len) + .toArray(INPUT_RECORD[]::new); + } + } + + public static String getLastErrorMessage() { + int errorCode = GetLastError(); + return getErrorMessage(errorCode); + } + + public static String getErrorMessage(int errorCode) { + int bufferSize = 160; + MemorySegment data = Arena.ofAuto().allocate(bufferSize); + FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, null, errorCode, 0, data, bufferSize, null); + return data.getUtf8String(0).trim(); + } + + static final OfBoolean C_BOOL$LAYOUT = ValueLayout.JAVA_BOOLEAN; + static final OfByte C_CHAR$LAYOUT = ValueLayout.JAVA_BYTE; + static final OfChar C_WCHAR$LAYOUT = ValueLayout.JAVA_CHAR.withByteAlignment(16); + static final OfShort C_SHORT$LAYOUT = ValueLayout.JAVA_SHORT.withByteAlignment(16); + static final OfShort C_WORD$LAYOUT = ValueLayout.JAVA_SHORT.withByteAlignment(16); + static final OfInt C_DWORD$LAYOUT = ValueLayout.JAVA_INT.withByteAlignment(32); + static final OfInt C_INT$LAYOUT = JAVA_INT.withByteAlignment(32); + static final OfLong C_LONG$LAYOUT = ValueLayout.JAVA_LONG.withByteAlignment(64); + static final OfLong C_LONG_LONG$LAYOUT = ValueLayout.JAVA_LONG.withByteAlignment(64); + static final OfFloat C_FLOAT$LAYOUT = ValueLayout.JAVA_FLOAT.withByteAlignment(32); + static final OfDouble C_DOUBLE$LAYOUT = ValueLayout.JAVA_DOUBLE.withByteAlignment(64); + static final AddressLayout C_POINTER$LAYOUT = ValueLayout.ADDRESS.withByteAlignment(64); + + static final MethodHandle WaitForSingleObject$MH = + downcallHandle("WaitForSingleObject", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT)); + static final MethodHandle GetStdHandle$MH = + downcallHandle("GetStdHandle", FunctionDescriptor.of(C_POINTER$LAYOUT, C_INT$LAYOUT)); + static final MethodHandle FormatMessageW$MH = downcallHandle( + "FormatMessageW", + FunctionDescriptor.of( + C_INT$LAYOUT, + C_INT$LAYOUT, + C_POINTER$LAYOUT, + C_INT$LAYOUT, + C_INT$LAYOUT, + C_POINTER$LAYOUT, + C_INT$LAYOUT, + C_POINTER$LAYOUT)); + static final MethodHandle SetConsoleTextAttribute$MH = downcallHandle( + "SetConsoleTextAttribute", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_SHORT$LAYOUT)); + static final MethodHandle SetConsoleMode$MH = + downcallHandle("SetConsoleMode", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT)); + static final MethodHandle GetConsoleMode$MH = + downcallHandle("GetConsoleMode", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT)); + + static final MethodHandle SetConsoleTitleW$MH = + downcallHandle("SetConsoleTitleW", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT)); + static final MethodHandle SetConsoleCursorPosition$MH = downcallHandle( + "SetConsoleCursorPosition", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, COORD.LAYOUT)); + static final MethodHandle FillConsoleOutputCharacterW$MH = downcallHandle( + "FillConsoleOutputCharacterW", + FunctionDescriptor.of( + C_INT$LAYOUT, C_POINTER$LAYOUT, C_SHORT$LAYOUT, C_INT$LAYOUT, COORD.LAYOUT, C_POINTER$LAYOUT)); + static final MethodHandle FillConsoleOutputAttribute$MH = downcallHandle( + "FillConsoleOutputAttribute", + FunctionDescriptor.of( + C_INT$LAYOUT, C_POINTER$LAYOUT, C_SHORT$LAYOUT, C_INT$LAYOUT, COORD.LAYOUT, C_POINTER$LAYOUT)); + static final MethodHandle WriteConsoleW$MH = downcallHandle( + "WriteConsoleW", + FunctionDescriptor.of( + C_INT$LAYOUT, + C_POINTER$LAYOUT, + C_POINTER$LAYOUT, + C_INT$LAYOUT, + C_POINTER$LAYOUT, + C_POINTER$LAYOUT)); + + static final MethodHandle ReadConsoleInputW$MH = downcallHandle( + "ReadConsoleInputW", + FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT, C_POINTER$LAYOUT)); + static final MethodHandle PeekConsoleInputW$MH = downcallHandle( + "PeekConsoleInputW", + FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT, C_POINTER$LAYOUT)); + + static final MethodHandle GetConsoleScreenBufferInfo$MH = downcallHandle( + "GetConsoleScreenBufferInfo", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT)); + + static final MethodHandle ScrollConsoleScreenBuffer$MH = downcallHandle( + "ScrollConsoleScreenBuffer", + FunctionDescriptor.of( + C_INT$LAYOUT, + C_POINTER$LAYOUT, + C_POINTER$LAYOUT, + C_POINTER$LAYOUT, + COORD.LAYOUT, + C_POINTER$LAYOUT)); + static final MethodHandle GetLastError$MH = downcallHandle("GetLastError", FunctionDescriptor.of(C_INT$LAYOUT)); + + public static class INPUT_RECORD { + static final MemoryLayout LAYOUT = MemoryLayout.structLayout( + ValueLayout.JAVA_SHORT.withName("EventType"), + MemoryLayout.unionLayout( + KEY_EVENT_RECORD.LAYOUT.withName("KeyEvent"), + MOUSE_EVENT_RECORD.LAYOUT.withName("MouseEvent"), + WINDOW_BUFFER_SIZE_RECORD.LAYOUT.withName("WindowBufferSizeEvent"), + MENU_EVENT_RECORD.LAYOUT.withName("MenuEvent"), + FOCUS_EVENT_RECORD.LAYOUT.withName("FocusEvent")) + .withName("Event")); + static final VarHandle EventType$VH = varHandle(LAYOUT, "EventType"); + static final long Event$OFFSET = byteOffset(LAYOUT, "Event"); + + private final MemorySegment seg; + + public INPUT_RECORD() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + INPUT_RECORD(MemorySegment seg) { + this.seg = seg; + } + + public short eventType() { + return (short) EventType$VH.get(seg); + } + + public KEY_EVENT_RECORD keyEvent() { + return new KEY_EVENT_RECORD(seg, Event$OFFSET); + } + + public MOUSE_EVENT_RECORD mouseEvent() { + return new MOUSE_EVENT_RECORD(seg, Event$OFFSET); + } + + public FOCUS_EVENT_RECORD focusEvent() { + return new FOCUS_EVENT_RECORD(seg, Event$OFFSET); + } + } + + public static class MENU_EVENT_RECORD { + + static final GroupLayout LAYOUT = MemoryLayout.structLayout(C_DWORD$LAYOUT.withName("dwCommandId")); + static final VarHandle COMMAND_ID = varHandle(LAYOUT, "dwCommandId"); + + private final MemorySegment seg; + + public MENU_EVENT_RECORD() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + MENU_EVENT_RECORD(MemorySegment seg) { + this.seg = seg; + } + + public int commandId() { + return (int) MENU_EVENT_RECORD.COMMAND_ID.get(seg); + } + + public void commandId(int commandId) { + MENU_EVENT_RECORD.COMMAND_ID.set(seg, commandId); + } + } + + public static class FOCUS_EVENT_RECORD { + + static final GroupLayout LAYOUT = MemoryLayout.structLayout(C_BOOL$LAYOUT.withName("bSetFocus")); + static final VarHandle SET_FOCUS = varHandle(LAYOUT, "bSetFocus"); + + private final MemorySegment seg; + + public FOCUS_EVENT_RECORD() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + FOCUS_EVENT_RECORD(MemorySegment seg) { + this.seg = Objects.requireNonNull(seg); + } + + FOCUS_EVENT_RECORD(MemorySegment seg, long offset) { + this.seg = Objects.requireNonNull(seg).asSlice(offset, LAYOUT.byteSize()); + } + + public boolean setFocus() { + return (boolean) FOCUS_EVENT_RECORD.SET_FOCUS.get(seg); + } + + public void setFocus(boolean setFocus) { + FOCUS_EVENT_RECORD.SET_FOCUS.set(seg, setFocus); + } + } + + public static class WINDOW_BUFFER_SIZE_RECORD { + + static final GroupLayout LAYOUT = MemoryLayout.structLayout(COORD.LAYOUT.withName("size")); + static final long SIZE_OFFSET = byteOffset(LAYOUT, "size"); + + private final MemorySegment seg; + + public WINDOW_BUFFER_SIZE_RECORD() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + WINDOW_BUFFER_SIZE_RECORD(MemorySegment seg) { + this.seg = seg; + } + + public COORD size() { + return new COORD(seg, SIZE_OFFSET); + } + + public String toString() { + return "WINDOW_BUFFER_SIZE_RECORD{size=" + this.size() + '}'; + } + } + + public static class MOUSE_EVENT_RECORD { + + private static final MemoryLayout LAYOUT = MemoryLayout.structLayout( + COORD.LAYOUT.withName("dwMousePosition"), + C_DWORD$LAYOUT.withName("dwButtonState"), + C_DWORD$LAYOUT.withName("dwControlKeyState"), + C_DWORD$LAYOUT.withName("dwEventFlags")); + private static final long MOUSE_POSITION_OFFSET = byteOffset(LAYOUT, "dwMousePosition"); + private static final VarHandle BUTTON_STATE = varHandle(LAYOUT, "dwButtonState"); + private static final VarHandle CONTROL_KEY_STATE = varHandle(LAYOUT, "dwControlKeyState"); + private static final VarHandle EVENT_FLAGS = varHandle(LAYOUT, "dwEventFlags"); + + private final MemorySegment seg; + + public MOUSE_EVENT_RECORD() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + MOUSE_EVENT_RECORD(MemorySegment seg) { + this.seg = Objects.requireNonNull(seg); + } + + MOUSE_EVENT_RECORD(MemorySegment seg, long offset) { + this.seg = Objects.requireNonNull(seg).asSlice(offset, LAYOUT.byteSize()); + } + + public COORD mousePosition() { + return new COORD(seg, MOUSE_POSITION_OFFSET); + } + + public int buttonState() { + return (int) BUTTON_STATE.get(seg); + } + + public int controlKeyState() { + return (int) CONTROL_KEY_STATE.get(seg); + } + + public int eventFlags() { + return (int) EVENT_FLAGS.get(seg); + } + + public String toString() { + return "MOUSE_EVENT_RECORD{mousePosition=" + mousePosition() + ", buttonState=" + buttonState() + + ", controlKeyState=" + controlKeyState() + ", eventFlags=" + eventFlags() + '}'; + } + } + + public static class KEY_EVENT_RECORD { + + static final MemoryLayout LAYOUT = MemoryLayout.structLayout( + JAVA_INT.withName("bKeyDown"), + ValueLayout.JAVA_SHORT.withName("wRepeatCount"), + ValueLayout.JAVA_SHORT.withName("wVirtualKeyCode"), + ValueLayout.JAVA_SHORT.withName("wVirtualScanCode"), + MemoryLayout.unionLayout( + ValueLayout.JAVA_CHAR.withName("UnicodeChar"), + ValueLayout.JAVA_BYTE.withName("AsciiChar")) + .withName("uChar"), + JAVA_INT.withName("dwControlKeyState")); + static final VarHandle bKeyDown$VH = varHandle(LAYOUT, "bKeyDown"); + static final VarHandle wRepeatCount$VH = varHandle(LAYOUT, "wRepeatCount"); + static final VarHandle wVirtualKeyCode$VH = varHandle(LAYOUT, "wVirtualKeyCode"); + static final VarHandle wVirtualScanCode$VH = varHandle(LAYOUT, "wVirtualScanCode"); + static final VarHandle UnicodeChar$VH = varHandle(LAYOUT, "uChar", "UnicodeChar"); + static final VarHandle AsciiChar$VH = varHandle(LAYOUT, "uChar", "AsciiChar"); + static final VarHandle dwControlKeyState$VH = varHandle(LAYOUT, "dwControlKeyState"); + + final MemorySegment seg; + + public KEY_EVENT_RECORD() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + KEY_EVENT_RECORD(MemorySegment seg) { + this.seg = seg; + } + + KEY_EVENT_RECORD(MemorySegment seg, long offset) { + this.seg = Objects.requireNonNull(seg).asSlice(offset, LAYOUT.byteSize()); + } + + public boolean keyDown() { + return (boolean) bKeyDown$VH.get(seg); + } + + public int repeatCount() { + return (int) wRepeatCount$VH.get(seg); + } + + public short keyCode() { + return (short) wVirtualKeyCode$VH.get(seg); + } + + public short scanCode() { + return (short) wVirtualScanCode$VH.get(seg); + } + + public char uchar() { + return (char) UnicodeChar$VH.get(seg); + } + + public int controlKeyState() { + return (int) dwControlKeyState$VH.get(seg); + } + + public String toString() { + return "KEY_EVENT_RECORD{keyDown=" + this.keyDown() + ", repeatCount=" + this.repeatCount() + ", keyCode=" + + this.keyCode() + ", scanCode=" + this.scanCode() + ", uchar=" + this.uchar() + + ", controlKeyState=" + + this.controlKeyState() + '}'; + } + } + + public static class CHAR_INFO { + + static final GroupLayout LAYOUT = MemoryLayout.structLayout( + MemoryLayout.unionLayout(C_WCHAR$LAYOUT.withName("UnicodeChar"), C_CHAR$LAYOUT.withName("AsciiChar")) + .withName("Char"), + C_WORD$LAYOUT.withName("Attributes")); + static final VarHandle UnicodeChar$VH = varHandle(LAYOUT, "Char", "UnicodeChar"); + static final VarHandle Attributes$VH = varHandle(LAYOUT, "Attributes"); + + final MemorySegment seg; + + public CHAR_INFO() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + public CHAR_INFO(char c, short a) { + this(); + UnicodeChar$VH.set(seg, c); + Attributes$VH.set(seg, a); + } + + CHAR_INFO(MemorySegment seg) { + this.seg = seg; + } + + public char unicodeChar() { + return (char) UnicodeChar$VH.get(seg); + } + } + + public static class CONSOLE_SCREEN_BUFFER_INFO { + static final GroupLayout LAYOUT = MemoryLayout.structLayout( + COORD.LAYOUT.withName("dwSize"), + COORD.LAYOUT.withName("dwCursorPosition"), + C_WORD$LAYOUT.withName("wAttributes"), + SMALL_RECT.LAYOUT.withName("srWindow"), + COORD.LAYOUT.withName("dwMaximumWindowSize")); + static final long dwSize$OFFSET = byteOffset(LAYOUT, "dwSize"); + static final long dwCursorPosition$OFFSET = byteOffset(LAYOUT, "dwCursorPosition"); + static final VarHandle wAttributes$VH = varHandle(LAYOUT, "wAttributes"); + static final long srWindow$OFFSET = byteOffset(LAYOUT, "srWindow"); + + private final MemorySegment seg; + + public CONSOLE_SCREEN_BUFFER_INFO() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + CONSOLE_SCREEN_BUFFER_INFO(MemorySegment seg) { + this.seg = seg; + } + + public COORD size() { + return new COORD(seg, dwSize$OFFSET); + } + + public COORD cursorPosition() { + return new COORD(seg, dwCursorPosition$OFFSET); + } + + public short attributes() { + return (short) wAttributes$VH.get(seg); + } + + public SMALL_RECT window() { + return new SMALL_RECT(seg, srWindow$OFFSET); + } + + public int windowWidth() { + return this.window().width() + 1; + } + + public int windowHeight() { + return this.window().height() + 1; + } + + public void attributes(short attr) { + wAttributes$VH.set(seg, attr); + } + } + + public static class COORD { + + static final GroupLayout LAYOUT = + MemoryLayout.structLayout(C_SHORT$LAYOUT.withName("x"), C_SHORT$LAYOUT.withName("y")); + static final VarHandle x$VH = varHandle(LAYOUT, "x"); + static final VarHandle y$VH = varHandle(LAYOUT, "y"); + + private final MemorySegment seg; + + public COORD() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + public COORD(short x, short y) { + this(Arena.ofAuto().allocate(LAYOUT)); + x(x); + y(y); + } + + public COORD(COORD from) { + this(Arena.ofAuto().allocate(LAYOUT).copyFrom(Objects.requireNonNull(from).seg)); + } + + COORD(MemorySegment seg) { + this.seg = seg; + } + + COORD(MemorySegment seg, long offset) { + this.seg = Objects.requireNonNull(seg).asSlice(offset, LAYOUT.byteSize()); + } + + public short x() { + return (short) COORD.x$VH.get(seg); + } + + public void x(short x) { + COORD.x$VH.set(seg, x); + } + + public short y() { + return (short) COORD.y$VH.get(seg); + } + + public void y(short y) { + COORD.y$VH.set(seg, y); + } + + public COORD copy() { + return new COORD(this); + } + } + + public static class SMALL_RECT { + + static final GroupLayout LAYOUT = MemoryLayout.structLayout( + C_SHORT$LAYOUT.withName("Left"), + C_SHORT$LAYOUT.withName("Top"), + C_SHORT$LAYOUT.withName("Right"), + C_SHORT$LAYOUT.withName("Bottom")); + static final VarHandle Left$VH = varHandle(LAYOUT, "Left"); + static final VarHandle Top$VH = varHandle(LAYOUT, "Top"); + static final VarHandle Right$VH = varHandle(LAYOUT, "Right"); + static final VarHandle Bottom$VH = varHandle(LAYOUT, "Bottom"); + + private final MemorySegment seg; + + public SMALL_RECT() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + public SMALL_RECT(SMALL_RECT from) { + this(Arena.ofAuto().allocate(LAYOUT).copyFrom(from.seg)); + } + + SMALL_RECT(MemorySegment seg, long offset) { + this(seg.asSlice(offset, LAYOUT.byteSize())); + } + + SMALL_RECT(MemorySegment seg) { + this.seg = seg; + } + + public short left() { + return (short) Left$VH.get(seg); + } + + public short top() { + return (short) Top$VH.get(seg); + } + + public short right() { + return (short) Right$VH.get(seg); + } + + public short bottom() { + return (short) Bottom$VH.get(seg); + } + + public short width() { + return (short) (this.right() - this.left()); + } + + public short height() { + return (short) (this.bottom() - this.top()); + } + + public void left(short l) { + Left$VH.set(seg, l); + } + + public void top(short t) { + Top$VH.set(seg, t); + } + + public SMALL_RECT copy() { + return new SMALL_RECT(this); + } + } + + private static final Linker LINKER = Linker.nativeLinker(); + + private static final SymbolLookup SYMBOL_LOOKUP; + + static { + SymbolLookup loaderLookup = SymbolLookup.loaderLookup(); + SYMBOL_LOOKUP = + name -> loaderLookup.find(name).or(() -> LINKER.defaultLookup().find(name)); + } + + static MethodHandle downcallHandle(String name, FunctionDescriptor fdesc) { + return SYMBOL_LOOKUP + .find(name) + .map(addr -> LINKER.downcallHandle(addr, fdesc)) + .orElse(null); + } + + static T requireNonNull(T obj, String symbolName) { + if (obj == null) { + throw new UnsatisfiedLinkError("unresolved symbol: " + symbolName); + } + return obj; + } + + static VarHandle varHandle(MemoryLayout layout, String e1) { + return layout.varHandle(MemoryLayout.PathElement.groupElement(e1)); + } + + static VarHandle varHandle(MemoryLayout layout, String e1, String e2) { + return layout.varHandle(MemoryLayout.PathElement.groupElement(e1), MemoryLayout.PathElement.groupElement(e2)); + } + + static long byteOffset(MemoryLayout layout, String e1) { + return layout.byteOffset(MemoryLayout.PathElement.groupElement(e1)); + } +} diff --git a/src/main/java/org/fusesource/jansi/ffm/WindowsAnsiProcessor.java b/src/main/java/org/fusesource/jansi/ffm/WindowsAnsiProcessor.java new file mode 100644 index 00000000..25d20030 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/ffm/WindowsAnsiProcessor.java @@ -0,0 +1,439 @@ +/* + * Copyright (C) 2009-2023 the original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.fusesource.jansi.ffm; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; + +import org.fusesource.jansi.WindowsSupport; +import org.fusesource.jansi.io.AnsiProcessor; +import org.fusesource.jansi.io.Colors; + +import static org.fusesource.jansi.ffm.Kernel32.*; + +/** + * A Windows ANSI escape processor, that uses JNA to access native platform + * API's to change the console attributes (see + * Jansi native Kernel32). + *

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

The native library used is named jansi and is loaded using HawtJNI Runtime + * Library + * + * @since 1.19 + * @author Hiram Chirino + * @author Joris Kuipers + */ +public class WindowsAnsiProcessor extends AnsiProcessor { + + private final long console; + + private static final short FOREGROUND_BLACK = 0; + private static final short FOREGROUND_YELLOW = (short) (FOREGROUND_RED | FOREGROUND_GREEN); + private static final short FOREGROUND_MAGENTA = (short) (FOREGROUND_BLUE | FOREGROUND_RED); + private static final short FOREGROUND_CYAN = (short) (FOREGROUND_BLUE | FOREGROUND_GREEN); + private static final short FOREGROUND_WHITE = (short) (FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE); + + private static final short BACKGROUND_BLACK = 0; + private static final short BACKGROUND_YELLOW = (short) (BACKGROUND_RED | BACKGROUND_GREEN); + private static final short BACKGROUND_MAGENTA = (short) (BACKGROUND_BLUE | BACKGROUND_RED); + private static final short BACKGROUND_CYAN = (short) (BACKGROUND_BLUE | BACKGROUND_GREEN); + private static final short BACKGROUND_WHITE = (short) (BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE); + + private static final short[] ANSI_FOREGROUND_COLOR_MAP = { + FOREGROUND_BLACK, + FOREGROUND_RED, + FOREGROUND_GREEN, + FOREGROUND_YELLOW, + FOREGROUND_BLUE, + FOREGROUND_MAGENTA, + FOREGROUND_CYAN, + FOREGROUND_WHITE, + }; + + private static final short[] ANSI_BACKGROUND_COLOR_MAP = { + BACKGROUND_BLACK, + BACKGROUND_RED, + BACKGROUND_GREEN, + BACKGROUND_YELLOW, + BACKGROUND_BLUE, + BACKGROUND_MAGENTA, + BACKGROUND_CYAN, + BACKGROUND_WHITE, + }; + + private final CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); + private final short originalColors; + + private boolean negative; + private short savedX = -1; + private short savedY = -1; + + public WindowsAnsiProcessor(OutputStream ps, long console) throws IOException { + super(ps); + this.console = console; + getConsoleInfo(); + originalColors = info.attributes; + } + + public WindowsAnsiProcessor(OutputStream ps, boolean stdout) throws IOException { + this(ps, GetStdHandle(stdout ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE)); + } + + public WindowsAnsiProcessor(OutputStream ps) throws IOException { + this(ps, true); + } + + private void getConsoleInfo() throws IOException { + os.flush(); + if (GetConsoleScreenBufferInfo(console, info) == 0) { + throw new IOException("Could not get the screen info: " + WindowsSupport.getLastErrorMessage()); + } + if (negative) { + info.attributes = invertAttributeColors(info.attributes); + } + } + + private void applyAttribute() throws IOException { + os.flush(); + short attributes = info.attributes; + if (negative) { + attributes = invertAttributeColors(attributes); + } + if (SetConsoleTextAttribute(console, attributes) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + + private short invertAttributeColors(short attributes) { + // Swap the the Foreground and Background bits. + int fg = 0x000F & attributes; + fg <<= 4; + int bg = 0X00F0 & attributes; + bg >>= 4; + attributes = (short) ((attributes & 0xFF00) | fg | bg); + return attributes; + } + + private void applyCursorPosition() throws IOException { + if (SetConsoleCursorPosition(console, info.cursorPosition.copy()) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + + @Override + protected void processEraseScreen(int eraseOption) throws IOException { + getConsoleInfo(); + int[] written = new int[1]; + switch (eraseOption) { + case ERASE_SCREEN: + COORD topLeft = new COORD(); + topLeft.x = 0; + topLeft.y = info.window.top; + int screenLength = info.window.height() * info.size.x; + FillConsoleOutputAttribute(console, info.attributes, screenLength, topLeft, written); + FillConsoleOutputCharacterW(console, ' ', screenLength, topLeft, written); + break; + case ERASE_SCREEN_TO_BEGINING: + COORD topLeft2 = new COORD(); + topLeft2.x = 0; + topLeft2.y = info.window.top; + int lengthToCursor = (info.cursorPosition.y - info.window.top) * info.size.x + info.cursorPosition.x; + FillConsoleOutputAttribute(console, info.attributes, lengthToCursor, topLeft2, written); + FillConsoleOutputCharacterW(console, ' ', lengthToCursor, topLeft2, written); + break; + case ERASE_SCREEN_TO_END: + int lengthToEnd = (info.window.bottom - info.cursorPosition.y) * info.size.x + + (info.size.x - info.cursorPosition.x); + FillConsoleOutputAttribute(console, info.attributes, lengthToEnd, info.cursorPosition.copy(), written); + FillConsoleOutputCharacterW(console, ' ', lengthToEnd, info.cursorPosition.copy(), written); + break; + default: + break; + } + } + + @Override + protected void processEraseLine(int eraseOption) throws IOException { + getConsoleInfo(); + int[] written = new int[1]; + switch (eraseOption) { + case ERASE_LINE: + COORD leftColCurrRow = info.cursorPosition.copy(); + leftColCurrRow.x = 0; + FillConsoleOutputAttribute(console, info.attributes, info.size.x, leftColCurrRow, written); + FillConsoleOutputCharacterW(console, ' ', info.size.x, leftColCurrRow, written); + break; + case ERASE_LINE_TO_BEGINING: + COORD leftColCurrRow2 = info.cursorPosition.copy(); + leftColCurrRow2.x = 0; + FillConsoleOutputAttribute(console, info.attributes, info.cursorPosition.x, leftColCurrRow2, written); + FillConsoleOutputCharacterW(console, ' ', info.cursorPosition.x, leftColCurrRow2, written); + break; + case ERASE_LINE_TO_END: + int lengthToLastCol = info.size.x - info.cursorPosition.x; + FillConsoleOutputAttribute( + console, info.attributes, lengthToLastCol, info.cursorPosition.copy(), written); + FillConsoleOutputCharacterW(console, ' ', lengthToLastCol, info.cursorPosition.copy(), written); + break; + default: + break; + } + } + + @Override + protected void processCursorLeft(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition.x = (short) Math.max(0, info.cursorPosition.x - count); + applyCursorPosition(); + } + + @Override + protected void processCursorRight(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition.x = (short) Math.min(info.window.width(), info.cursorPosition.x + count); + applyCursorPosition(); + } + + @Override + protected void processCursorDown(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition.y = (short) Math.min(Math.max(0, info.size.y - 1), info.cursorPosition.y + count); + applyCursorPosition(); + } + + @Override + protected void processCursorUp(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition.y = (short) Math.max(info.window.top, info.cursorPosition.y - count); + applyCursorPosition(); + } + + @Override + protected void processCursorTo(int row, int col) throws IOException { + getConsoleInfo(); + info.cursorPosition.y = (short) Math.max(info.window.top, Math.min(info.size.y, info.window.top + row - 1)); + info.cursorPosition.x = (short) Math.max(0, Math.min(info.window.width(), col - 1)); + applyCursorPosition(); + } + + @Override + protected void processCursorToColumn(int x) throws IOException { + getConsoleInfo(); + info.cursorPosition.x = (short) Math.max(0, Math.min(info.window.width(), x - 1)); + applyCursorPosition(); + } + + @Override + protected void processCursorUpLine(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition.x = 0; + info.cursorPosition.y = (short) Math.max(info.window.top, info.cursorPosition.y - count); + applyCursorPosition(); + } + + @Override + protected void processCursorDownLine(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition.x = 0; + info.cursorPosition.y = (short) Math.max(info.window.top, info.cursorPosition.y + count); + applyCursorPosition(); + } + + @Override + protected void processSetForegroundColor(int color, boolean bright) throws IOException { + info.attributes = (short) ((info.attributes & ~0x0007) | ANSI_FOREGROUND_COLOR_MAP[color]); + if (bright) { + info.attributes |= FOREGROUND_INTENSITY; + } + applyAttribute(); + } + + @Override + protected void processSetForegroundColorExt(int paletteIndex) throws IOException { + int round = Colors.roundColor(paletteIndex, 16); + processSetForegroundColor(round >= 8 ? round - 8 : round, round >= 8); + } + + @Override + protected void processSetForegroundColorExt(int r, int g, int b) throws IOException { + int round = Colors.roundRgbColor(r, g, b, 16); + processSetForegroundColor(round >= 8 ? round - 8 : round, round >= 8); + } + + @Override + protected void processSetBackgroundColor(int color, boolean bright) throws IOException { + info.attributes = (short) ((info.attributes & ~0x0070) | ANSI_BACKGROUND_COLOR_MAP[color]); + if (bright) { + info.attributes |= BACKGROUND_INTENSITY; + } + applyAttribute(); + } + + @Override + protected void processSetBackgroundColorExt(int paletteIndex) throws IOException { + int round = Colors.roundColor(paletteIndex, 16); + processSetBackgroundColor(round >= 8 ? round - 8 : round, round >= 8); + } + + @Override + protected void processSetBackgroundColorExt(int r, int g, int b) throws IOException { + int round = Colors.roundRgbColor(r, g, b, 16); + processSetBackgroundColor(round >= 8 ? round - 8 : round, round >= 8); + } + + @Override + protected void processDefaultTextColor() throws IOException { + info.attributes = (short) ((info.attributes & ~0x000F) | (originalColors & 0xF)); + info.attributes = (short) (info.attributes & ~FOREGROUND_INTENSITY); + applyAttribute(); + } + + @Override + protected void processDefaultBackgroundColor() throws IOException { + info.attributes = (short) ((info.attributes & ~0x00F0) | (originalColors & 0xF0)); + info.attributes = (short) (info.attributes & ~BACKGROUND_INTENSITY); + applyAttribute(); + } + + @Override + protected void processAttributeReset() throws IOException { + info.attributes = (short) ((info.attributes & ~0x00FF) | originalColors); + this.negative = false; + applyAttribute(); + } + + @Override + protected void processSetAttribute(int attribute) throws IOException { + switch (attribute) { + case ATTRIBUTE_INTENSITY_BOLD: + info.attributes = (short) (info.attributes | FOREGROUND_INTENSITY); + applyAttribute(); + break; + case ATTRIBUTE_INTENSITY_NORMAL: + info.attributes = (short) (info.attributes & ~FOREGROUND_INTENSITY); + applyAttribute(); + break; + + // Yeah, setting the background intensity is not underlining.. but it's best we can do + // using the Windows console API + case ATTRIBUTE_UNDERLINE: + info.attributes = (short) (info.attributes | BACKGROUND_INTENSITY); + applyAttribute(); + break; + case ATTRIBUTE_UNDERLINE_OFF: + info.attributes = (short) (info.attributes & ~BACKGROUND_INTENSITY); + applyAttribute(); + break; + + case ATTRIBUTE_NEGATIVE_ON: + negative = true; + applyAttribute(); + break; + case ATTRIBUTE_NEGATIVE_OFF: + negative = false; + applyAttribute(); + break; + default: + break; + } + } + + @Override + protected void processSaveCursorPosition() throws IOException { + getConsoleInfo(); + savedX = info.cursorPosition.x; + savedY = info.cursorPosition.y; + } + + @Override + protected void processRestoreCursorPosition() throws IOException { + // restore only if there was a save operation first + if (savedX != -1 && savedY != -1) { + os.flush(); + info.cursorPosition.x = savedX; + info.cursorPosition.y = savedY; + applyCursorPosition(); + } + } + + @Override + protected void processInsertLine(int optionInt) throws IOException { + getConsoleInfo(); + SMALL_RECT scroll = info.window.copy(); + scroll.top = info.cursorPosition.y; + COORD org = new COORD(); + org.x = 0; + org.y = (short) (info.cursorPosition.y + optionInt); + CHAR_INFO info = new CHAR_INFO(); + info.attributes = originalColors; + info.unicodeChar = ' '; + if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + + @Override + protected void processDeleteLine(int optionInt) throws IOException { + getConsoleInfo(); + SMALL_RECT scroll = info.window.copy(); + scroll.top = info.cursorPosition.y; + COORD org = new COORD(); + org.x = 0; + org.y = (short) (info.cursorPosition.y - optionInt); + CHAR_INFO info = new CHAR_INFO(); + info.attributes = originalColors; + info.unicodeChar = ' '; + if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + + @Override + protected void processChangeWindowTitle(String label) { + SetConsoleTitle(label); + } +} diff --git a/src/main/java/org/fusesource/jansi/io/WindowsAnsiProcessor.java b/src/main/java/org/fusesource/jansi/io/WindowsAnsiProcessor.java index 74a178a5..dccb8403 100644 --- a/src/main/java/org/fusesource/jansi/io/WindowsAnsiProcessor.java +++ b/src/main/java/org/fusesource/jansi/io/WindowsAnsiProcessor.java @@ -18,31 +18,6 @@ import java.io.IOException; import java.io.OutputStream; -import org.fusesource.jansi.WindowsSupport; -import org.fusesource.jansi.internal.Kernel32.CONSOLE_SCREEN_BUFFER_INFO; -import org.fusesource.jansi.internal.Kernel32.COORD; - -import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_BLUE; -import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_GREEN; -import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_INTENSITY; -import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_RED; -import static org.fusesource.jansi.internal.Kernel32.CHAR_INFO; -import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_BLUE; -import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_GREEN; -import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_INTENSITY; -import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_RED; -import static org.fusesource.jansi.internal.Kernel32.FillConsoleOutputAttribute; -import static org.fusesource.jansi.internal.Kernel32.FillConsoleOutputCharacterW; -import static org.fusesource.jansi.internal.Kernel32.GetConsoleScreenBufferInfo; -import static org.fusesource.jansi.internal.Kernel32.GetStdHandle; -import static org.fusesource.jansi.internal.Kernel32.SMALL_RECT; -import static org.fusesource.jansi.internal.Kernel32.STD_ERROR_HANDLE; -import static org.fusesource.jansi.internal.Kernel32.STD_OUTPUT_HANDLE; -import static org.fusesource.jansi.internal.Kernel32.ScrollConsoleScreenBuffer; -import static org.fusesource.jansi.internal.Kernel32.SetConsoleCursorPosition; -import static org.fusesource.jansi.internal.Kernel32.SetConsoleTextAttribute; -import static org.fusesource.jansi.internal.Kernel32.SetConsoleTitle; - /** * A Windows ANSI escape processor, that uses JNA to access native platform * API's to change the console attributes (see @@ -51,374 +26,20 @@ * Library * * @since 1.19 + * @author Hiram Chirino + * @author Joris Kuipers */ -public final class WindowsAnsiProcessor extends AnsiProcessor { - - private final long console; - - private static final short FOREGROUND_BLACK = 0; - private static final short FOREGROUND_YELLOW = (short) (FOREGROUND_RED | FOREGROUND_GREEN); - private static final short FOREGROUND_MAGENTA = (short) (FOREGROUND_BLUE | FOREGROUND_RED); - private static final short FOREGROUND_CYAN = (short) (FOREGROUND_BLUE | FOREGROUND_GREEN); - private static final short FOREGROUND_WHITE = (short) (FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE); - - private static final short BACKGROUND_BLACK = 0; - private static final short BACKGROUND_YELLOW = (short) (BACKGROUND_RED | BACKGROUND_GREEN); - private static final short BACKGROUND_MAGENTA = (short) (BACKGROUND_BLUE | BACKGROUND_RED); - private static final short BACKGROUND_CYAN = (short) (BACKGROUND_BLUE | BACKGROUND_GREEN); - private static final short BACKGROUND_WHITE = (short) (BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE); - - private static final short[] ANSI_FOREGROUND_COLOR_MAP = { - FOREGROUND_BLACK, - FOREGROUND_RED, - FOREGROUND_GREEN, - FOREGROUND_YELLOW, - FOREGROUND_BLUE, - FOREGROUND_MAGENTA, - FOREGROUND_CYAN, - FOREGROUND_WHITE, - }; - - private static final short[] ANSI_BACKGROUND_COLOR_MAP = { - BACKGROUND_BLACK, - BACKGROUND_RED, - BACKGROUND_GREEN, - BACKGROUND_YELLOW, - BACKGROUND_BLUE, - BACKGROUND_MAGENTA, - BACKGROUND_CYAN, - BACKGROUND_WHITE, - }; - - private final CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); - private final short originalColors; - - private boolean negative; - private short savedX = -1; - private short savedY = -1; +public final class WindowsAnsiProcessor extends org.fusesource.jansi.internal.WindowsAnsiProcessor { public WindowsAnsiProcessor(OutputStream ps, long console) throws IOException { - super(ps); - this.console = console; - getConsoleInfo(); - originalColors = info.attributes; + super(ps, console); } public WindowsAnsiProcessor(OutputStream ps, boolean stdout) throws IOException { - this(ps, GetStdHandle(stdout ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE)); + super(ps, stdout); } public WindowsAnsiProcessor(OutputStream ps) throws IOException { - this(ps, true); - } - - private void getConsoleInfo() throws IOException { - os.flush(); - if (GetConsoleScreenBufferInfo(console, info) == 0) { - throw new IOException("Could not get the screen info: " + WindowsSupport.getLastErrorMessage()); - } - if (negative) { - info.attributes = invertAttributeColors(info.attributes); - } - } - - private void applyAttribute() throws IOException { - os.flush(); - short attributes = info.attributes; - if (negative) { - attributes = invertAttributeColors(attributes); - } - if (SetConsoleTextAttribute(console, attributes) == 0) { - throw new IOException(WindowsSupport.getLastErrorMessage()); - } - } - - private short invertAttributeColors(short attributes) { - // Swap the the Foreground and Background bits. - int fg = 0x000F & attributes; - fg <<= 4; - int bg = 0X00F0 & attributes; - bg >>= 4; - attributes = (short) ((attributes & 0xFF00) | fg | bg); - return attributes; - } - - private void applyCursorPosition() throws IOException { - if (SetConsoleCursorPosition(console, info.cursorPosition.copy()) == 0) { - throw new IOException(WindowsSupport.getLastErrorMessage()); - } - } - - @Override - protected void processEraseScreen(int eraseOption) throws IOException { - getConsoleInfo(); - int[] written = new int[1]; - switch (eraseOption) { - case ERASE_SCREEN: - COORD topLeft = new COORD(); - topLeft.x = 0; - topLeft.y = info.window.top; - int screenLength = info.window.height() * info.size.x; - FillConsoleOutputAttribute(console, info.attributes, screenLength, topLeft, written); - FillConsoleOutputCharacterW(console, ' ', screenLength, topLeft, written); - break; - case ERASE_SCREEN_TO_BEGINING: - COORD topLeft2 = new COORD(); - topLeft2.x = 0; - topLeft2.y = info.window.top; - int lengthToCursor = (info.cursorPosition.y - info.window.top) * info.size.x + info.cursorPosition.x; - FillConsoleOutputAttribute(console, info.attributes, lengthToCursor, topLeft2, written); - FillConsoleOutputCharacterW(console, ' ', lengthToCursor, topLeft2, written); - break; - case ERASE_SCREEN_TO_END: - int lengthToEnd = (info.window.bottom - info.cursorPosition.y) * info.size.x - + (info.size.x - info.cursorPosition.x); - FillConsoleOutputAttribute(console, info.attributes, lengthToEnd, info.cursorPosition.copy(), written); - FillConsoleOutputCharacterW(console, ' ', lengthToEnd, info.cursorPosition.copy(), written); - break; - default: - break; - } - } - - @Override - protected void processEraseLine(int eraseOption) throws IOException { - getConsoleInfo(); - int[] written = new int[1]; - switch (eraseOption) { - case ERASE_LINE: - COORD leftColCurrRow = info.cursorPosition.copy(); - leftColCurrRow.x = 0; - FillConsoleOutputAttribute(console, info.attributes, info.size.x, leftColCurrRow, written); - FillConsoleOutputCharacterW(console, ' ', info.size.x, leftColCurrRow, written); - break; - case ERASE_LINE_TO_BEGINING: - COORD leftColCurrRow2 = info.cursorPosition.copy(); - leftColCurrRow2.x = 0; - FillConsoleOutputAttribute(console, info.attributes, info.cursorPosition.x, leftColCurrRow2, written); - FillConsoleOutputCharacterW(console, ' ', info.cursorPosition.x, leftColCurrRow2, written); - break; - case ERASE_LINE_TO_END: - int lengthToLastCol = info.size.x - info.cursorPosition.x; - FillConsoleOutputAttribute( - console, info.attributes, lengthToLastCol, info.cursorPosition.copy(), written); - FillConsoleOutputCharacterW(console, ' ', lengthToLastCol, info.cursorPosition.copy(), written); - break; - default: - break; - } - } - - @Override - protected void processCursorLeft(int count) throws IOException { - getConsoleInfo(); - info.cursorPosition.x = (short) Math.max(0, info.cursorPosition.x - count); - applyCursorPosition(); - } - - @Override - protected void processCursorRight(int count) throws IOException { - getConsoleInfo(); - info.cursorPosition.x = (short) Math.min(info.window.width(), info.cursorPosition.x + count); - applyCursorPosition(); - } - - @Override - protected void processCursorDown(int count) throws IOException { - getConsoleInfo(); - info.cursorPosition.y = (short) Math.min(Math.max(0, info.size.y - 1), info.cursorPosition.y + count); - applyCursorPosition(); - } - - @Override - protected void processCursorUp(int count) throws IOException { - getConsoleInfo(); - info.cursorPosition.y = (short) Math.max(info.window.top, info.cursorPosition.y - count); - applyCursorPosition(); - } - - @Override - protected void processCursorTo(int row, int col) throws IOException { - getConsoleInfo(); - info.cursorPosition.y = (short) Math.max(info.window.top, Math.min(info.size.y, info.window.top + row - 1)); - info.cursorPosition.x = (short) Math.max(0, Math.min(info.window.width(), col - 1)); - applyCursorPosition(); - } - - @Override - protected void processCursorToColumn(int x) throws IOException { - getConsoleInfo(); - info.cursorPosition.x = (short) Math.max(0, Math.min(info.window.width(), x - 1)); - applyCursorPosition(); - } - - @Override - protected void processCursorUpLine(int count) throws IOException { - getConsoleInfo(); - info.cursorPosition.x = 0; - info.cursorPosition.y = (short) Math.max(info.window.top, info.cursorPosition.y - count); - applyCursorPosition(); - } - - @Override - protected void processCursorDownLine(int count) throws IOException { - getConsoleInfo(); - info.cursorPosition.x = 0; - info.cursorPosition.y = (short) Math.max(info.window.top, info.cursorPosition.y + count); - applyCursorPosition(); - } - - @Override - protected void processSetForegroundColor(int color, boolean bright) throws IOException { - info.attributes = (short) ((info.attributes & ~0x0007) | ANSI_FOREGROUND_COLOR_MAP[color]); - if (bright) { - info.attributes |= FOREGROUND_INTENSITY; - } - applyAttribute(); - } - - @Override - protected void processSetForegroundColorExt(int paletteIndex) throws IOException { - int round = Colors.roundColor(paletteIndex, 16); - processSetForegroundColor(round >= 8 ? round - 8 : round, round >= 8); - } - - @Override - protected void processSetForegroundColorExt(int r, int g, int b) throws IOException { - int round = Colors.roundRgbColor(r, g, b, 16); - processSetForegroundColor(round >= 8 ? round - 8 : round, round >= 8); - } - - @Override - protected void processSetBackgroundColor(int color, boolean bright) throws IOException { - info.attributes = (short) ((info.attributes & ~0x0070) | ANSI_BACKGROUND_COLOR_MAP[color]); - if (bright) { - info.attributes |= BACKGROUND_INTENSITY; - } - applyAttribute(); - } - - @Override - protected void processSetBackgroundColorExt(int paletteIndex) throws IOException { - int round = Colors.roundColor(paletteIndex, 16); - processSetBackgroundColor(round >= 8 ? round - 8 : round, round >= 8); - } - - @Override - protected void processSetBackgroundColorExt(int r, int g, int b) throws IOException { - int round = Colors.roundRgbColor(r, g, b, 16); - processSetBackgroundColor(round >= 8 ? round - 8 : round, round >= 8); - } - - @Override - protected void processDefaultTextColor() throws IOException { - info.attributes = (short) ((info.attributes & ~0x000F) | (originalColors & 0xF)); - info.attributes = (short) (info.attributes & ~FOREGROUND_INTENSITY); - applyAttribute(); - } - - @Override - protected void processDefaultBackgroundColor() throws IOException { - info.attributes = (short) ((info.attributes & ~0x00F0) | (originalColors & 0xF0)); - info.attributes = (short) (info.attributes & ~BACKGROUND_INTENSITY); - applyAttribute(); - } - - @Override - protected void processAttributeReset() throws IOException { - info.attributes = (short) ((info.attributes & ~0x00FF) | originalColors); - this.negative = false; - applyAttribute(); - } - - @Override - protected void processSetAttribute(int attribute) throws IOException { - switch (attribute) { - case ATTRIBUTE_INTENSITY_BOLD: - info.attributes = (short) (info.attributes | FOREGROUND_INTENSITY); - applyAttribute(); - break; - case ATTRIBUTE_INTENSITY_NORMAL: - info.attributes = (short) (info.attributes & ~FOREGROUND_INTENSITY); - applyAttribute(); - break; - - // Yeah, setting the background intensity is not underlining.. but it's best we can do - // using the Windows console API - case ATTRIBUTE_UNDERLINE: - info.attributes = (short) (info.attributes | BACKGROUND_INTENSITY); - applyAttribute(); - break; - case ATTRIBUTE_UNDERLINE_OFF: - info.attributes = (short) (info.attributes & ~BACKGROUND_INTENSITY); - applyAttribute(); - break; - - case ATTRIBUTE_NEGATIVE_ON: - negative = true; - applyAttribute(); - break; - case ATTRIBUTE_NEGATIVE_OFF: - negative = false; - applyAttribute(); - break; - default: - break; - } - } - - @Override - protected void processSaveCursorPosition() throws IOException { - getConsoleInfo(); - savedX = info.cursorPosition.x; - savedY = info.cursorPosition.y; - } - - @Override - protected void processRestoreCursorPosition() throws IOException { - // restore only if there was a save operation first - if (savedX != -1 && savedY != -1) { - os.flush(); - info.cursorPosition.x = savedX; - info.cursorPosition.y = savedY; - applyCursorPosition(); - } - } - - @Override - protected void processInsertLine(int optionInt) throws IOException { - getConsoleInfo(); - SMALL_RECT scroll = info.window.copy(); - scroll.top = info.cursorPosition.y; - COORD org = new COORD(); - org.x = 0; - org.y = (short) (info.cursorPosition.y + optionInt); - CHAR_INFO info = new CHAR_INFO(); - info.attributes = originalColors; - info.unicodeChar = ' '; - if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { - throw new IOException(WindowsSupport.getLastErrorMessage()); - } - } - - @Override - protected void processDeleteLine(int optionInt) throws IOException { - getConsoleInfo(); - SMALL_RECT scroll = info.window.copy(); - scroll.top = info.cursorPosition.y; - COORD org = new COORD(); - org.x = 0; - org.y = (short) (info.cursorPosition.y - optionInt); - CHAR_INFO info = new CHAR_INFO(); - info.attributes = originalColors; - info.unicodeChar = ' '; - if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { - throw new IOException(WindowsSupport.getLastErrorMessage()); - } - } - - @Override - protected void processChangeWindowTitle(String label) { - SetConsoleTitle(label); + super(ps); } } From 27a7bb5b3d1c2a7ada1e491bd9e033264c574b46 Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 29 Sep 2023 17:10:04 +0800 Subject: [PATCH 04/12] Fix FFM backend on Windows (#263) --- .../org/fusesource/jansi/AnsiConsole.java | 5 +- .../jansi/ffm/AnsiConsoleSupportFfm.java | 94 ++----- .../org/fusesource/jansi/ffm/Kernel32.java | 229 ++++++++---------- .../fusesource/jansi/ffm/PosixCLibrary.java | 84 +++++++ .../jansi/ffm/WindowsAnsiProcessor.java | 82 +++---- .../fusesource/jansi/ffm/WindowsCLibrary.java | 116 +++++++++ .../org/fusesource/jansi/internal/OSInfo.java | 12 +- 7 files changed, 362 insertions(+), 260 deletions(-) create mode 100644 src/main/java/org/fusesource/jansi/ffm/PosixCLibrary.java create mode 100644 src/main/java/org/fusesource/jansi/ffm/WindowsCLibrary.java diff --git a/src/main/java/org/fusesource/jansi/AnsiConsole.java b/src/main/java/org/fusesource/jansi/AnsiConsole.java index ff0cc657..d8de9dc9 100644 --- a/src/main/java/org/fusesource/jansi/AnsiConsole.java +++ b/src/main/java/org/fusesource/jansi/AnsiConsole.java @@ -24,8 +24,8 @@ import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.nio.charset.UnsupportedCharsetException; -import java.util.Locale; +import org.fusesource.jansi.internal.OSInfo; import org.fusesource.jansi.io.AnsiOutputStream; import org.fusesource.jansi.io.AnsiProcessor; import org.fusesource.jansi.io.FastBufferedOutputStream; @@ -194,8 +194,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("/"); diff --git a/src/main/java/org/fusesource/jansi/ffm/AnsiConsoleSupportFfm.java b/src/main/java/org/fusesource/jansi/ffm/AnsiConsoleSupportFfm.java index b1af763f..2033fad8 100644 --- a/src/main/java/org/fusesource/jansi/ffm/AnsiConsoleSupportFfm.java +++ b/src/main/java/org/fusesource/jansi/ffm/AnsiConsoleSupportFfm.java @@ -18,44 +18,16 @@ import java.io.IOException; import java.io.OutputStream; import java.lang.foreign.Arena; -import java.lang.foreign.FunctionDescriptor; -import java.lang.foreign.GroupLayout; -import java.lang.foreign.Linker; -import java.lang.foreign.MemoryLayout; import java.lang.foreign.MemorySegment; import java.lang.foreign.ValueLayout; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.VarHandle; import org.fusesource.jansi.AnsiConsoleSupport; +import org.fusesource.jansi.internal.OSInfo; import org.fusesource.jansi.io.AnsiProcessor; import static org.fusesource.jansi.ffm.Kernel32.*; public class AnsiConsoleSupportFfm implements AnsiConsoleSupport { - static GroupLayout wsLayout; - static MethodHandle ioctl; - static VarHandle ws_col; - static MethodHandle isatty; - - static { - wsLayout = MemoryLayout.structLayout( - ValueLayout.JAVA_SHORT.withName("ws_row"), - ValueLayout.JAVA_SHORT.withName("ws_col"), - ValueLayout.JAVA_SHORT, - ValueLayout.JAVA_SHORT); - ws_col = wsLayout.varHandle(MemoryLayout.PathElement.groupElement("ws_col")); - Linker linker = Linker.nativeLinker(); - ioctl = linker.downcallHandle( - linker.defaultLookup().find("ioctl").get(), - FunctionDescriptor.of( - ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS), - Linker.Option.firstVariadicArg(2)); - isatty = linker.downcallHandle( - linker.defaultLookup().find("isatty").get(), - FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT)); - } - @Override public String getProviderName() { return "ffm"; @@ -63,48 +35,11 @@ public String getProviderName() { @Override public CLibrary getCLibrary() { - return new CLibrary() { - static final int TIOCGWINSZ; - - static { - String osName = System.getProperty("os.name"); - if (osName.startsWith("Linux")) { - String arch = System.getProperty("os.arch"); - boolean isMipsPpcOrSparc = - arch.startsWith("mips") || arch.startsWith("ppc") || arch.startsWith("sparc"); - TIOCGWINSZ = isMipsPpcOrSparc ? 0x40087468 : 0x00005413; - } else if (osName.startsWith("Solaris") || osName.startsWith("SunOS")) { - int _TIOC = ('T' << 8); - TIOCGWINSZ = (_TIOC | 104); - } else if (osName.startsWith("Mac") || osName.startsWith("Darwin")) { - TIOCGWINSZ = 0x40087468; - } else if (osName.startsWith("FreeBSD")) { - TIOCGWINSZ = 0x40087468; - } else { - throw new UnsupportedOperationException(); - } - } - - @Override - public short getTerminalWidth(int fd) { - MemorySegment segment = Arena.ofAuto().allocate(wsLayout); - try { - int res = (int) ioctl.invoke(fd, (long) TIOCGWINSZ, segment); - return (short) ws_col.get(segment); - } catch (Throwable e) { - throw new RuntimeException("Unable to ioctl(TIOCGWINSZ)", e); - } - } - - @Override - public int isTty(int fd) { - try { - return (int) isatty.invoke(fd); - } catch (Throwable e) { - throw new RuntimeException("Unable to call isatty", e); - } - } - }; + if (OSInfo.isWindows()) { + return new WindowsCLibrary(); + } else { + return new PosixCLibrary(); + } } @Override @@ -118,9 +53,11 @@ public int isTty(long console) { @Override public int getTerminalWidth(long console) { - CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); - GetConsoleScreenBufferInfo(MemorySegment.ofAddress(console), info); - return info.windowWidth(); + 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 @@ -131,8 +68,8 @@ public long getStdHandle(boolean stdout) { @Override public int getConsoleMode(long console, int[] mode) { - try (Arena session = Arena.ofConfined()) { - MemorySegment written = session.allocate(ValueLayout.JAVA_INT); + 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; @@ -151,10 +88,7 @@ public int getLastError() { @Override public String getErrorMessage(int errorCode) { - int bufferSize = 160; - MemorySegment data = Arena.ofAuto().allocate(bufferSize); - FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, null, errorCode, 0, data, bufferSize, null); - return data.getUtf8String(0).trim(); + return org.fusesource.jansi.ffm.Kernel32.getErrorMessage(errorCode); } @Override diff --git a/src/main/java/org/fusesource/jansi/ffm/Kernel32.java b/src/main/java/org/fusesource/jansi/ffm/Kernel32.java index fc17db68..0cc409ac 100644 --- a/src/main/java/org/fusesource/jansi/ffm/Kernel32.java +++ b/src/main/java/org/fusesource/jansi/ffm/Kernel32.java @@ -27,20 +27,13 @@ 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.JAVA_INT; -import static java.lang.foreign.ValueLayout.OfBoolean; -import static java.lang.foreign.ValueLayout.OfByte; -import static java.lang.foreign.ValueLayout.OfChar; -import static java.lang.foreign.ValueLayout.OfDouble; -import static java.lang.foreign.ValueLayout.OfFloat; -import static java.lang.foreign.ValueLayout.OfInt; -import static java.lang.foreign.ValueLayout.OfLong; -import static java.lang.foreign.ValueLayout.OfShort; +import static java.lang.foreign.ValueLayout.*; -@SuppressWarnings({"unused", "CopyConstructorMissesField"}) -class Kernel32 { +@SuppressWarnings("unused") +final class Kernel32 { public static final int FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000; @@ -105,7 +98,7 @@ public static int WaitForSingleObject(MemorySegment hHandle, int dwMilliseconds) public static MemorySegment GetStdHandle(int nStdHandle) { MethodHandle mh$ = requireNonNull(GetStdHandle$MH, "GetStdHandle"); try { - return MemorySegment.ofAddress((long) mh$.invokeExact(nStdHandle)); + return (MemorySegment) mh$.invokeExact(nStdHandle); } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } @@ -121,14 +114,7 @@ public static int FormatMessageW( MemorySegment Arguments) { MethodHandle mh$ = requireNonNull(FormatMessageW$MH, "FormatMessageW"); try { - return (int) mh$.invokeExact( - dwFlags, - lpSource.address(), - dwMessageId, - dwLanguageId, - lpBuffer.address(), - nSize, - Arguments.address()); + return (int) mh$.invokeExact(dwFlags, lpSource, dwMessageId, dwLanguageId, lpBuffer, nSize, Arguments); } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } @@ -146,7 +132,7 @@ public static int SetConsoleTextAttribute(MemorySegment hConsoleOutput, short wA public static int SetConsoleMode(MemorySegment hConsoleHandle, int dwMode) { MethodHandle mh$ = requireNonNull(SetConsoleMode$MH, "SetConsoleMode"); try { - return (int) mh$.invokeExact(hConsoleHandle.address(), dwMode); + return (int) mh$.invokeExact(hConsoleHandle, dwMode); } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } @@ -155,7 +141,7 @@ public static int SetConsoleMode(MemorySegment hConsoleHandle, int dwMode) { public static int GetConsoleMode(MemorySegment hConsoleHandle, MemorySegment lpMode) { MethodHandle mh$ = requireNonNull(GetConsoleMode$MH, "GetConsoleMode"); try { - return (int) mh$.invokeExact(hConsoleHandle.address(), lpMode.address()); + return (int) mh$.invokeExact(hConsoleHandle, lpMode); } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } @@ -164,7 +150,7 @@ public static int GetConsoleMode(MemorySegment hConsoleHandle, MemorySegment lpM public static int SetConsoleTitleW(MemorySegment lpConsoleTitle) { MethodHandle mh$ = requireNonNull(SetConsoleTitleW$MH, "SetConsoleTitleW"); try { - return (int) mh$.invokeExact(lpConsoleTitle.address()); + return (int) mh$.invokeExact(lpConsoleTitle); } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } @@ -187,8 +173,7 @@ public static int FillConsoleOutputCharacterW( MemorySegment lpNumberOfCharsWritten) { MethodHandle mh$ = requireNonNull(FillConsoleOutputCharacterW$MH, "FillConsoleOutputCharacterW"); try { - return (int) mh$.invokeExact( - hConsoleOutput.address(), cCharacter, nLength, dwWriteCoord.seg, lpNumberOfCharsWritten.address()); + return (int) mh$.invokeExact(hConsoleOutput, cCharacter, nLength, dwWriteCoord.seg, lpNumberOfCharsWritten); } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } @@ -202,8 +187,7 @@ public static int FillConsoleOutputAttribute( MemorySegment lpNumberOfAttrsWritten) { MethodHandle mh$ = requireNonNull(FillConsoleOutputAttribute$MH, "FillConsoleOutputAttribute"); try { - return (int) mh$.invokeExact( - hConsoleOutput, wAttribute, nLength, dwWriteCoord.seg, lpNumberOfAttrsWritten.address()); + return (int) mh$.invokeExact(hConsoleOutput, wAttribute, nLength, dwWriteCoord.seg, lpNumberOfAttrsWritten); } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } @@ -228,8 +212,7 @@ public static int ReadConsoleInputW( MemorySegment hConsoleInput, MemorySegment lpBuffer, int nLength, MemorySegment lpNumberOfEventsRead) { MethodHandle mh$ = requireNonNull(ReadConsoleInputW$MH, "ReadConsoleInputW"); try { - return (int) mh$.invokeExact( - hConsoleInput.address(), lpBuffer.address(), nLength, lpNumberOfEventsRead.address()); + return (int) mh$.invokeExact(hConsoleInput, lpBuffer, nLength, lpNumberOfEventsRead); } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } @@ -239,8 +222,7 @@ public static int PeekConsoleInputW( MemorySegment hConsoleInput, MemorySegment lpBuffer, int nLength, MemorySegment lpNumberOfEventsRead) { MethodHandle mh$ = requireNonNull(PeekConsoleInputW$MH, "PeekConsoleInputW"); try { - return (int) mh$.invokeExact( - hConsoleInput.address(), lpBuffer.address(), nLength, lpNumberOfEventsRead.address()); + return (int) mh$.invokeExact(hConsoleInput, lpBuffer, nLength, lpNumberOfEventsRead); } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } @@ -250,7 +232,7 @@ public static int GetConsoleScreenBufferInfo( MemorySegment hConsoleOutput, CONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo) { MethodHandle mh$ = requireNonNull(GetConsoleScreenBufferInfo$MH, "GetConsoleScreenBufferInfo"); try { - return (int) mh$.invokeExact(hConsoleOutput.address(), lpConsoleScreenBufferInfo.seg); + return (int) mh$.invokeExact(hConsoleOutput, lpConsoleScreenBufferInfo.seg); } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } @@ -271,10 +253,28 @@ public static int ScrollConsoleScreenBuffer( } } - public static int GetLastError(Object... x0) { + public static int GetLastError() { MethodHandle mh$ = requireNonNull(GetLastError$MH, "GetLastError"); try { - return (int) mh$.invokeExact(x0); + 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$); } @@ -282,9 +282,9 @@ public static int GetLastError(Object... x0) { public static INPUT_RECORD[] readConsoleInputHelper(MemorySegment handle, int count, boolean peek) throws IOException { - try (Arena session = Arena.ofConfined()) { - MemorySegment inputRecordPtr = session.allocateArray(INPUT_RECORD.LAYOUT, count); - MemorySegment length = session.allocate(JAVA_INT, 0); + 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); @@ -307,23 +307,39 @@ public static String getLastErrorMessage() { public static String getErrorMessage(int errorCode) { int bufferSize = 160; - MemorySegment data = Arena.ofAuto().allocate(bufferSize); - FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, null, errorCode, 0, data, bufferSize, null); - return data.getUtf8String(0).trim(); - } - - static final OfBoolean C_BOOL$LAYOUT = ValueLayout.JAVA_BOOLEAN; - static final OfByte C_CHAR$LAYOUT = ValueLayout.JAVA_BYTE; - static final OfChar C_WCHAR$LAYOUT = ValueLayout.JAVA_CHAR.withByteAlignment(16); - static final OfShort C_SHORT$LAYOUT = ValueLayout.JAVA_SHORT.withByteAlignment(16); - static final OfShort C_WORD$LAYOUT = ValueLayout.JAVA_SHORT.withByteAlignment(16); - static final OfInt C_DWORD$LAYOUT = ValueLayout.JAVA_INT.withByteAlignment(32); - static final OfInt C_INT$LAYOUT = JAVA_INT.withByteAlignment(32); - static final OfLong C_LONG$LAYOUT = ValueLayout.JAVA_LONG.withByteAlignment(64); - static final OfLong C_LONG_LONG$LAYOUT = ValueLayout.JAVA_LONG.withByteAlignment(64); - static final OfFloat C_FLOAT$LAYOUT = ValueLayout.JAVA_FLOAT.withByteAlignment(32); - static final OfDouble C_DOUBLE$LAYOUT = ValueLayout.JAVA_DOUBLE.withByteAlignment(64); - static final AddressLayout C_POINTER$LAYOUT = ValueLayout.ADDRESS.withByteAlignment(64); + try (Arena arena = Arena.ofConfined()) { + MemorySegment data = arena.allocate(bufferSize); + FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, null, errorCode, 0, data, bufferSize, null); + return new String(data.toArray(JAVA_BYTE), StandardCharsets.UTF_16LE).trim(); + } + } + + private static final SymbolLookup SYMBOL_LOOKUP; + + static { + System.loadLibrary("Kernel32"); + SYMBOL_LOOKUP = SymbolLookup.loaderLookup().or(Linker.nativeLinker().defaultLookup()); + } + + 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)); @@ -389,8 +405,12 @@ public static String getErrorMessage(int errorCode) { 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 class INPUT_RECORD { + public static final class INPUT_RECORD { static final MemoryLayout LAYOUT = MemoryLayout.structLayout( ValueLayout.JAVA_SHORT.withName("EventType"), MemoryLayout.unionLayout( @@ -405,10 +425,6 @@ public static class INPUT_RECORD { private final MemorySegment seg; - public INPUT_RECORD() { - this(Arena.ofAuto().allocate(LAYOUT)); - } - INPUT_RECORD(MemorySegment seg) { this.seg = seg; } @@ -430,17 +446,13 @@ public FOCUS_EVENT_RECORD focusEvent() { } } - public static class MENU_EVENT_RECORD { + 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; - public MENU_EVENT_RECORD() { - this(Arena.ofAuto().allocate(LAYOUT)); - } - MENU_EVENT_RECORD(MemorySegment seg) { this.seg = seg; } @@ -454,17 +466,13 @@ public void commandId(int commandId) { } } - public static class FOCUS_EVENT_RECORD { + 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; - public FOCUS_EVENT_RECORD() { - this(Arena.ofAuto().allocate(LAYOUT)); - } - FOCUS_EVENT_RECORD(MemorySegment seg) { this.seg = Objects.requireNonNull(seg); } @@ -482,17 +490,13 @@ public void setFocus(boolean setFocus) { } } - public static class WINDOW_BUFFER_SIZE_RECORD { + 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; - public WINDOW_BUFFER_SIZE_RECORD() { - this(Arena.ofAuto().allocate(LAYOUT)); - } - WINDOW_BUFFER_SIZE_RECORD(MemorySegment seg) { this.seg = seg; } @@ -506,7 +510,7 @@ public String toString() { } } - public static class MOUSE_EVENT_RECORD { + public static final class MOUSE_EVENT_RECORD { private static final MemoryLayout LAYOUT = MemoryLayout.structLayout( COORD.LAYOUT.withName("dwMousePosition"), @@ -520,10 +524,6 @@ public static class MOUSE_EVENT_RECORD { private final MemorySegment seg; - public MOUSE_EVENT_RECORD() { - this(Arena.ofAuto().allocate(LAYOUT)); - } - MOUSE_EVENT_RECORD(MemorySegment seg) { this.seg = Objects.requireNonNull(seg); } @@ -554,7 +554,7 @@ public String toString() { } } - public static class KEY_EVENT_RECORD { + public static final class KEY_EVENT_RECORD { static final MemoryLayout LAYOUT = MemoryLayout.structLayout( JAVA_INT.withName("bKeyDown"), @@ -576,10 +576,6 @@ public static class KEY_EVENT_RECORD { final MemorySegment seg; - public KEY_EVENT_RECORD() { - this(Arena.ofAuto().allocate(LAYOUT)); - } - KEY_EVENT_RECORD(MemorySegment seg) { this.seg = seg; } @@ -620,7 +616,7 @@ public String toString() { } } - public static class CHAR_INFO { + public static final class CHAR_INFO { static final GroupLayout LAYOUT = MemoryLayout.structLayout( MemoryLayout.unionLayout(C_WCHAR$LAYOUT.withName("UnicodeChar"), C_CHAR$LAYOUT.withName("AsciiChar")) @@ -631,12 +627,12 @@ public static class CHAR_INFO { final MemorySegment seg; - public CHAR_INFO() { - this(Arena.ofAuto().allocate(LAYOUT)); + public CHAR_INFO(Arena arena) { + this(arena.allocate(LAYOUT)); } - public CHAR_INFO(char c, short a) { - this(); + public CHAR_INFO(Arena arena, char c, short a) { + this(arena); UnicodeChar$VH.set(seg, c); Attributes$VH.set(seg, a); } @@ -650,7 +646,7 @@ public char unicodeChar() { } } - public static class CONSOLE_SCREEN_BUFFER_INFO { + public static final class CONSOLE_SCREEN_BUFFER_INFO { static final GroupLayout LAYOUT = MemoryLayout.structLayout( COORD.LAYOUT.withName("dwSize"), COORD.LAYOUT.withName("dwCursorPosition"), @@ -664,8 +660,8 @@ public static class CONSOLE_SCREEN_BUFFER_INFO { private final MemorySegment seg; - public CONSOLE_SCREEN_BUFFER_INFO() { - this(Arena.ofAuto().allocate(LAYOUT)); + public CONSOLE_SCREEN_BUFFER_INFO(Arena arena) { + this(arena.allocate(LAYOUT)); } CONSOLE_SCREEN_BUFFER_INFO(MemorySegment seg) { @@ -701,7 +697,7 @@ public void attributes(short attr) { } } - public static class COORD { + public static final class COORD { static final GroupLayout LAYOUT = MemoryLayout.structLayout(C_SHORT$LAYOUT.withName("x"), C_SHORT$LAYOUT.withName("y")); @@ -710,20 +706,16 @@ public static class COORD { private final MemorySegment seg; - public COORD() { - this(Arena.ofAuto().allocate(LAYOUT)); + public COORD(Arena arena) { + this(arena.allocate(LAYOUT)); } - public COORD(short x, short y) { - this(Arena.ofAuto().allocate(LAYOUT)); + public COORD(Arena arena, short x, short y) { + this(arena.allocate(LAYOUT)); x(x); y(y); } - public COORD(COORD from) { - this(Arena.ofAuto().allocate(LAYOUT).copyFrom(Objects.requireNonNull(from).seg)); - } - COORD(MemorySegment seg) { this.seg = seg; } @@ -748,12 +740,12 @@ public void y(short y) { COORD.y$VH.set(seg, y); } - public COORD copy() { - return new COORD(this); + public COORD copy(Arena arena) { + return new COORD(arena.allocate(LAYOUT).copyFrom(seg)); } } - public static class SMALL_RECT { + public static final class SMALL_RECT { static final GroupLayout LAYOUT = MemoryLayout.structLayout( C_SHORT$LAYOUT.withName("Left"), @@ -767,14 +759,6 @@ public static class SMALL_RECT { private final MemorySegment seg; - public SMALL_RECT() { - this(Arena.ofAuto().allocate(LAYOUT)); - } - - public SMALL_RECT(SMALL_RECT from) { - this(Arena.ofAuto().allocate(LAYOUT).copyFrom(from.seg)); - } - SMALL_RECT(MemorySegment seg, long offset) { this(seg.asSlice(offset, LAYOUT.byteSize())); } @@ -815,28 +799,11 @@ public void top(short t) { Top$VH.set(seg, t); } - public SMALL_RECT copy() { - return new SMALL_RECT(this); + public SMALL_RECT copy(Arena arena) { + return new SMALL_RECT(arena.allocate(LAYOUT).copyFrom(seg)); } } - private static final Linker LINKER = Linker.nativeLinker(); - - private static final SymbolLookup SYMBOL_LOOKUP; - - static { - SymbolLookup loaderLookup = SymbolLookup.loaderLookup(); - SYMBOL_LOOKUP = - name -> loaderLookup.find(name).or(() -> LINKER.defaultLookup().find(name)); - } - - static MethodHandle downcallHandle(String name, FunctionDescriptor fdesc) { - return SYMBOL_LOOKUP - .find(name) - .map(addr -> LINKER.downcallHandle(addr, fdesc)) - .orElse(null); - } - static T requireNonNull(T obj, String symbolName) { if (obj == null) { throw new UnsatisfiedLinkError("unresolved symbol: " + symbolName); diff --git a/src/main/java/org/fusesource/jansi/ffm/PosixCLibrary.java b/src/main/java/org/fusesource/jansi/ffm/PosixCLibrary.java new file mode 100644 index 00000000..bd4f1f73 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/ffm/PosixCLibrary.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.ffm; + +import java.lang.foreign.*; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.VarHandle; + +import org.fusesource.jansi.AnsiConsoleSupport; + +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(); + ioctl = linker.downcallHandle( + linker.defaultLookup().find("ioctl").get(), + FunctionDescriptor.of( + ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS), + Linker.Option.firstVariadicArg(2)); + isatty = linker.downcallHandle( + linker.defaultLookup().find("isatty").get(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT)); + } + + @Override + public 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/ffm/WindowsAnsiProcessor.java b/src/main/java/org/fusesource/jansi/ffm/WindowsAnsiProcessor.java index 25d20030..e933ff0a 100644 --- a/src/main/java/org/fusesource/jansi/ffm/WindowsAnsiProcessor.java +++ b/src/main/java/org/fusesource/jansi/ffm/WindowsAnsiProcessor.java @@ -20,6 +20,7 @@ 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; @@ -76,7 +77,7 @@ public class WindowsAnsiProcessor extends AnsiProcessor { BACKGROUND_WHITE, }; - private final CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); + private final CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(Arena.ofAuto()); private final short originalColors; private boolean negative; @@ -130,7 +131,7 @@ private short invertAttributeColors(short attributes) { } private void applyCursorPosition() throws IOException { - if (SetConsoleCursorPosition(console, info.cursorPosition().copy()) == 0) { + if (SetConsoleCursorPosition(console, info.cursorPosition()) == 0) { throw new IOException(WindowsSupport.getLastErrorMessage()); } } @@ -138,11 +139,11 @@ private void applyCursorPosition() throws IOException { @Override protected void processEraseScreen(int eraseOption) throws IOException { getConsoleInfo(); - try (Arena session = Arena.ofConfined()) { - MemorySegment written = session.allocate(ValueLayout.JAVA_INT); + try (Arena arena = Arena.ofConfined()) { + MemorySegment written = arena.allocate(ValueLayout.JAVA_INT); switch (eraseOption) { case ERASE_SCREEN: - COORD topLeft = new COORD(); + COORD topLeft = new COORD(arena); topLeft.x((short) 0); topLeft.y(info.window().top()); int screenLength = info.window().height() * info.size().x(); @@ -150,7 +151,7 @@ protected void processEraseScreen(int eraseOption) throws IOException { FillConsoleOutputCharacterW(console, ' ', screenLength, topLeft, written); break; case ERASE_SCREEN_TO_BEGINING: - COORD topLeft2 = new COORD(); + COORD topLeft2 = new COORD(arena); topLeft2.x((short) 0); topLeft2.y(info.window().top()); int lengthToCursor = @@ -165,14 +166,8 @@ protected void processEraseScreen(int eraseOption) throws IOException { (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); + FillConsoleOutputAttribute(console, info.attributes(), lengthToEnd, info.cursorPosition(), written); + FillConsoleOutputCharacterW(console, ' ', lengthToEnd, info.cursorPosition(), written); break; default: break; @@ -183,18 +178,18 @@ protected void processEraseScreen(int eraseOption) throws IOException { @Override protected void processEraseLine(int eraseOption) throws IOException { getConsoleInfo(); - try (Arena session = Arena.ofConfined()) { - MemorySegment written = session.allocate(ValueLayout.JAVA_INT); + try (Arena arena = Arena.ofConfined()) { + MemorySegment written = arena.allocate(ValueLayout.JAVA_INT); switch (eraseOption) { case ERASE_LINE: - COORD leftColCurrRow = info.cursorPosition().copy(); + 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(); + COORD leftColCurrRow2 = info.cursorPosition().copy(arena); leftColCurrRow2.x((short) 0); FillConsoleOutputAttribute( console, info.attributes(), info.cursorPosition().x(), leftColCurrRow2, written); @@ -205,13 +200,8 @@ protected void processEraseLine(int eraseOption) throws IOException { int lengthToLastCol = info.size().x() - info.cursorPosition().x(); FillConsoleOutputAttribute( - console, - info.attributes(), - lengthToLastCol, - info.cursorPosition().copy(), - written); - FillConsoleOutputCharacterW( - console, ' ', lengthToLastCol, info.cursorPosition().copy(), written); + console, info.attributes(), lengthToLastCol, info.cursorPosition(), written); + FillConsoleOutputCharacterW(console, ' ', lengthToLastCol, info.cursorPosition(), written); break; default: break; @@ -404,35 +394,41 @@ protected void processRestoreCursorPosition() throws IOException { @Override protected void processInsertLine(int optionInt) throws IOException { getConsoleInfo(); - SMALL_RECT scroll = info.window().copy(); - scroll.top(info.cursorPosition().y()); - COORD org = new COORD(); - org.x((short) 0); - org.y((short) (info.cursorPosition().y() + optionInt)); - CHAR_INFO info = new CHAR_INFO(' ', originalColors); - if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { - throw new IOException(WindowsSupport.getLastErrorMessage()); + 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(); - SMALL_RECT scroll = info.window().copy(); - scroll.top(info.cursorPosition().y()); - COORD org = new COORD(); - org.x((short) 0); - org.y((short) (info.cursorPosition().y() - optionInt)); - CHAR_INFO info = new CHAR_INFO(' ', originalColors); - if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { - throw new IOException(WindowsSupport.getLastErrorMessage()); + 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 session = Arena.ofConfined()) { - MemorySegment str = session.allocateUtf8String(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/ffm/WindowsCLibrary.java b/src/main/java/org/fusesource/jansi/ffm/WindowsCLibrary.java new file mode 100644 index 00000000..c68854bf --- /dev/null +++ b/src/main/java/org/fusesource/jansi/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.ffm; + +import java.lang.foreign.*; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.VarHandle; +import java.nio.charset.StandardCharsets; + +import org.fusesource.jansi.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/OSInfo.java b/src/main/java/org/fusesource/jansi/internal/OSInfo.java index fe53cbb5..6957f8c7 100644 --- a/src/main/java/org/fusesource/jansi/internal/OSInfo.java +++ b/src/main/java/org/fusesource/jansi/internal/OSInfo.java @@ -120,8 +120,14 @@ 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() { @@ -131,7 +137,7 @@ public static boolean isAlpine() { InputStream in = p.getInputStream(); try { - return readFully(in).toLowerCase().contains("alpine"); + return readFully(in).toLowerCase(Locale.ROOT).contains("alpine"); } finally { in.close(); } @@ -207,7 +213,7 @@ public static String getArchName() { if (osArch.startsWith("arm")) { osArch = resolveArmArchType(); } else { - String lc = osArch.toLowerCase(Locale.US); + String lc = osArch.toLowerCase(Locale.ROOT); if (archMapping.containsKey(lc)) return archMapping.get(lc); } return translateArchNameToFolderName(osArch); From b4aa44880ce00d5c8508715b21d284e7cb98eada Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Fri, 29 Sep 2023 11:10:24 +0200 Subject: [PATCH 05/12] Send both SCO and DEC command for save/restore cursor position (fixes #226) (#262) --- src/main/java/org/fusesource/jansi/Ansi.java | 32 ++++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/fusesource/jansi/Ansi.java b/src/main/java/org/fusesource/jansi/Ansi.java index c1054742..803604cb 100644 --- a/src/main/java/org/fusesource/jansi/Ansi.java +++ b/src/main/java/org/fusesource/jansi/Ansi.java @@ -716,19 +716,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); } From 3c6f950fb523332f1abb6fdf4b2605aaed440e7e Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Fri, 29 Sep 2023 14:01:28 +0200 Subject: [PATCH 06/12] Fix rebuilding the project --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index 2b8a8f61..49bf9c81 100644 --- a/pom.xml +++ b/pom.xml @@ -266,6 +266,7 @@ org.fusesource.jansi.io; + true From bc4f3e9b34c0cd411f46ba0b8211090c23303a59 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 30 Sep 2023 23:37:03 +0800 Subject: [PATCH 07/12] init --- pom.xml | 6 +++ .../nativeimage/AnsiConsoleSupportImpl.java | 20 +++++++++ .../jansi/internal/nativeimage/Kernel32.java | 42 +++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 src/main/java/org/fusesource/jansi/internal/nativeimage/AnsiConsoleSupportImpl.java create mode 100644 src/main/java/org/fusesource/jansi/internal/nativeimage/Kernel32.java diff --git a/pom.xml b/pom.xml index 2b8a8f61..21ed8e47 100644 --- a/pom.xml +++ b/pom.xml @@ -109,6 +109,12 @@ + + org.graalvm.sdk + nativeimage + 23.1.0 + true + org.junit.jupiter junit-jupiter diff --git a/src/main/java/org/fusesource/jansi/internal/nativeimage/AnsiConsoleSupportImpl.java b/src/main/java/org/fusesource/jansi/internal/nativeimage/AnsiConsoleSupportImpl.java new file mode 100644 index 00000000..2130db30 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/nativeimage/AnsiConsoleSupportImpl.java @@ -0,0 +1,20 @@ +package org.fusesource.jansi.internal.nativeimage; + +import org.fusesource.jansi.AnsiConsoleSupport; + +public class AnsiConsoleSupportImpl implements AnsiConsoleSupport { + @Override + public String getProviderName() { + return "native-image"; + } + + @Override + public CLibrary getCLibrary() { + return null; // TODO + } + + @Override + public Kernel32 getKernel32() { + return null; // TODO + } +} diff --git a/src/main/java/org/fusesource/jansi/internal/nativeimage/Kernel32.java b/src/main/java/org/fusesource/jansi/internal/nativeimage/Kernel32.java new file mode 100644 index 00000000..e809fbb1 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/nativeimage/Kernel32.java @@ -0,0 +1,42 @@ +package org.fusesource.jansi.internal.nativeimage; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; +import org.graalvm.nativeimage.c.function.CFunction; +import org.graalvm.nativeimage.c.function.CLibrary; +import org.graalvm.nativeimage.c.type.CCharPointer; +import org.graalvm.nativeimage.c.type.CIntPointer; +import org.graalvm.nativeimage.c.type.VoidPointer; +import org.graalvm.nativeimage.c.type.WordPointer; + +@Platforms(value = Platform.WINDOWS.class) +@CLibrary("Kernel32") +final class Kernel32 { + @CFunction(transition = CFunction.Transition.NO_TRANSITION) + static native VoidPointer GetStdHandle(int nStdHandle); + + @CFunction(transition = CFunction.Transition.NO_TRANSITION) + static native int FormatMessageW( + int dwFlags, + VoidPointer lpSource, + int dwMessageId, + int dwLanguageId, + WordPointer lpBuffer, + int nSize, + VoidPointer Arguments); + + @CFunction(transition = CFunction.Transition.NO_TRANSITION) + static native int SetConsoleTextAttribute(VoidPointer hConsoleOutput, short wAttributes); + + @CFunction(transition = CFunction.Transition.NO_TRANSITION) + static native int SetConsoleMode(VoidPointer hConsoleHandle, int dwMode); + + @CFunction(transition = CFunction.Transition.NO_TRANSITION) + static native int GetConsoleMode(VoidPointer hConsoleHandle, CIntPointer lpMode); + + @CFunction(transition = CFunction.Transition.NO_TRANSITION) + static native int SetConsoleTitleW(CCharPointer lpConsoleTitle); + + static void f() { + } +} From 764a42207f12cb44009d4dd4896834977ac21c9d Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 2 Oct 2023 22:43:19 +0800 Subject: [PATCH 08/12] Modernize code (#267) --- src/main/java/org/fusesource/jansi/Ansi.java | 9 ++---- .../org/fusesource/jansi/AnsiConsole.java | 21 +++++--------- .../java/org/fusesource/jansi/AnsiMain.java | 4 ++- .../jansi/internal/JansiLoader.java | 29 +++---------------- .../org/fusesource/jansi/internal/OSInfo.java | 20 ++++++------- .../fusesource/jansi/io/AnsiOutputStream.java | 8 +++-- .../jansi/io/FastBufferedOutputStream.java | 2 +- .../java/org/fusesource/jansi/AnsiTest.java | 22 ++++---------- .../org/fusesource/jansi/EncodingTest.java | 10 +++---- .../jansi/io/AnsiOutputStreamTest.java | 4 +-- 10 files changed, 45 insertions(+), 84 deletions(-) diff --git a/src/main/java/org/fusesource/jansi/Ansi.java b/src/main/java/org/fusesource/jansi/Ansi.java index 803604cb..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)); diff --git a/src/main/java/org/fusesource/jansi/AnsiConsole.java b/src/main/java/org/fusesource/jansi/AnsiConsole.java index d8de9dc9..dbe9d268 100644 --- a/src/main/java/org/fusesource/jansi/AnsiConsole.java +++ b/src/main/java/org/fusesource/jansi/AnsiConsole.java @@ -276,19 +276,13 @@ private static AnsiPrintStream ansiStream(boolean stdout) { getKernel32().setConsoleMode(console, mode[0]); processor = null; type = AnsiType.VirtualTerminal; - installer = new AnsiOutputStream.IoRunnable() { - @Override - public void run() throws IOException { - virtualProcessing++; - getKernel32().setConsoleMode(console, mode[0] | ENABLE_VIRTUAL_TERMINAL_PROCESSING); - } + installer = () -> { + virtualProcessing++; + getKernel32().setConsoleMode(console, mode[0] | ENABLE_VIRTUAL_TERMINAL_PROCESSING); }; - uninstaller = new AnsiOutputStream.IoRunnable() { - @Override - public void run() throws IOException { - if (--virtualProcessing == 0) { - getKernel32().setConsoleMode(console, mode[0]); - } + uninstaller = () -> { + if (--virtualProcessing == 0) { + getKernel32().setConsoleMode(console, mode[0]); } }; } else if ((IS_CONEMU || IS_CYGWIN || IS_MSYSTEM) && !isConsole) { @@ -427,8 +421,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 24fe6f2f..a3518ad1 100644 --- a/src/main/java/org/fusesource/jansi/AnsiMain.java +++ b/src/main/java/org/fusesource/jansi/AnsiMain.java @@ -23,11 +23,13 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintStream; +import java.nio.charset.StandardCharsets; import java.util.Properties; import org.fusesource.jansi.Ansi.Attribute; import org.fusesource.jansi.internal.JansiLoader; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.fusesource.jansi.Ansi.ansi; /** @@ -294,7 +296,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/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/OSInfo.java b/src/main/java/org/fusesource/jansi/internal/OSInfo.java index 6957f8c7..14b7b0ec 100644 --- a/src/main/java/org/fusesource/jansi/internal/OSInfo.java +++ b/src/main/java/org/fusesource/jansi/internal/OSInfo.java @@ -34,6 +34,8 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.HashMap; import java.util.Locale; @@ -132,19 +134,15 @@ public static boolean isAndroid() { 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(Locale.ROOT).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; } static String getHardwareName() { 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/test/java/org/fusesource/jansi/AnsiTest.java b/src/test/java/org/fusesource/jansi/AnsiTest.java index 2e4a6453..824c8d0d 100644 --- a/src/test/java/org/fusesource/jansi/AnsiTest.java +++ b/src/test/java/org/fusesource/jansi/AnsiTest.java @@ -21,6 +21,8 @@ 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 +32,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 @@ -59,11 +51,7 @@ public void testApply() { assertEquals( "test", Ansi.ansi() - .apply(new Ansi.Consumer() { - public void apply(Ansi ansi) { - ansi.a("test"); - } - }) + .apply(ansi -> ansi.a("test")) .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); From ffd16888db1f9c5406648e62e0d5f57a03e3eb1d Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 2 Oct 2023 16:44:11 +0200 Subject: [PATCH 09/12] Fix terminal width support on MINGW (fixes #233) (#264) --- .../org/fusesource/jansi/AnsiConsole.java | 11 +- .../java/org/fusesource/jansi/AnsiMain.java | 15 +- .../jansi/internal/MingwSupport.java | 137 ++++++++++++++++++ 3 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/fusesource/jansi/internal/MingwSupport.java diff --git a/src/main/java/org/fusesource/jansi/AnsiConsole.java b/src/main/java/org/fusesource/jansi/AnsiConsole.java index dbe9d268..749c39cb 100644 --- a/src/main/java/org/fusesource/jansi/AnsiConsole.java +++ b/src/main/java/org/fusesource/jansi/AnsiConsole.java @@ -25,6 +25,7 @@ import java.nio.charset.Charset; import java.nio.charset.UnsupportedCharsetException; +import org.fusesource.jansi.internal.MingwSupport; import org.fusesource.jansi.internal.OSInfo; import org.fusesource.jansi.io.AnsiOutputStream; import org.fusesource.jansi.io.AnsiProcessor; @@ -285,11 +286,19 @@ private static AnsiPrintStream ansiStream(boolean stdout) { 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,8 +317,8 @@ private static AnsiPrintStream ansiStream(boolean stdout) { processor = proc; type = ttype; installer = uninstaller = null; + width = () -> getKernel32().getTerminalWidth(console); } - width = () -> getKernel32().getTerminalWidth(console); } // We must be on some Unix variant... diff --git a/src/main/java/org/fusesource/jansi/AnsiMain.java b/src/main/java/org/fusesource/jansi/AnsiMain.java index a3518ad1..bb6ab3cc 100644 --- a/src/main/java/org/fusesource/jansi/AnsiMain.java +++ b/src/main/java/org/fusesource/jansi/AnsiMain.java @@ -28,6 +28,7 @@ import org.fusesource.jansi.Ansi.Attribute; 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; @@ -205,7 +206,19 @@ private static void diagnoseTty(boolean stderr) { if (AnsiConsole.IS_WINDOWS) { long console = AnsiConsoleSupport.getInstance().getKernel32().getStdHandle(!stderr); isatty = AnsiConsoleSupport.getInstance().getKernel32().isTty(console); - width = AnsiConsoleSupport.getInstance().getKernel32().getTerminalWidth(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 = AnsiConsoleSupport.getInstance().getKernel32().getTerminalWidth(console); + } } else { int fd = stderr ? AnsiConsoleSupport.CLibrary.STDERR_FILENO : AnsiConsoleSupport.CLibrary.STDOUT_FILENO; isatty = AnsiConsoleSupport.getInstance().getCLibrary().isTty(fd); 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; + } +} From a8961cb811a7b641d0ea2b973a2cf8fc00af0b45 Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 2 Oct 2023 23:03:59 +0800 Subject: [PATCH 10/12] Refactoring AnsiConsoleSupport (#266) --- .../org/fusesource/jansi/AnsiConsole.java | 24 ++-- .../jansi/AnsiConsoleSupportHolder.java | 60 --------- .../java/org/fusesource/jansi/AnsiMain.java | 18 +-- .../org/fusesource/jansi/WindowsSupport.java | 10 +- .../{ => internal}/AnsiConsoleSupport.java | 6 +- .../internal/AnsiConsoleSupportHolder.java | 127 ++++++++++++++++++ .../ffm/AnsiConsoleSupportImpl.java} | 19 ++- .../jansi/{ => internal}/ffm/Kernel32.java | 5 +- .../{ => internal}/ffm/PosixCLibrary.java | 4 +- .../ffm/WindowsAnsiProcessor.java | 4 +- .../{ => internal}/ffm/WindowsCLibrary.java | 4 +- .../AnsiConsoleSupportImpl.java} | 6 +- .../java/org/fusesource/jansi/AnsiTest.java | 6 +- 13 files changed, 183 insertions(+), 110 deletions(-) delete mode 100644 src/main/java/org/fusesource/jansi/AnsiConsoleSupportHolder.java rename src/main/java/org/fusesource/jansi/{ => internal}/AnsiConsoleSupport.java (91%) create mode 100644 src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportHolder.java rename src/main/java/org/fusesource/jansi/{ffm/AnsiConsoleSupportFfm.java => internal/ffm/AnsiConsoleSupportImpl.java} (82%) rename src/main/java/org/fusesource/jansi/{ => internal}/ffm/Kernel32.java (99%) rename src/main/java/org/fusesource/jansi/{ => internal}/ffm/PosixCLibrary.java (97%) rename src/main/java/org/fusesource/jansi/{ => internal}/ffm/WindowsAnsiProcessor.java (99%) rename src/main/java/org/fusesource/jansi/{ => internal}/ffm/WindowsCLibrary.java (97%) rename src/main/java/org/fusesource/jansi/internal/{AnsiConsoleSupportJni.java => jni/AnsiConsoleSupportImpl.java} (95%) diff --git a/src/main/java/org/fusesource/jansi/AnsiConsole.java b/src/main/java/org/fusesource/jansi/AnsiConsole.java index 749c39cb..0e1d5df3 100644 --- a/src/main/java/org/fusesource/jansi/AnsiConsole.java +++ b/src/main/java/org/fusesource/jansi/AnsiConsole.java @@ -31,6 +31,9 @@ import org.fusesource.jansi.io.AnsiProcessor; import org.fusesource.jansi.io.FastBufferedOutputStream; +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 * if not on a terminal (see @@ -155,10 +158,21 @@ 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"; - public static final String JANSI_PROVIDERS_DEFAULT = JANSI_PROVIDER_FFM + "," + JANSI_PROVIDER_JNI; /** * @deprecated this field will be made private in a future release, use {@link #sysOut()} instead @@ -536,12 +550,4 @@ static synchronized void initStreams() { initialized = true; } } - - private static AnsiConsoleSupport.Kernel32 getKernel32() { - return AnsiConsoleSupport.getInstance().getKernel32(); - } - - private static AnsiConsoleSupport.CLibrary getCLibrary() { - return AnsiConsoleSupport.getInstance().getCLibrary(); - } } diff --git a/src/main/java/org/fusesource/jansi/AnsiConsoleSupportHolder.java b/src/main/java/org/fusesource/jansi/AnsiConsoleSupportHolder.java deleted file mode 100644 index 082f78d0..00000000 --- a/src/main/java/org/fusesource/jansi/AnsiConsoleSupportHolder.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) 2009-2023 the original author(s). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.fusesource.jansi; - -import org.fusesource.jansi.internal.AnsiConsoleSupportJni; - -import static org.fusesource.jansi.AnsiConsole.JANSI_PROVIDERS; -import static org.fusesource.jansi.AnsiConsole.JANSI_PROVIDERS_DEFAULT; -import static org.fusesource.jansi.AnsiConsole.JANSI_PROVIDER_FFM; -import static org.fusesource.jansi.AnsiConsole.JANSI_PROVIDER_JNI; - -class AnsiConsoleSupportHolder { - static volatile AnsiConsoleSupport instance; - - static AnsiConsoleSupport get() { - if (instance == null) { - synchronized (AnsiConsoleSupportHolder.class) { - if (instance == null) { - instance = doGet(); - } - } - } - return instance; - } - - static AnsiConsoleSupport doGet() { - RuntimeException error = new RuntimeException("Unable to create AnsiConsoleSupport provider"); - String[] providers = - System.getProperty(JANSI_PROVIDERS, JANSI_PROVIDERS_DEFAULT).split(","); - for (String provider : providers) { - try { - if (JANSI_PROVIDER_FFM.equals(provider)) { - return (AnsiConsoleSupport) AnsiConsoleSupport.class - .getClassLoader() - .loadClass("org.fusesource.jansi.ffm.AnsiConsoleSupportFfm") - .getConstructor() - .newInstance(); - } else if (JANSI_PROVIDER_JNI.equals(provider)) { - return new AnsiConsoleSupportJni(); - } - } catch (Throwable t) { - error.addSuppressed(t); - } - } - throw error; - } -} diff --git a/src/main/java/org/fusesource/jansi/AnsiMain.java b/src/main/java/org/fusesource/jansi/AnsiMain.java index bb6ab3cc..e824322d 100644 --- a/src/main/java/org/fusesource/jansi/AnsiMain.java +++ b/src/main/java/org/fusesource/jansi/AnsiMain.java @@ -23,10 +23,11 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintStream; -import java.nio.charset.StandardCharsets; import java.util.Properties; import org.fusesource.jansi.Ansi.Attribute; +import org.fusesource.jansi.internal.AnsiConsoleSupport; +import org.fusesource.jansi.internal.AnsiConsoleSupportHolder; import org.fusesource.jansi.internal.JansiLoader; import org.fusesource.jansi.internal.MingwSupport; @@ -56,9 +57,8 @@ public static void main(String... args) throws IOException { System.out.println(); - System.out.println("jansi.providers= " - + System.getProperty(AnsiConsole.JANSI_PROVIDERS, AnsiConsole.JANSI_PROVIDERS_DEFAULT)); - String provider = AnsiConsoleSupport.getInstance().getProviderName(); + 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)) { @@ -204,8 +204,8 @@ private static void diagnoseTty(boolean stderr) { int isatty; int width; if (AnsiConsole.IS_WINDOWS) { - long console = AnsiConsoleSupport.getInstance().getKernel32().getStdHandle(!stderr); - isatty = AnsiConsoleSupport.getInstance().getKernel32().isTty(console); + 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); @@ -217,12 +217,12 @@ private static void diagnoseTty(boolean stderr) { width = 0; } } else { - width = AnsiConsoleSupport.getInstance().getKernel32().getTerminalWidth(console); + width = AnsiConsoleSupportHolder.getKernel32().getTerminalWidth(console); } } else { int fd = stderr ? AnsiConsoleSupport.CLibrary.STDERR_FILENO : AnsiConsoleSupport.CLibrary.STDOUT_FILENO; - isatty = AnsiConsoleSupport.getInstance().getCLibrary().isTty(fd); - width = AnsiConsoleSupport.getInstance().getCLibrary().getTerminalWidth(fd); + isatty = AnsiConsoleSupportHolder.getCLibrary().isTty(fd); + width = AnsiConsoleSupportHolder.getCLibrary().getTerminalWidth(fd); } System.out.println("isatty(STD" + (stderr ? "ERR" : "OUT") + "_FILENO): " + isatty + ", System." diff --git a/src/main/java/org/fusesource/jansi/WindowsSupport.java b/src/main/java/org/fusesource/jansi/WindowsSupport.java index e14854cd..cfc0f9bc 100644 --- a/src/main/java/org/fusesource/jansi/WindowsSupport.java +++ b/src/main/java/org/fusesource/jansi/WindowsSupport.java @@ -15,18 +15,16 @@ */ package org.fusesource.jansi; +import org.fusesource.jansi.internal.AnsiConsoleSupportHolder; + public class WindowsSupport { public static String getLastErrorMessage() { - int errorCode = getKernel32().getLastError(); + int errorCode = AnsiConsoleSupportHolder.getKernel32().getLastError(); return getErrorMessage(errorCode); } public static String getErrorMessage(int errorCode) { - return getKernel32().getErrorMessage(errorCode); - } - - private static AnsiConsoleSupport.Kernel32 getKernel32() { - return AnsiConsoleSupport.getInstance().getKernel32(); + return AnsiConsoleSupportHolder.getKernel32().getErrorMessage(errorCode); } } diff --git a/src/main/java/org/fusesource/jansi/AnsiConsoleSupport.java b/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupport.java similarity index 91% rename from src/main/java/org/fusesource/jansi/AnsiConsoleSupport.java rename to src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupport.java index 02907c55..4868207d 100644 --- a/src/main/java/org/fusesource/jansi/AnsiConsoleSupport.java +++ b/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupport.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.fusesource.jansi; +package org.fusesource.jansi.internal; import java.io.IOException; import java.io.OutputStream; @@ -56,8 +56,4 @@ interface Kernel32 { CLibrary getCLibrary(); Kernel32 getKernel32(); - - static AnsiConsoleSupport getInstance() { - return AnsiConsoleSupportHolder.get(); - } } 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..eb049f98 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportHolder.java @@ -0,0 +1,127 @@ +/* + * 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 { + + private static final String PROVIDER_NAME; + private static final AnsiConsoleSupport.CLibrary CLIBRARY; + private static final AnsiConsoleSupport.Kernel32 KERNEL32; + private static final Throwable ERR; + + private static AnsiConsoleSupport getDefaultProvider() { + 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; + } + + String providerName = null; + AnsiConsoleSupport.CLibrary clib = null; + AnsiConsoleSupport.Kernel32 kernel32 = null; + + if (ansiConsoleSupport != null) { + try { + providerName = ansiConsoleSupport.getProviderName(); + clib = ansiConsoleSupport.getCLibrary(); + kernel32 = OSInfo.isWindows() ? ansiConsoleSupport.getKernel32() : null; + } catch (Throwable e) { + err = e; + } + } + + PROVIDER_NAME = providerName; + CLIBRARY = clib; + KERNEL32 = kernel32; + ERR = err; + } + + public static String getProviderName() { + return PROVIDER_NAME; + } + + public static AnsiConsoleSupport.CLibrary getCLibrary() { + if (CLIBRARY == null) { + throw new RuntimeException("Unable to get the instance of CLibrary", ERR); + } + + return CLIBRARY; + } + + public static AnsiConsoleSupport.Kernel32 getKernel32() { + if (KERNEL32 == null) { + if (OSInfo.isWindows()) { + throw new RuntimeException("Unable to get the instance of Kernel32", ERR); + } else { + throw new UnsupportedOperationException("Not Windows"); + } + } + + return KERNEL32; + } +} diff --git a/src/main/java/org/fusesource/jansi/ffm/AnsiConsoleSupportFfm.java b/src/main/java/org/fusesource/jansi/internal/ffm/AnsiConsoleSupportImpl.java similarity index 82% rename from src/main/java/org/fusesource/jansi/ffm/AnsiConsoleSupportFfm.java rename to src/main/java/org/fusesource/jansi/internal/ffm/AnsiConsoleSupportImpl.java index 2033fad8..bc252b41 100644 --- a/src/main/java/org/fusesource/jansi/ffm/AnsiConsoleSupportFfm.java +++ b/src/main/java/org/fusesource/jansi/internal/ffm/AnsiConsoleSupportImpl.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.fusesource.jansi.ffm; +package org.fusesource.jansi.internal.ffm; import java.io.IOException; import java.io.OutputStream; @@ -21,13 +21,22 @@ import java.lang.foreign.MemorySegment; import java.lang.foreign.ValueLayout; -import org.fusesource.jansi.AnsiConsoleSupport; +import org.fusesource.jansi.internal.AnsiConsoleSupport; import org.fusesource.jansi.internal.OSInfo; import org.fusesource.jansi.io.AnsiProcessor; -import static org.fusesource.jansi.ffm.Kernel32.*; +import static org.fusesource.jansi.internal.ffm.Kernel32.*; + +public final class AnsiConsoleSupportImpl implements AnsiConsoleSupport { + + public AnsiConsoleSupportImpl() {} + + public AnsiConsoleSupportImpl(boolean checkNativeAccess) { + if (checkNativeAccess && !AnsiConsoleSupportImpl.class.getModule().isNativeAccessEnabled()) { + throw new UnsupportedOperationException("Native access is not enabled for the current module"); + } + } -public class AnsiConsoleSupportFfm implements AnsiConsoleSupport { @Override public String getProviderName() { return "ffm"; @@ -88,7 +97,7 @@ public int getLastError() { @Override public String getErrorMessage(int errorCode) { - return org.fusesource.jansi.ffm.Kernel32.getErrorMessage(errorCode); + return org.fusesource.jansi.internal.ffm.Kernel32.getErrorMessage(errorCode); } @Override diff --git a/src/main/java/org/fusesource/jansi/ffm/Kernel32.java b/src/main/java/org/fusesource/jansi/internal/ffm/Kernel32.java similarity index 99% rename from src/main/java/org/fusesource/jansi/ffm/Kernel32.java rename to src/main/java/org/fusesource/jansi/internal/ffm/Kernel32.java index 0cc409ac..a657e096 100644 --- a/src/main/java/org/fusesource/jansi/ffm/Kernel32.java +++ b/src/main/java/org/fusesource/jansi/internal/ffm/Kernel32.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.fusesource.jansi.ffm; +package org.fusesource.jansi.internal.ffm; import java.io.IOException; import java.lang.foreign.AddressLayout; @@ -309,7 +309,8 @@ public static String getErrorMessage(int errorCode) { int bufferSize = 160; try (Arena arena = Arena.ofConfined()) { MemorySegment data = arena.allocate(bufferSize); - FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, null, errorCode, 0, data, bufferSize, null); + FormatMessageW( + FORMAT_MESSAGE_FROM_SYSTEM, MemorySegment.NULL, errorCode, 0, data, bufferSize, MemorySegment.NULL); return new String(data.toArray(JAVA_BYTE), StandardCharsets.UTF_16LE).trim(); } } diff --git a/src/main/java/org/fusesource/jansi/ffm/PosixCLibrary.java b/src/main/java/org/fusesource/jansi/internal/ffm/PosixCLibrary.java similarity index 97% rename from src/main/java/org/fusesource/jansi/ffm/PosixCLibrary.java rename to src/main/java/org/fusesource/jansi/internal/ffm/PosixCLibrary.java index bd4f1f73..30e959b9 100644 --- a/src/main/java/org/fusesource/jansi/ffm/PosixCLibrary.java +++ b/src/main/java/org/fusesource/jansi/internal/ffm/PosixCLibrary.java @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.fusesource.jansi.ffm; +package org.fusesource.jansi.internal.ffm; import java.lang.foreign.*; import java.lang.invoke.MethodHandle; import java.lang.invoke.VarHandle; -import org.fusesource.jansi.AnsiConsoleSupport; +import org.fusesource.jansi.internal.AnsiConsoleSupport; final class PosixCLibrary implements AnsiConsoleSupport.CLibrary { private static final int TIOCGWINSZ; diff --git a/src/main/java/org/fusesource/jansi/ffm/WindowsAnsiProcessor.java b/src/main/java/org/fusesource/jansi/internal/ffm/WindowsAnsiProcessor.java similarity index 99% rename from src/main/java/org/fusesource/jansi/ffm/WindowsAnsiProcessor.java rename to src/main/java/org/fusesource/jansi/internal/ffm/WindowsAnsiProcessor.java index e933ff0a..cc6789e9 100644 --- a/src/main/java/org/fusesource/jansi/ffm/WindowsAnsiProcessor.java +++ b/src/main/java/org/fusesource/jansi/internal/ffm/WindowsAnsiProcessor.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.fusesource.jansi.ffm; +package org.fusesource.jansi.internal.ffm; import java.io.IOException; import java.io.OutputStream; @@ -26,7 +26,7 @@ import org.fusesource.jansi.io.AnsiProcessor; import org.fusesource.jansi.io.Colors; -import static org.fusesource.jansi.ffm.Kernel32.*; +import static org.fusesource.jansi.internal.ffm.Kernel32.*; /** * A Windows ANSI escape processor, that uses JNA to access native platform diff --git a/src/main/java/org/fusesource/jansi/ffm/WindowsCLibrary.java b/src/main/java/org/fusesource/jansi/internal/ffm/WindowsCLibrary.java similarity index 97% rename from src/main/java/org/fusesource/jansi/ffm/WindowsCLibrary.java rename to src/main/java/org/fusesource/jansi/internal/ffm/WindowsCLibrary.java index c68854bf..2acfedb3 100644 --- a/src/main/java/org/fusesource/jansi/ffm/WindowsCLibrary.java +++ b/src/main/java/org/fusesource/jansi/internal/ffm/WindowsCLibrary.java @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.fusesource.jansi.ffm; +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.AnsiConsoleSupport; +import org.fusesource.jansi.internal.AnsiConsoleSupport; import static java.lang.foreign.ValueLayout.*; diff --git a/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportJni.java b/src/main/java/org/fusesource/jansi/internal/jni/AnsiConsoleSupportImpl.java similarity index 95% rename from src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportJni.java rename to src/main/java/org/fusesource/jansi/internal/jni/AnsiConsoleSupportImpl.java index 83f1a31e..ea3bc2fc 100644 --- a/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportJni.java +++ b/src/main/java/org/fusesource/jansi/internal/jni/AnsiConsoleSupportImpl.java @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.fusesource.jansi.internal; +package org.fusesource.jansi.internal.jni; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; -import org.fusesource.jansi.AnsiConsoleSupport; +import org.fusesource.jansi.internal.AnsiConsoleSupport; import org.fusesource.jansi.io.AnsiProcessor; import org.fusesource.jansi.io.WindowsAnsiProcessor; @@ -33,7 +33,7 @@ import static org.fusesource.jansi.internal.Kernel32.STD_OUTPUT_HANDLE; import static org.fusesource.jansi.internal.Kernel32.SetConsoleMode; -public class AnsiConsoleSupportJni implements AnsiConsoleSupport { +public final class AnsiConsoleSupportImpl implements AnsiConsoleSupport { @Override public String getProviderName() { diff --git a/src/test/java/org/fusesource/jansi/AnsiTest.java b/src/test/java/org/fusesource/jansi/AnsiTest.java index 824c8d0d..332fb4b5 100644 --- a/src/test/java/org/fusesource/jansi/AnsiTest.java +++ b/src/test/java/org/fusesource/jansi/AnsiTest.java @@ -48,11 +48,7 @@ public void testClone() throws CloneNotSupportedException { @Test public void testApply() { - assertEquals( - "test", - Ansi.ansi() - .apply(ansi -> ansi.a("test")) - .toString()); + assertEquals("test", Ansi.ansi().apply(ansi -> ansi.a("test")).toString()); } @ParameterizedTest From 74b8320472116258974eb400f3a24ab2c9faf263 Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 2 Oct 2023 23:09:16 +0800 Subject: [PATCH 11/12] fix build --- .../nativeimage/AnsiConsoleSupportImpl.java | 17 ++++++++++++++++- .../jansi/internal/nativeimage/Kernel32.java | 18 ++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/fusesource/jansi/internal/nativeimage/AnsiConsoleSupportImpl.java b/src/main/java/org/fusesource/jansi/internal/nativeimage/AnsiConsoleSupportImpl.java index 2130db30..9be7a0e0 100644 --- a/src/main/java/org/fusesource/jansi/internal/nativeimage/AnsiConsoleSupportImpl.java +++ b/src/main/java/org/fusesource/jansi/internal/nativeimage/AnsiConsoleSupportImpl.java @@ -1,6 +1,21 @@ +/* + * 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.nativeimage; -import org.fusesource.jansi.AnsiConsoleSupport; +import org.fusesource.jansi.internal.AnsiConsoleSupport; public class AnsiConsoleSupportImpl implements AnsiConsoleSupport { @Override diff --git a/src/main/java/org/fusesource/jansi/internal/nativeimage/Kernel32.java b/src/main/java/org/fusesource/jansi/internal/nativeimage/Kernel32.java index e809fbb1..bdbe5a11 100644 --- a/src/main/java/org/fusesource/jansi/internal/nativeimage/Kernel32.java +++ b/src/main/java/org/fusesource/jansi/internal/nativeimage/Kernel32.java @@ -1,3 +1,18 @@ +/* + * 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.nativeimage; import org.graalvm.nativeimage.Platform; @@ -37,6 +52,5 @@ static native int FormatMessageW( @CFunction(transition = CFunction.Transition.NO_TRANSITION) static native int SetConsoleTitleW(CCharPointer lpConsoleTitle); - static void f() { - } + static void f() {} } From 70b77b446e2d197a54af404aee4d8fb5edd3e6be Mon Sep 17 00:00:00 2001 From: Glavo Date: Tue, 3 Oct 2023 01:22:35 +0800 Subject: [PATCH 12/12] update --- .../internal/AnsiConsoleSupportHolder.java | 15 +- .../nativeimage/AnsiConsoleSupportImpl.java | 69 +++++- .../jansi/internal/nativeimage/Kernel32.java | 219 +++++++++++++++++- .../internal/nativeimage/WindowsCLibrary.java | 45 ++++ .../nativeimage/WindowsDirectives.java | 48 ++++ 5 files changed, 382 insertions(+), 14 deletions(-) create mode 100644 src/main/java/org/fusesource/jansi/internal/nativeimage/WindowsCLibrary.java create mode 100644 src/main/java/org/fusesource/jansi/internal/nativeimage/WindowsDirectives.java diff --git a/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportHolder.java b/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportHolder.java index eb049f98..b9c19a4d 100644 --- a/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportHolder.java +++ b/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportHolder.java @@ -24,6 +24,17 @@ public final class AnsiConsoleSupportHolder { private static final AnsiConsoleSupport.Kernel32 KERNEL32; private static final Throwable ERR; + private static AnsiConsoleSupport getNativeImageProvider() { + try { + return (AnsiConsoleSupport) + Class.forName("org.fusesource.jansi.internal.nativeimage.AnsiConsoleSupportImpl") + .getConstructor() + .newInstance(); + } catch (Throwable e) { + throw new AssertionError("should not reach here", e); + } + } + private static AnsiConsoleSupport getDefaultProvider() { try { // Call the specialized constructor to check whether the module has native access enabled @@ -72,7 +83,9 @@ private static AnsiConsoleSupport findProvider(String providerList) { Throwable err = null; try { - if (providerList == null) { + if ("nativeimage".equals(providerList) && System.getProperty("org.graalvm.nativeimage.imagecode") != null) { + ansiConsoleSupport = getNativeImageProvider(); + } else if (providerList == null) { ansiConsoleSupport = getDefaultProvider(); } else { ansiConsoleSupport = findProvider(providerList); diff --git a/src/main/java/org/fusesource/jansi/internal/nativeimage/AnsiConsoleSupportImpl.java b/src/main/java/org/fusesource/jansi/internal/nativeimage/AnsiConsoleSupportImpl.java index 9be7a0e0..ff9140fc 100644 --- a/src/main/java/org/fusesource/jansi/internal/nativeimage/AnsiConsoleSupportImpl.java +++ b/src/main/java/org/fusesource/jansi/internal/nativeimage/AnsiConsoleSupportImpl.java @@ -15,21 +15,84 @@ */ package org.fusesource.jansi.internal.nativeimage; +import java.io.IOException; +import java.io.OutputStream; + import org.fusesource.jansi.internal.AnsiConsoleSupport; +import org.fusesource.jansi.internal.OSInfo; +import org.fusesource.jansi.io.AnsiProcessor; +import org.graalvm.nativeimage.StackValue; +import org.graalvm.nativeimage.c.type.CIntPointer; +import org.graalvm.word.WordFactory; + +import static org.fusesource.jansi.internal.nativeimage.Kernel32.*; public class AnsiConsoleSupportImpl implements AnsiConsoleSupport { @Override public String getProviderName() { - return "native-image"; + return "nativeimage"; } @Override public CLibrary getCLibrary() { - return null; // TODO + if (OSInfo.isWindows()) { + return new WindowsCLibrary(); + } else { + throw new UnsupportedOperationException(); // TODO + } } @Override public Kernel32 getKernel32() { - return null; // TODO + return new Kernel32() { + @Override + public int isTty(long console) { + int[] mode = new int[1]; + return getConsoleMode(console, mode); + } + + @Override + public int getTerminalWidth(long console) { + ConsoleScreenBufferInfo info = StackValue.get(ConsoleScreenBufferInfo.class); + GetConsoleScreenBufferInfo(WordFactory.pointer(console), (ConsoleScreenBufferInfoPointer) info); + + SmallRect window = info.getSrWindow(); + return Short.toUnsignedInt(window.getRight()) - Short.toUnsignedInt(window.getLeft()) + 1; + } + + @Override + public long getStdHandle(boolean stdout) { + return GetStdHandle(stdout ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE) + .rawValue(); + } + + @Override + public int getConsoleMode(long console, int[] mode) { + CIntPointer written = StackValue.get(Integer.BYTES); + int res = GetConsoleMode(WordFactory.pointer(console), written); + mode[0] = written.read(); + return res; + } + + @Override + public int setConsoleMode(long console, int mode) { + return SetConsoleMode(WordFactory.pointer(console), mode); + } + + @Override + public int getLastError() { + return GetLastError(); + } + + @Override + public String getErrorMessage(int errorCode) { + return org.fusesource.jansi.internal.nativeimage.Kernel32.getErrorMessage(errorCode); + } + + @Override + public AnsiProcessor newProcessor(OutputStream os, long console) throws IOException { + throw new UnsupportedOperationException(); // TODO + } + }; } } diff --git a/src/main/java/org/fusesource/jansi/internal/nativeimage/Kernel32.java b/src/main/java/org/fusesource/jansi/internal/nativeimage/Kernel32.java index bdbe5a11..b2eea023 100644 --- a/src/main/java/org/fusesource/jansi/internal/nativeimage/Kernel32.java +++ b/src/main/java/org/fusesource/jansi/internal/nativeimage/Kernel32.java @@ -15,23 +15,149 @@ */ package org.fusesource.jansi.internal.nativeimage; +import java.nio.charset.StandardCharsets; + import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; +import org.graalvm.nativeimage.StackValue; +import org.graalvm.nativeimage.c.CContext; import org.graalvm.nativeimage.c.function.CFunction; -import org.graalvm.nativeimage.c.function.CLibrary; -import org.graalvm.nativeimage.c.type.CCharPointer; +import org.graalvm.nativeimage.c.struct.CField; +import org.graalvm.nativeimage.c.struct.CPointerTo; +import org.graalvm.nativeimage.c.struct.CStruct; import org.graalvm.nativeimage.c.type.CIntPointer; +import org.graalvm.nativeimage.c.type.CTypeConversion; import org.graalvm.nativeimage.c.type.VoidPointer; import org.graalvm.nativeimage.c.type.WordPointer; +import org.graalvm.word.PointerBase; +import org.graalvm.word.WordFactory; -@Platforms(value = Platform.WINDOWS.class) -@CLibrary("Kernel32") +@Platforms(Platform.WINDOWS.class) +@CContext(WindowsDirectives.class) 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; + + @CPointerTo(nameOfCType = "wchar_t") + public interface WCharPointer extends PointerBase {} + + @CStruct("COORD") + public interface COORD extends PointerBase { + @CField("X") + short getX(); + + @CField("Y") + short getY(); + } + + @CStruct("SMALL_RECT") + public interface SmallRect extends PointerBase { + @CField("Left") + short getLeft(); + + @CField("Top") + short getTop(); + + @CField("Right") + short getRight(); + + @CField("Bottom") + short getBottom(); + } + + @CStruct("CONSOLE_SCREEN_BUFFER_INFO") + public interface ConsoleScreenBufferInfo extends PointerBase { + @CField("dwSize") + COORD getDwSize(); + + @CField("dwCursorPosition") + COORD getDwCursorPosition(); + + @CField("wAttributes") + short getWAttributes(); + + @CField("srWindow") + SmallRect getSrWindow(); + + @CField("dwMaximumWindowSize") + COORD getDwMaximumWindowSize(); + } + + @CStruct("CHAR_INFO") + public interface CharInfo extends PointerBase { + @CField("UnicodeChar") + char getUnicodeChar(); + + @CField("AsciiChar") + byte getAsciiChar(); + + @CField("Attributes") + short getAttributes(); + } + + @CPointerTo(SmallRect.class) + public interface SmallRectPointer extends PointerBase {} + + @CPointerTo(ConsoleScreenBufferInfo.class) + public interface ConsoleScreenBufferInfoPointer extends PointerBase {} + + @CPointerTo(CharInfo.class) + public interface CharInfoPointer extends PointerBase {} + @CFunction(transition = CFunction.Transition.NO_TRANSITION) - static native VoidPointer GetStdHandle(int nStdHandle); + public static native VoidPointer GetStdHandle(int nStdHandle); @CFunction(transition = CFunction.Transition.NO_TRANSITION) - static native int FormatMessageW( + public static native int FormatMessageW( int dwFlags, VoidPointer lpSource, int dwMessageId, @@ -41,16 +167,89 @@ static native int FormatMessageW( VoidPointer Arguments); @CFunction(transition = CFunction.Transition.NO_TRANSITION) - static native int SetConsoleTextAttribute(VoidPointer hConsoleOutput, short wAttributes); + public static native int SetConsoleTextAttribute(VoidPointer hConsoleOutput, short wAttributes); + + @CFunction(transition = CFunction.Transition.NO_TRANSITION) + public static native int SetConsoleMode(VoidPointer hConsoleHandle, int dwMode); + + @CFunction(transition = CFunction.Transition.NO_TRANSITION) + public static native int GetConsoleMode(VoidPointer hConsoleHandle, CIntPointer lpMode); @CFunction(transition = CFunction.Transition.NO_TRANSITION) - static native int SetConsoleMode(VoidPointer hConsoleHandle, int dwMode); + public static native int SetConsoleTitleW(WCharPointer lpConsoleTitle); @CFunction(transition = CFunction.Transition.NO_TRANSITION) - static native int GetConsoleMode(VoidPointer hConsoleHandle, CIntPointer lpMode); + public static native int SetConsoleCursorPosition(VoidPointer hConsoleOutput, COORD dwCursorPosition); @CFunction(transition = CFunction.Transition.NO_TRANSITION) - static native int SetConsoleTitleW(CCharPointer lpConsoleTitle); + public static native int FillConsoleOutputCharacterW( + VoidPointer hConsoleOutput, + char cCharacter, // TODO: ? + int nLength, + COORD dwWriteCoord, + CIntPointer lpNumberOfCharsWritten); + + @CFunction(transition = CFunction.Transition.NO_TRANSITION) + public static native int FillConsoleOutputAttribute( + VoidPointer hConsoleOutput, + short wAttribute, + int nLength, + COORD dwWriteCoord, + CIntPointer lpNumberOfAttrsWritten); + + @CFunction(transition = CFunction.Transition.NO_TRANSITION) + public static native int WriteConsoleW( + VoidPointer hConsoleOutput, + WCharPointer lpBuffer, + int nNumberOfCharsToWrite, + CIntPointer lpNumberOfCharsWritten, + VoidPointer lpReserved); + + // @CFunction(transition = CFunction.Transition.NO_TRANSITION) + // public static native int ReadConsoleInputW( + // VoidPointer hConsoleInput, VoidPointer lpBuffer, int nLength, CIntPointer lpNumberOfEventsRead); + + // @CFunction(transition = CFunction.Transition.NO_TRANSITION) + // public static native int PeekConsoleInputW( + // VoidPointer hConsoleInput, VoidPointer lpBuffer, int nLength, CIntPointer lpNumberOfEventsRead); + + @CFunction(transition = CFunction.Transition.NO_TRANSITION) + public static native int GetConsoleScreenBufferInfo( + VoidPointer hConsoleOutput, ConsoleScreenBufferInfoPointer lpConsoleScreenBufferInfo); + + @CFunction(transition = CFunction.Transition.NO_TRANSITION) + public static native int ScrollConsoleScreenBuffer( + VoidPointer hConsoleOutput, + SmallRectPointer lpScrollRectangle, + SmallRectPointer lpClipRectangle, + COORD dwDestinationOrigin, + CharInfoPointer lpFill); + + @CFunction(transition = CFunction.Transition.NO_TRANSITION) + public static native int GetLastError(); + + @CFunction(transition = CFunction.Transition.NO_TRANSITION) + public static native int GetFileType(VoidPointer hFile); + + @CFunction(transition = CFunction.Transition.NO_TRANSITION) + public static native VoidPointer _get_osfhandle(int fd); + + public static String getErrorMessage(int errorCode) { + int bufferSize = 160; + WordPointer data = StackValue.get(bufferSize); + FormatMessageW( + FORMAT_MESSAGE_FROM_SYSTEM, + WordFactory.nullPointer(), + errorCode, + 0, + data, + bufferSize, + WordFactory.nullPointer()); + + byte[] arr = new byte[bufferSize]; + CTypeConversion.asByteBuffer(data, bufferSize).get(arr); + return new String(arr, StandardCharsets.UTF_16LE).trim(); + } static void f() {} } diff --git a/src/main/java/org/fusesource/jansi/internal/nativeimage/WindowsCLibrary.java b/src/main/java/org/fusesource/jansi/internal/nativeimage/WindowsCLibrary.java new file mode 100644 index 00000000..f3e8102f --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/nativeimage/WindowsCLibrary.java @@ -0,0 +1,45 @@ +/* + * 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.nativeimage; + +import org.fusesource.jansi.internal.AnsiConsoleSupport; +import org.graalvm.nativeimage.StackValue; +import org.graalvm.nativeimage.c.type.VoidPointer; + +final class WindowsCLibrary implements AnsiConsoleSupport.CLibrary { + + private static final int FILE_TYPE_CHAR = 0x0002; + + @Override + public short getTerminalWidth(int fd) { + throw new UnsupportedOperationException("Windows does not support ioctl"); + } + + @Override + public int isTty(int fd) { + // check if fd is a pipe + VoidPointer 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, StackValue.get(Integer.BYTES)); + } + + // TODO + return 0; + } +} diff --git a/src/main/java/org/fusesource/jansi/internal/nativeimage/WindowsDirectives.java b/src/main/java/org/fusesource/jansi/internal/nativeimage/WindowsDirectives.java new file mode 100644 index 00000000..19df7a01 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/nativeimage/WindowsDirectives.java @@ -0,0 +1,48 @@ +/* + * 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.nativeimage; + +import java.util.Arrays; +import java.util.List; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; +import org.graalvm.nativeimage.c.CContext; + +@Platforms(Platform.WINDOWS.class) +public class WindowsDirectives implements CContext.Directives { + + private static final String[] HEADERS = { + "", "", "", "", "", "" + }; + + private static final String[] LIBS = {"Kernel32"}; + + @Override + public boolean isInConfiguration() { + return Platform.includedIn(Platform.WINDOWS.class); + } + + @Override + public List getHeaderFiles() { + return Arrays.asList(HEADERS); + } + + @Override + public List getLibraries() { + return Arrays.asList(LIBS); + } +}