From d0e1ce16e9c9631961e30047b2cbd8af2dd86deb Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 9 Oct 2023 09:36:16 +0200 Subject: [PATCH] FFM support --- .github/workflows/master-build.yml | 2 +- builtins/pom.xml | 22 - console/pom.xml | 22 - demo/jline-gogo.bat | 6 + demo/jline-gogo.sh | 5 + demo/pom.xml | 4 + jline/pom.xml | 21 +- .../src/main/java/org/jline/nativ/OSInfo.java | 3 +- pom.xml | 11 +- reader/pom.xml | 1 + .../org/jline/reader/impl/LineReaderTest.java | 6 +- .../terminal/impl/ExternalTerminalTest.java | 12 + terminal-ffm/pom.xml | 75 ++ .../org/jline/terminal/impl/ffm/CLibrary.java | 1075 +++++++++++++++++ .../jline/terminal/impl/ffm/FfmNativePty.java | 157 +++ .../impl/ffm/FfmTerminalProvider.java | 104 ++ .../org/jline/terminal/impl/ffm/Kernel32.java | 925 ++++++++++++++ .../impl/ffm/NativeWinConsoleWriter.java | 36 + .../impl/ffm/NativeWinSysTerminal.java | 289 +++++ .../terminal/impl/ffm/WindowsAnsiWriter.java | 408 +++++++ .../services/org/jline/terminal/provider/ffm | 16 + .../org/jline/terminal/impl/ffm/FfmTest.java | 74 ++ terminal/pom.xml | 1 + .../org/jline/terminal/TerminalBuilder.java | 25 +- .../java/org/jline/terminal/impl/Diag.java | 11 + 25 files changed, 3249 insertions(+), 62 deletions(-) create mode 100644 terminal-ffm/pom.xml create mode 100644 terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/CLibrary.java create mode 100644 terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/FfmNativePty.java create mode 100644 terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/FfmTerminalProvider.java create mode 100644 terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/Kernel32.java create mode 100644 terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/NativeWinConsoleWriter.java create mode 100644 terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/NativeWinSysTerminal.java create mode 100644 terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/WindowsAnsiWriter.java create mode 100644 terminal-ffm/src/main/resources/META-INF/services/org/jline/terminal/provider/ffm create mode 100644 terminal-ffm/src/test/java/org/jline/terminal/impl/ffm/FfmTest.java diff --git a/.github/workflows/master-build.yml b/.github/workflows/master-build.yml index bd72f85e5..a99a08553 100644 --- a/.github/workflows/master-build.yml +++ b/.github/workflows/master-build.yml @@ -31,7 +31,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest, windows-latest, macos-latest ] - java: [ '11', '21' ] + java: [ '21' ] steps: - uses: actions/checkout@v2 diff --git a/builtins/pom.xml b/builtins/pom.xml index fc9e4bfdc..33d2efd2b 100644 --- a/builtins/pom.xml +++ b/builtins/pom.xml @@ -62,34 +62,12 @@ default-compile - - **/ConsoleEngineImpl.java - **/TTop.java - -Xlint:all,-options -Werror - - compact - - compile - - - - **/ConsoleEngineImpl.java - **/TTop.java - - - -Xlint:all,-options - -Werror - -profile - compact1 - - - diff --git a/console/pom.xml b/console/pom.xml index a3a53c2d1..c0d0ff3eb 100644 --- a/console/pom.xml +++ b/console/pom.xml @@ -62,34 +62,12 @@ default-compile - - **/ConsoleEngineImpl.java - **/TTop.java - -Xlint:all,-options -Werror - - compact - - compile - - - - **/ConsoleEngineImpl.java - **/TTop.java - - - -Xlint:all,-options - -Werror - -profile - compact1 - - - diff --git a/demo/jline-gogo.bat b/demo/jline-gogo.bat index 3acd60ec7..127489835 100755 --- a/demo/jline-gogo.bat +++ b/demo/jline-gogo.bat @@ -36,6 +36,7 @@ set "logconf=%DIRNAME%etc\logging.properties" if "%1" == "debug" goto :EXECUTE_DEBUG if "%1" == "debugs" goto :EXECUTE_DEBUGS if "%1" == "verbose" goto :EXECUTE_VERBOSE + if "%1" == "ffm" goto :EXECUTE_FFM if "%1" == "" goto :EXECUTE_MAIN set "opts=%opts% %~1" shift @@ -76,6 +77,11 @@ set "logconf=%DIRNAME%etc\logging.properties" shift goto :RUN_LOOP +:EXECUTE_FFM + set "opts=%opts% --enable-preview --enable-native-access=ALL-UNNAMED" + shift + goto :RUN_LOOP + :EXECUTE_MAIN popd diff --git a/demo/jline-gogo.sh b/demo/jline-gogo.sh index 1444171d3..c6c8325b1 100755 --- a/demo/jline-gogo.sh +++ b/demo/jline-gogo.sh @@ -65,6 +65,10 @@ while [ "${1}" != "" ]; do logconf="${DIRNAME}/etc/logging-verbose.properties" shift ;; + 'ffm') + opts="${opts} --enable-preview --enable-native-access=ALL-UNNAMED" + shift + ;; *) opts="${opts} ${1}" shift @@ -98,6 +102,7 @@ echo "Launching Gogo JLine..." echo "Classpath: $cp" set mouse=a java -cp $cp \ + --enable-preview \ $opts \ -Dgosh.home="${DIRNAME}" \ -Djava.util.logging.config.file="${logconf}" \ diff --git a/demo/pom.xml b/demo/pom.xml index b14abbded..9c7ca7eea 100644 --- a/demo/pom.xml +++ b/demo/pom.xml @@ -27,6 +27,10 @@ + + org.jline + jline-terminal-ffm + org.jline jline-terminal-jansi diff --git a/jline/pom.xml b/jline/pom.xml index b2eedb4f8..c10bd7fa8 100644 --- a/jline/pom.xml +++ b/jline/pom.xml @@ -24,6 +24,7 @@ org.jline + --enable-preview --release 21 @@ -316,10 +317,9 @@ default-compile - - **/TTop.java - **/ConsoleEngineImpl.java - + + **/ffm/*.java + -Xlint:all,-options -Werror @@ -327,20 +327,19 @@ - compact + jdk21 compile - - **/TTop.java - **/ConsoleEngineImpl.java - + + **/ffm/*.java + + 21 -Xlint:all,-options -Werror - -profile - compact1 + --enable-preview diff --git a/native/src/main/java/org/jline/nativ/OSInfo.java b/native/src/main/java/org/jline/nativ/OSInfo.java index 689f5b385..9df657e4f 100644 --- a/native/src/main/java/org/jline/nativ/OSInfo.java +++ b/native/src/main/java/org/jline/nativ/OSInfo.java @@ -48,7 +48,7 @@ public class OSInfo { public static final String PPC64 = "ppc64"; public static final String ARM64 = "arm64"; - private static final HashMap archMapping = new HashMap(); + private static final HashMap archMapping = new HashMap<>(); static { // x86 mappings @@ -116,6 +116,7 @@ public static boolean isAndroid() { return System.getProperty("java.runtime.name", "").toLowerCase().contains("android"); } + @SuppressWarnings("unused") public static boolean isAlpine() { try { Process p = Runtime.getRuntime().exec(new String[] {"cat", "/etc/os-release", "|", "grep", "^ID"}); diff --git a/pom.xml b/pom.xml index 67de2bd1c..6e1e237ce 100644 --- a/pom.xml +++ b/pom.xml @@ -58,6 +58,7 @@ native terminal + terminal-ffm terminal-jna terminal-jansi reader @@ -92,7 +93,7 @@ UTF-8 2023-03-08T19:46:51Z - 11 + 21 1.8 3.5.0 @@ -131,6 +132,12 @@ ${project.version} + + org.jline + jline-terminal-ffm + ${project.version} + + org.jline jline-terminal-jansi @@ -407,8 +414,6 @@ -Xlint:all,-options,-processing -Werror - -profile - compact1 true diff --git a/reader/pom.xml b/reader/pom.xml index f25c86abe..1eccc313f 100644 --- a/reader/pom.xml +++ b/reader/pom.xml @@ -24,6 +24,7 @@ org.jline.reader + --add-opens java.base/java.io=ALL-UNNAMED --enable-preview diff --git a/reader/src/test/java/org/jline/reader/impl/LineReaderTest.java b/reader/src/test/java/org/jline/reader/impl/LineReaderTest.java index 10fabd33c..584f433bb 100644 --- a/reader/src/test/java/org/jline/reader/impl/LineReaderTest.java +++ b/reader/src/test/java/org/jline/reader/impl/LineReaderTest.java @@ -13,6 +13,8 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; import java.io.StringWriter; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -181,7 +183,9 @@ public void testPreferAppNameFromConstructor() throws IOException { @Test public void terminalLineInfiniteLoop() throws IOException { - ByteArrayInputStream in = new ByteArrayInputStream("hello\nworld\n".getBytes(StandardCharsets.UTF_8)); + PipedInputStream in = new PipedInputStream(); + PipedOutputStream outIn = new PipedOutputStream(in); + outIn.write("hello\nworld\n".getBytes(StandardCharsets.UTF_8)); ByteArrayOutputStream out = new ByteArrayOutputStream(1024); Terminal terminal = TerminalBuilder.builder().streams(in, out).build(); diff --git a/reader/src/test/java/org/jline/terminal/impl/ExternalTerminalTest.java b/reader/src/test/java/org/jline/terminal/impl/ExternalTerminalTest.java index 7573c5964..7d2e28557 100644 --- a/reader/src/test/java/org/jline/terminal/impl/ExternalTerminalTest.java +++ b/reader/src/test/java/org/jline/terminal/impl/ExternalTerminalTest.java @@ -29,6 +29,8 @@ import org.jline.terminal.Cursor; import org.jline.terminal.Terminal; import org.jline.terminal.TerminalBuilder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -39,6 +41,16 @@ public class ExternalTerminalTest { + @BeforeEach + public void setup() { + System.setProperty(TerminalBuilder.PROP_PROVIDERS, "exec"); + } + + @AfterEach + public void tearDown() { + System.clearProperty(TerminalBuilder.PROP_PROVIDERS); + } + @Test public void testInput() throws IOException, InterruptedException { PipedInputStream in = new PipedInputStream(); diff --git a/terminal-ffm/pom.xml b/terminal-ffm/pom.xml new file mode 100644 index 000000000..5bf5e6ec8 --- /dev/null +++ b/terminal-ffm/pom.xml @@ -0,0 +1,75 @@ + + + + + 4.0.0 + + + org.jline + jline-parent + 3.23.1-SNAPSHOT + + + jline-terminal-ffm + JLine FFM Terminal + + + 21 + org.jline.terminal.ffm + + + + + org.jline + jline-terminal + + + + org.junit.jupiter + junit-jupiter-api + test + + + + + + + org.apache.felix + maven-bundle-plugin + + + *;-noimport:=true + org.jline.terminal + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 21 + + --enable-preview + + + + + org.apache.maven.plugins + maven-surefire-plugin + + --enable-preview --enable-native-access=ALL-UNNAMED + + + + + + diff --git a/terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/CLibrary.java b/terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/CLibrary.java new file mode 100644 index 000000000..c694b6839 --- /dev/null +++ b/terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/CLibrary.java @@ -0,0 +1,1075 @@ +/* + * Copyright (c) 2022-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl.ffm; + +import java.lang.foreign.*; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.VarHandle; +import java.util.EnumMap; +import java.util.EnumSet; + +import org.jline.terminal.Attributes; +import org.jline.terminal.Size; +import org.jline.terminal.spi.Pty; + +@SuppressWarnings("preview") +class CLibrary { + // Window sizes. + // @see IOCTL_TTY(2) man-page + static class winsize { + static final GroupLayout LAYOUT; + private static final VarHandle ws_col; + private static final VarHandle ws_row; + + static { + LAYOUT = MemoryLayout.structLayout( + ValueLayout.JAVA_SHORT.withName("ws_row"), + ValueLayout.JAVA_SHORT.withName("ws_col"), + ValueLayout.JAVA_SHORT, + ValueLayout.JAVA_SHORT); + ws_row = LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("ws_row")); + ws_col = LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("ws_col")); + } + + private final java.lang.foreign.MemorySegment seg; + + winsize() { + seg = java.lang.foreign.Arena.ofAuto().allocate(LAYOUT); + } + + winsize(short ws_col, short ws_row) { + this(); + ws_col(ws_col); + ws_row(ws_row); + } + + java.lang.foreign.MemorySegment segment() { + return seg; + } + + short ws_col() { + return (short) ws_col.get(seg); + } + + void ws_col(short col) { + ws_col.set(seg, col); + } + + short ws_row() { + return (short) ws_row.get(seg); + } + + void ws_row(short row) { + ws_row.set(seg, row); + } + } + + // termios structure for termios functions, describing a general terminal interface that is + // provided to control asynchronous communications ports + // @see TERMIOS(3) man-page + static class termios { + static final GroupLayout LAYOUT; + private static final VarHandle c_iflag; + private static final VarHandle c_oflag; + private static final VarHandle c_cflag; + private static final VarHandle c_lflag; + private static final VarHandle c_ispeed; + private static final VarHandle c_ospeed; + + static { + LAYOUT = MemoryLayout.structLayout( + ValueLayout.JAVA_LONG.withName("c_iflag"), + ValueLayout.JAVA_LONG.withName("c_oflag"), + ValueLayout.JAVA_LONG.withName("c_cflag"), + ValueLayout.JAVA_LONG.withName("c_lflag"), + MemoryLayout.sequenceLayout(32, ValueLayout.JAVA_BYTE).withName("c_cc"), + ValueLayout.JAVA_LONG.withName("c_ispeed"), + ValueLayout.JAVA_LONG.withName("c_ospeed")); + c_iflag = LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("c_iflag")); + c_oflag = LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("c_oflag")); + c_cflag = LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("c_cflag")); + c_lflag = LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("c_lflag")); + c_ispeed = LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("c_ispeed")); + c_ospeed = LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("c_ospeed")); + } + + private final java.lang.foreign.MemorySegment seg; + + termios() { + seg = java.lang.foreign.Arena.ofAuto().allocate(LAYOUT); + } + + termios(Attributes t) { + this(); + // Input flags + long c_iflag = 0; + c_iflag = setFlag(t.getInputFlag(Attributes.InputFlag.IGNBRK), IGNBRK, c_iflag); + c_iflag = setFlag(t.getInputFlag(Attributes.InputFlag.BRKINT), BRKINT, c_iflag); + c_iflag = setFlag(t.getInputFlag(Attributes.InputFlag.IGNPAR), IGNPAR, c_iflag); + c_iflag = setFlag(t.getInputFlag(Attributes.InputFlag.PARMRK), PARMRK, c_iflag); + c_iflag = setFlag(t.getInputFlag(Attributes.InputFlag.INPCK), INPCK, c_iflag); + c_iflag = setFlag(t.getInputFlag(Attributes.InputFlag.ISTRIP), ISTRIP, c_iflag); + c_iflag = setFlag(t.getInputFlag(Attributes.InputFlag.INLCR), INLCR, c_iflag); + c_iflag = setFlag(t.getInputFlag(Attributes.InputFlag.IGNCR), IGNCR, c_iflag); + c_iflag = setFlag(t.getInputFlag(Attributes.InputFlag.ICRNL), ICRNL, c_iflag); + c_iflag = setFlag(t.getInputFlag(Attributes.InputFlag.IXON), IXON, c_iflag); + c_iflag = setFlag(t.getInputFlag(Attributes.InputFlag.IXOFF), IXOFF, c_iflag); + c_iflag = setFlag(t.getInputFlag(Attributes.InputFlag.IXANY), IXANY, c_iflag); + c_iflag = setFlag(t.getInputFlag(Attributes.InputFlag.IMAXBEL), IMAXBEL, c_iflag); + c_iflag = setFlag(t.getInputFlag(Attributes.InputFlag.IUTF8), IUTF8, c_iflag); + c_iflag(c_iflag); + // Output flags + long c_oflag = 0; + c_oflag = setFlag(t.getOutputFlag(Attributes.OutputFlag.OPOST), OPOST, c_oflag); + c_oflag = setFlag(t.getOutputFlag(Attributes.OutputFlag.ONLCR), ONLCR, c_oflag); + c_oflag = setFlag(t.getOutputFlag(Attributes.OutputFlag.OXTABS), OXTABS, c_oflag); + c_oflag = setFlag(t.getOutputFlag(Attributes.OutputFlag.ONOEOT), ONOEOT, c_oflag); + c_oflag = setFlag(t.getOutputFlag(Attributes.OutputFlag.OCRNL), OCRNL, c_oflag); + c_oflag = setFlag(t.getOutputFlag(Attributes.OutputFlag.ONOCR), ONOCR, c_oflag); + c_oflag = setFlag(t.getOutputFlag(Attributes.OutputFlag.ONLRET), ONLRET, c_oflag); + c_oflag = setFlag(t.getOutputFlag(Attributes.OutputFlag.OFILL), OFILL, c_oflag); + c_oflag = setFlag(t.getOutputFlag(Attributes.OutputFlag.NLDLY), NLDLY, c_oflag); + c_oflag = setFlag(t.getOutputFlag(Attributes.OutputFlag.TABDLY), TABDLY, c_oflag); + c_oflag = setFlag(t.getOutputFlag(Attributes.OutputFlag.CRDLY), CRDLY, c_oflag); + c_oflag = setFlag(t.getOutputFlag(Attributes.OutputFlag.FFDLY), FFDLY, c_oflag); + c_oflag = setFlag(t.getOutputFlag(Attributes.OutputFlag.BSDLY), BSDLY, c_oflag); + c_oflag = setFlag(t.getOutputFlag(Attributes.OutputFlag.VTDLY), VTDLY, c_oflag); + c_oflag = setFlag(t.getOutputFlag(Attributes.OutputFlag.OFDEL), OFDEL, c_oflag); + c_oflag(c_oflag); + // Control flags + long c_cflag = 0; + c_cflag = setFlag(t.getControlFlag(Attributes.ControlFlag.CIGNORE), CIGNORE, c_cflag); + c_cflag = setFlag(t.getControlFlag(Attributes.ControlFlag.CS5), CS5, c_cflag); + c_cflag = setFlag(t.getControlFlag(Attributes.ControlFlag.CS6), CS6, c_cflag); + c_cflag = setFlag(t.getControlFlag(Attributes.ControlFlag.CS7), CS7, c_cflag); + c_cflag = setFlag(t.getControlFlag(Attributes.ControlFlag.CS8), CS8, c_cflag); + c_cflag = setFlag(t.getControlFlag(Attributes.ControlFlag.CSTOPB), CSTOPB, c_cflag); + c_cflag = setFlag(t.getControlFlag(Attributes.ControlFlag.CREAD), CREAD, c_cflag); + c_cflag = setFlag(t.getControlFlag(Attributes.ControlFlag.PARENB), PARENB, c_cflag); + c_cflag = setFlag(t.getControlFlag(Attributes.ControlFlag.PARODD), PARODD, c_cflag); + c_cflag = setFlag(t.getControlFlag(Attributes.ControlFlag.HUPCL), HUPCL, c_cflag); + c_cflag = setFlag(t.getControlFlag(Attributes.ControlFlag.CLOCAL), CLOCAL, c_cflag); + c_cflag = setFlag(t.getControlFlag(Attributes.ControlFlag.CCTS_OFLOW), CCTS_OFLOW, c_cflag); + c_cflag = setFlag(t.getControlFlag(Attributes.ControlFlag.CRTS_IFLOW), CRTS_IFLOW, c_cflag); + c_cflag = setFlag(t.getControlFlag(Attributes.ControlFlag.CDTR_IFLOW), CDTR_IFLOW, c_cflag); + c_cflag = setFlag(t.getControlFlag(Attributes.ControlFlag.CDSR_OFLOW), CDSR_OFLOW, c_cflag); + c_cflag = setFlag(t.getControlFlag(Attributes.ControlFlag.CCAR_OFLOW), CCAR_OFLOW, c_cflag); + c_cflag(c_cflag); + // Local flags + long c_lflag = 0; + c_lflag = setFlag(t.getLocalFlag(Attributes.LocalFlag.ECHOKE), ECHOKE, c_lflag); + c_lflag = setFlag(t.getLocalFlag(Attributes.LocalFlag.ECHOE), ECHOE, c_lflag); + c_lflag = setFlag(t.getLocalFlag(Attributes.LocalFlag.ECHOK), ECHOK, c_lflag); + c_lflag = setFlag(t.getLocalFlag(Attributes.LocalFlag.ECHO), ECHO, c_lflag); + c_lflag = setFlag(t.getLocalFlag(Attributes.LocalFlag.ECHONL), ECHONL, c_lflag); + c_lflag = setFlag(t.getLocalFlag(Attributes.LocalFlag.ECHOPRT), ECHOPRT, c_lflag); + c_lflag = setFlag(t.getLocalFlag(Attributes.LocalFlag.ECHOCTL), ECHOCTL, c_lflag); + c_lflag = setFlag(t.getLocalFlag(Attributes.LocalFlag.ISIG), ISIG, c_lflag); + c_lflag = setFlag(t.getLocalFlag(Attributes.LocalFlag.ICANON), ICANON, c_lflag); + c_lflag = setFlag(t.getLocalFlag(Attributes.LocalFlag.ALTWERASE), ALTWERASE, c_lflag); + c_lflag = setFlag(t.getLocalFlag(Attributes.LocalFlag.IEXTEN), IEXTEN, c_lflag); + c_lflag = setFlag(t.getLocalFlag(Attributes.LocalFlag.EXTPROC), EXTPROC, c_lflag); + c_lflag = setFlag(t.getLocalFlag(Attributes.LocalFlag.TOSTOP), TOSTOP, c_lflag); + c_lflag = setFlag(t.getLocalFlag(Attributes.LocalFlag.FLUSHO), FLUSHO, c_lflag); + c_lflag = setFlag(t.getLocalFlag(Attributes.LocalFlag.NOKERNINFO), NOKERNINFO, c_lflag); + c_lflag = setFlag(t.getLocalFlag(Attributes.LocalFlag.PENDIN), PENDIN, c_lflag); + c_lflag = setFlag(t.getLocalFlag(Attributes.LocalFlag.NOFLSH), NOFLSH, c_lflag); + c_lflag(c_lflag); + // Control chars + byte[] c_cc = new byte[20]; + c_cc[VEOF] = (byte) t.getControlChar(Attributes.ControlChar.VEOF); + c_cc[VEOL] = (byte) t.getControlChar(Attributes.ControlChar.VEOL); + c_cc[VEOL2] = (byte) t.getControlChar(Attributes.ControlChar.VEOL2); + c_cc[VERASE] = (byte) t.getControlChar(Attributes.ControlChar.VERASE); + c_cc[VWERASE] = (byte) t.getControlChar(Attributes.ControlChar.VWERASE); + c_cc[VKILL] = (byte) t.getControlChar(Attributes.ControlChar.VKILL); + c_cc[VREPRINT] = (byte) t.getControlChar(Attributes.ControlChar.VREPRINT); + c_cc[VINTR] = (byte) t.getControlChar(Attributes.ControlChar.VINTR); + c_cc[VQUIT] = (byte) t.getControlChar(Attributes.ControlChar.VQUIT); + c_cc[VSUSP] = (byte) t.getControlChar(Attributes.ControlChar.VSUSP); + c_cc[VDSUSP] = (byte) t.getControlChar(Attributes.ControlChar.VDSUSP); + c_cc[VSTART] = (byte) t.getControlChar(Attributes.ControlChar.VSTART); + c_cc[VSTOP] = (byte) t.getControlChar(Attributes.ControlChar.VSTOP); + c_cc[VLNEXT] = (byte) t.getControlChar(Attributes.ControlChar.VLNEXT); + c_cc[VDISCARD] = (byte) t.getControlChar(Attributes.ControlChar.VDISCARD); + c_cc[VMIN] = (byte) t.getControlChar(Attributes.ControlChar.VMIN); + c_cc[VTIME] = (byte) t.getControlChar(Attributes.ControlChar.VTIME); + c_cc[VSTATUS] = (byte) t.getControlChar(Attributes.ControlChar.VSTATUS); + c_cc().copyFrom(java.lang.foreign.MemorySegment.ofArray(c_cc)); + } + + java.lang.foreign.MemorySegment segment() { + return seg; + } + + long c_iflag() { + return (long) c_iflag.get(seg); + } + + void c_iflag(long f) { + c_iflag.set(seg, f); + } + + long c_oflag() { + return (long) c_oflag.get(seg); + } + + void c_oflag(long f) { + c_oflag.set(seg, f); + } + + long c_cflag() { + return (long) c_cflag.get(seg); + } + + void c_cflag(long f) { + c_cflag.set(seg, f); + } + + long c_lflag() { + return (long) c_lflag.get(seg); + } + + void c_lflag(long f) { + c_lflag.set(seg, f); + } + + java.lang.foreign.MemorySegment c_cc() { + return seg.asSlice(32, 20); + } + + long c_ispeed() { + return (long) c_ispeed.get(seg); + } + + void c_ispeed(long f) { + c_ispeed.set(seg, f); + } + + long c_ospeed() { + return (long) c_ospeed.get(seg); + } + + void c_ospeed(long f) { + c_ospeed.set(seg, f); + } + + private static long setFlag(boolean flag, long value, long org) { + return flag ? org | value : org; + } + + private static > void addFlag(long value, EnumSet flags, T flag, int v) { + if ((value & v) != 0) { + flags.add(flag); + } + } + + public Attributes asAttributes() { + Attributes attr = new Attributes(); + // Input flags + long c_iflag = c_iflag(); + EnumSet iflag = attr.getInputFlags(); + addFlag(c_iflag, iflag, Attributes.InputFlag.IGNBRK, IGNBRK); + addFlag(c_iflag, iflag, Attributes.InputFlag.IGNBRK, IGNBRK); + addFlag(c_iflag, iflag, Attributes.InputFlag.BRKINT, BRKINT); + addFlag(c_iflag, iflag, Attributes.InputFlag.IGNPAR, IGNPAR); + addFlag(c_iflag, iflag, Attributes.InputFlag.PARMRK, PARMRK); + addFlag(c_iflag, iflag, Attributes.InputFlag.INPCK, INPCK); + addFlag(c_iflag, iflag, Attributes.InputFlag.ISTRIP, ISTRIP); + addFlag(c_iflag, iflag, Attributes.InputFlag.INLCR, INLCR); + addFlag(c_iflag, iflag, Attributes.InputFlag.IGNCR, IGNCR); + addFlag(c_iflag, iflag, Attributes.InputFlag.ICRNL, ICRNL); + addFlag(c_iflag, iflag, Attributes.InputFlag.IXON, IXON); + addFlag(c_iflag, iflag, Attributes.InputFlag.IXOFF, IXOFF); + addFlag(c_iflag, iflag, Attributes.InputFlag.IXANY, IXANY); + addFlag(c_iflag, iflag, Attributes.InputFlag.IMAXBEL, IMAXBEL); + addFlag(c_iflag, iflag, Attributes.InputFlag.IUTF8, IUTF8); + // Output flags + long c_oflag = c_oflag(); + EnumSet oflag = attr.getOutputFlags(); + addFlag(c_oflag, oflag, Attributes.OutputFlag.OPOST, OPOST); + addFlag(c_oflag, oflag, Attributes.OutputFlag.ONLCR, ONLCR); + addFlag(c_oflag, oflag, Attributes.OutputFlag.OXTABS, OXTABS); + addFlag(c_oflag, oflag, Attributes.OutputFlag.ONOEOT, ONOEOT); + addFlag(c_oflag, oflag, Attributes.OutputFlag.OCRNL, OCRNL); + addFlag(c_oflag, oflag, Attributes.OutputFlag.ONOCR, ONOCR); + addFlag(c_oflag, oflag, Attributes.OutputFlag.ONLRET, ONLRET); + addFlag(c_oflag, oflag, Attributes.OutputFlag.OFILL, OFILL); + addFlag(c_oflag, oflag, Attributes.OutputFlag.NLDLY, NLDLY); + addFlag(c_oflag, oflag, Attributes.OutputFlag.TABDLY, TABDLY); + addFlag(c_oflag, oflag, Attributes.OutputFlag.CRDLY, CRDLY); + addFlag(c_oflag, oflag, Attributes.OutputFlag.FFDLY, FFDLY); + addFlag(c_oflag, oflag, Attributes.OutputFlag.BSDLY, BSDLY); + addFlag(c_oflag, oflag, Attributes.OutputFlag.VTDLY, VTDLY); + addFlag(c_oflag, oflag, Attributes.OutputFlag.OFDEL, OFDEL); + // Control flags + long c_cflag = c_cflag(); + EnumSet cflag = attr.getControlFlags(); + addFlag(c_cflag, cflag, Attributes.ControlFlag.CIGNORE, CIGNORE); + addFlag(c_cflag, cflag, Attributes.ControlFlag.CS5, CS5); + addFlag(c_cflag, cflag, Attributes.ControlFlag.CS6, CS6); + addFlag(c_cflag, cflag, Attributes.ControlFlag.CS7, CS7); + addFlag(c_cflag, cflag, Attributes.ControlFlag.CS8, CS8); + addFlag(c_cflag, cflag, Attributes.ControlFlag.CSTOPB, CSTOPB); + addFlag(c_cflag, cflag, Attributes.ControlFlag.CREAD, CREAD); + addFlag(c_cflag, cflag, Attributes.ControlFlag.PARENB, PARENB); + addFlag(c_cflag, cflag, Attributes.ControlFlag.PARODD, PARODD); + addFlag(c_cflag, cflag, Attributes.ControlFlag.HUPCL, HUPCL); + addFlag(c_cflag, cflag, Attributes.ControlFlag.CLOCAL, CLOCAL); + addFlag(c_cflag, cflag, Attributes.ControlFlag.CCTS_OFLOW, CCTS_OFLOW); + addFlag(c_cflag, cflag, Attributes.ControlFlag.CRTS_IFLOW, CRTS_IFLOW); + addFlag(c_cflag, cflag, Attributes.ControlFlag.CDSR_OFLOW, CDSR_OFLOW); + addFlag(c_cflag, cflag, Attributes.ControlFlag.CCAR_OFLOW, CCAR_OFLOW); + // Local flags + long c_lflag = c_lflag(); + EnumSet lflag = attr.getLocalFlags(); + addFlag(c_lflag, lflag, Attributes.LocalFlag.ECHOKE, ECHOKE); + addFlag(c_lflag, lflag, Attributes.LocalFlag.ECHOE, ECHOE); + addFlag(c_lflag, lflag, Attributes.LocalFlag.ECHOK, ECHOK); + addFlag(c_lflag, lflag, Attributes.LocalFlag.ECHO, ECHO); + addFlag(c_lflag, lflag, Attributes.LocalFlag.ECHONL, ECHONL); + addFlag(c_lflag, lflag, Attributes.LocalFlag.ECHOPRT, ECHOPRT); + addFlag(c_lflag, lflag, Attributes.LocalFlag.ECHOCTL, ECHOCTL); + addFlag(c_lflag, lflag, Attributes.LocalFlag.ISIG, ISIG); + addFlag(c_lflag, lflag, Attributes.LocalFlag.ICANON, ICANON); + addFlag(c_lflag, lflag, Attributes.LocalFlag.ALTWERASE, ALTWERASE); + addFlag(c_lflag, lflag, Attributes.LocalFlag.IEXTEN, IEXTEN); + addFlag(c_lflag, lflag, Attributes.LocalFlag.EXTPROC, EXTPROC); + addFlag(c_lflag, lflag, Attributes.LocalFlag.TOSTOP, TOSTOP); + addFlag(c_lflag, lflag, Attributes.LocalFlag.FLUSHO, FLUSHO); + addFlag(c_lflag, lflag, Attributes.LocalFlag.NOKERNINFO, NOKERNINFO); + addFlag(c_lflag, lflag, Attributes.LocalFlag.PENDIN, PENDIN); + addFlag(c_lflag, lflag, Attributes.LocalFlag.NOFLSH, NOFLSH); + // Control chars + byte[] c_cc = c_cc().toArray(ValueLayout.JAVA_BYTE); + EnumMap cc = attr.getControlChars(); + cc.put(Attributes.ControlChar.VEOF, (int) c_cc[VEOF]); + cc.put(Attributes.ControlChar.VEOL, (int) c_cc[VEOL]); + cc.put(Attributes.ControlChar.VEOL2, (int) c_cc[VEOL2]); + cc.put(Attributes.ControlChar.VERASE, (int) c_cc[VERASE]); + cc.put(Attributes.ControlChar.VWERASE, (int) c_cc[VWERASE]); + cc.put(Attributes.ControlChar.VKILL, (int) c_cc[VKILL]); + cc.put(Attributes.ControlChar.VREPRINT, (int) c_cc[VREPRINT]); + cc.put(Attributes.ControlChar.VINTR, (int) c_cc[VINTR]); + cc.put(Attributes.ControlChar.VQUIT, (int) c_cc[VQUIT]); + cc.put(Attributes.ControlChar.VSUSP, (int) c_cc[VSUSP]); + cc.put(Attributes.ControlChar.VDSUSP, (int) c_cc[VDSUSP]); + cc.put(Attributes.ControlChar.VSTART, (int) c_cc[VSTART]); + cc.put(Attributes.ControlChar.VSTOP, (int) c_cc[VSTOP]); + cc.put(Attributes.ControlChar.VLNEXT, (int) c_cc[VLNEXT]); + cc.put(Attributes.ControlChar.VDISCARD, (int) c_cc[VDISCARD]); + cc.put(Attributes.ControlChar.VMIN, (int) c_cc[VMIN]); + cc.put(Attributes.ControlChar.VTIME, (int) c_cc[VTIME]); + cc.put(Attributes.ControlChar.VSTATUS, (int) c_cc[VSTATUS]); + // Return + return attr; + } + } + + static MethodHandle ioctl; + static MethodHandle isatty; + static MethodHandle openpty; + static MethodHandle tcsetattr; + static MethodHandle tcgetattr; + static MethodHandle ttyname_r; + + static { + // methods + Linker linker = Linker.nativeLinker(); + // https://man7.org/linux/man-pages/man2/ioctl.2.html + 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)); + // https://www.man7.org/linux/man-pages/man3/isatty.3.html + isatty = linker.downcallHandle( + linker.defaultLookup().find("isatty").get(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT)); + // https://man7.org/linux/man-pages/man3/openpty.3.html + openpty = linker.downcallHandle( + linker.defaultLookup().find("openpty").get(), + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, + ValueLayout.ADDRESS, + ValueLayout.ADDRESS, + ValueLayout.ADDRESS, + ValueLayout.ADDRESS)); + // https://man7.org/linux/man-pages/man3/tcsetattr.3p.html + tcsetattr = linker.downcallHandle( + linker.defaultLookup().find("tcsetattr").get(), + FunctionDescriptor.of( + ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.ADDRESS)); + // https://man7.org/linux/man-pages/man3/tcgetattr.3p.html + tcgetattr = linker.downcallHandle( + linker.defaultLookup().find("tcgetattr").get(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.ADDRESS)); + // https://man7.org/linux/man-pages/man3/ttyname.3.html + ttyname_r = linker.downcallHandle( + linker.defaultLookup().find("ttyname_r").get(), + FunctionDescriptor.of( + ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_LONG)); + } + + static Size getTerminalSize(int fd) { + try { + winsize ws = new winsize(); + int res = (int) ioctl.invoke(fd, (long) TIOCGWINSZ, ws.segment()); + return new Size(ws.ws_col(), ws.ws_row()); + } catch (Throwable e) { + throw new RuntimeException("Unable to call ioctl(TIOCGWINSZ)", e); + } + } + + static void setTerminalSize(int fd, Size size) { + try { + winsize ws = new winsize(); + ws.ws_row((short) size.getRows()); + ws.ws_col((short) size.getColumns()); + int res = (int) ioctl.invoke(fd, TIOCSWINSZ, ws.segment()); + } catch (Throwable e) { + throw new RuntimeException("Unable to call ioctl(TIOCSWINSZ)", e); + } + } + + static Attributes getAttributes(int fd) { + try { + termios t = new termios(); + int res = (int) tcgetattr.invoke(fd, t.segment()); + return t.asAttributes(); + } catch (Throwable e) { + throw new RuntimeException("Unable to call tcgetattr()", e); + } + } + + static void setAttributes(int fd, Attributes attr) { + try { + termios t = new termios(attr); + int res = (int) tcsetattr.invoke(fd, TCSANOW, t.segment()); + } catch (Throwable e) { + throw new RuntimeException("Unable to call tcsetattr()", e); + } + } + + static boolean isTty(int fd) { + try { + return (int) isatty.invoke(fd) == 1; + } catch (Throwable e) { + throw new RuntimeException("Unable to call isatty()", e); + } + } + + static String ttyName(int fd) { + try { + java.lang.foreign.MemorySegment buf = + java.lang.foreign.Arena.ofAuto().allocate(64); + int res = (int) ttyname_r.invoke(fd, buf, buf.byteSize()); + byte[] data = buf.toArray(ValueLayout.JAVA_BYTE); + int len = 0; + while (data[len] != 0) { + len++; + } + return new String(data, 0, len); + } catch (Throwable e) { + throw new RuntimeException("Unable to call ttyname_r()", e); + } + } + + static Pty openpty(Attributes attr, Size size) { + try { + java.lang.foreign.MemorySegment buf = + java.lang.foreign.Arena.ofAuto().allocate(64); + java.lang.foreign.MemorySegment master = + java.lang.foreign.Arena.ofAuto().allocate(ValueLayout.JAVA_INT); + java.lang.foreign.MemorySegment slave = + java.lang.foreign.Arena.ofAuto().allocate(ValueLayout.JAVA_INT); + int res = (int) openpty.invoke( + master, + slave, + buf, + attr != null ? new termios(attr).segment() : java.lang.foreign.MemorySegment.NULL, + size != null + ? new winsize((short) size.getRows(), (short) size.getColumns()).segment() + : java.lang.foreign.MemorySegment.NULL); + byte[] str = buf.toArray(ValueLayout.JAVA_BYTE); + int len = 0; + while (str[len] != 0) { + len++; + } + String device = new String(str, 0, len); + return new FfmNativePty(master.get(ValueLayout.JAVA_INT, 0), slave.get(ValueLayout.JAVA_INT, 0), device); + } catch (Throwable e) { + throw new RuntimeException("Unable to call openpty()", e); + } + } + + // CONSTANTS + + private static final int TIOCGWINSZ; + private static final int TIOCSWINSZ; + + private static final int TCSANOW; + private static int TCSADRAIN; + private static int TCSAFLUSH; + + private static final int VEOF; + private static final int VEOL; + private static final int VEOL2; + private static final int VERASE; + private static final int VWERASE; + private static final int VKILL; + private static final int VREPRINT; + private static int VERASE2; + private static final int VINTR; + private static final int VQUIT; + private static final int VSUSP; + private static int VDSUSP; + private static final int VSTART; + private static final int VSTOP; + private static final int VLNEXT; + private static final int VDISCARD; + private static final int VMIN; + private static int VSWTC; + private static final int VTIME; + private static int VSTATUS; + + private static final int IGNBRK; + private static final int BRKINT; + private static final int IGNPAR; + private static final int PARMRK; + private static final int INPCK; + private static final int ISTRIP; + private static final int INLCR; + private static final int IGNCR; + private static final int ICRNL; + private static int IUCLC; + private static final int IXON; + private static final int IXOFF; + private static final int IXANY; + private static final int IMAXBEL; + private static int IUTF8; + + private static final int OPOST; + private static int OLCUC; + private static final int ONLCR; + private static int OXTABS; + private static int NLDLY; + private static int NL0; + private static int NL1; + private static final int TABDLY; + private static int TAB0; + private static int TAB1; + private static int TAB2; + private static int TAB3; + private static int CRDLY; + private static int CR0; + private static int CR1; + private static int CR2; + private static int CR3; + private static int FFDLY; + private static int FF0; + private static int FF1; + private static int XTABS; + private static int BSDLY; + private static int BS0; + private static int BS1; + private static int VTDLY; + private static int VT0; + private static int VT1; + private static int CBAUD; + private static int B0; + private static int B50; + private static int B75; + private static int B110; + private static int B134; + private static int B150; + private static int B200; + private static int B300; + private static int B600; + private static int B1200; + private static int B1800; + private static int B2400; + private static int B4800; + private static int B9600; + private static int B19200; + private static int B38400; + private static int EXTA; + private static int EXTB; + private static int OFDEL; + private static int ONOEOT; + private static final int OCRNL; + private static int ONOCR; + private static final int ONLRET; + private static int OFILL; + + private static int CIGNORE; + private static int CSIZE; + private static final int CS5; + private static final int CS6; + private static final int CS7; + private static final int CS8; + private static final int CSTOPB; + private static final int CREAD; + private static final int PARENB; + private static final int PARODD; + private static final int HUPCL; + private static final int CLOCAL; + private static int CCTS_OFLOW; + private static int CRTS_IFLOW; + private static int CDTR_IFLOW; + private static int CDSR_OFLOW; + private static int CCAR_OFLOW; + + private static final int ECHOKE; + private static final int ECHOE; + private static final int ECHOK; + private static final int ECHO; + private static final int ECHONL; + private static final int ECHOPRT; + private static final int ECHOCTL; + private static final int ISIG; + private static final int ICANON; + private static int XCASE; + private static int ALTWERASE; + private static final int IEXTEN; + private static final int EXTPROC; + private static final int TOSTOP; + private static final int FLUSHO; + private static int NOKERNINFO; + private static final int PENDIN; + private static final int NOFLSH; + + static { + String osName = System.getProperty("os.name"); + if (osName.startsWith("Linux")) { + String arch = System.getProperty("os.arch"); + boolean isMipsPpcOrSparc = arch.equals("mips") + || arch.equals("mips64") + || arch.equals("mipsel") + || arch.equals("mips64el") + || arch.startsWith("ppc") + || arch.startsWith("sparc"); + TIOCGWINSZ = isMipsPpcOrSparc ? 0x40087468 : 0x00005413; + TIOCSWINSZ = isMipsPpcOrSparc ? 0x80087467 : 0x00005414; + + TCSANOW = 0x0; + TCSADRAIN = 0x1; + TCSAFLUSH = 0x2; + + VINTR = 0; + VQUIT = 1; + VERASE = 2; + VKILL = 3; + VEOF = 4; + VTIME = 5; + VMIN = 6; + VSWTC = 7; + VSTART = 8; + VSTOP = 9; + VSUSP = 10; + VEOL = 11; + VREPRINT = 12; + VDISCARD = 13; + VWERASE = 14; + VLNEXT = 15; + VEOL2 = 16; + + IGNBRK = 0x0000001; + BRKINT = 0x0000002; + IGNPAR = 0x0000004; + PARMRK = 0x0000008; + INPCK = 0x0000010; + ISTRIP = 0x0000020; + INLCR = 0x0000040; + IGNCR = 0x0000080; + ICRNL = 0x0000100; + IUCLC = 0x0000200; + IXON = 0x0000400; + IXANY = 0x0000800; + IXOFF = 0x0001000; + IMAXBEL = 0x0002000; + IUTF8 = 0x0004000; + + OPOST = 0x0000001; + OLCUC = 0x0000002; + ONLCR = 0x0000004; + OCRNL = 0x0000008; + ONOCR = 0x0000010; + ONLRET = 0x0000020; + OFILL = 0x0000040; + OFDEL = 0x0000080; + NLDLY = 0x0000100; + NL0 = 0x0000000; + NL1 = 0x0000100; + CRDLY = 0x0000600; + CR0 = 0x0000000; + CR1 = 0x0000200; + CR2 = 0x0000400; + CR3 = 0x0000600; + TABDLY = 0x0001800; + TAB0 = 0x0000000; + TAB1 = 0x0000800; + TAB2 = 0x0001000; + TAB3 = 0x0001800; + XTABS = 0x0001800; + BSDLY = 0x0002000; + BS0 = 0x0000000; + BS1 = 0x0002000; + VTDLY = 0x0004000; + VT0 = 0x0000000; + VT1 = 0x0004000; + FFDLY = 0x0008000; + FF0 = 0x0000000; + FF1 = 0x0008000; + + CBAUD = 0x000100f; + B0 = 0x0000000; + B50 = 0x0000001; + B75 = 0x0000002; + B110 = 0x0000003; + B134 = 0x0000004; + B150 = 0x0000005; + B200 = 0x0000006; + B300 = 0x0000007; + B600 = 0x0000008; + B1200 = 0x0000009; + B1800 = 0x000000a; + B2400 = 0x000000b; + B4800 = 0x000000c; + B9600 = 0x000000d; + B19200 = 0x000000e; + B38400 = 0x000000f; + EXTA = B19200; + EXTB = B38400; + CSIZE = 0x0000030; + CS5 = 0x0000000; + CS6 = 0x0000010; + CS7 = 0x0000020; + CS8 = 0x0000030; + CSTOPB = 0x0000040; + CREAD = 0x0000080; + PARENB = 0x0000100; + PARODD = 0x0000200; + HUPCL = 0x0000400; + CLOCAL = 0x0000800; + + ISIG = 0x0000001; + ICANON = 0x0000002; + XCASE = 0x0000004; + ECHO = 0x0000008; + ECHOE = 0x0000010; + ECHOK = 0x0000020; + ECHONL = 0x0000040; + NOFLSH = 0x0000080; + TOSTOP = 0x0000100; + ECHOCTL = 0x0000200; + ECHOPRT = 0x0000400; + ECHOKE = 0x0000800; + FLUSHO = 0x0001000; + PENDIN = 0x0002000; + IEXTEN = 0x0008000; + EXTPROC = 0x0010000; + } else if (osName.startsWith("Solaris") || osName.startsWith("SunOS")) { + int _TIOC = ('T' << 8); + TIOCGWINSZ = (_TIOC | 104); + TIOCSWINSZ = (_TIOC | 103); + + TCSANOW = 0x0; + TCSADRAIN = 0x1; + TCSAFLUSH = 0x2; + + VINTR = 0; + VQUIT = 1; + VERASE = 2; + VKILL = 3; + VEOF = 4; + VTIME = 5; + VMIN = 6; + VSWTC = 7; + VSTART = 8; + VSTOP = 9; + VSUSP = 10; + VEOL = 11; + VREPRINT = 12; + VDISCARD = 13; + VWERASE = 14; + VLNEXT = 15; + VEOL2 = 16; + + IGNBRK = 0x0000001; + BRKINT = 0x0000002; + IGNPAR = 0x0000004; + PARMRK = 0x0000010; + INPCK = 0x0000020; + ISTRIP = 0x0000040; + INLCR = 0x0000100; + IGNCR = 0x0000200; + ICRNL = 0x0000400; + IUCLC = 0x0001000; + IXON = 0x0002000; + IXANY = 0x0004000; + IXOFF = 0x0010000; + IMAXBEL = 0x0020000; + IUTF8 = 0x0040000; + + OPOST = 0x0000001; + OLCUC = 0x0000002; + ONLCR = 0x0000004; + OCRNL = 0x0000010; + ONOCR = 0x0000020; + ONLRET = 0x0000040; + OFILL = 0x0000100; + OFDEL = 0x0000200; + NLDLY = 0x0000400; + NL0 = 0x0000000; + NL1 = 0x0000400; + CRDLY = 0x0003000; + CR0 = 0x0000000; + CR1 = 0x0001000; + CR2 = 0x0002000; + CR3 = 0x0003000; + TABDLY = 0x0014000; + TAB0 = 0x0000000; + TAB1 = 0x0004000; + TAB2 = 0x0010000; + TAB3 = 0x0014000; + XTABS = 0x0014000; + BSDLY = 0x0020000; + BS0 = 0x0000000; + BS1 = 0x0020000; + VTDLY = 0x0040000; + VT0 = 0x0000000; + VT1 = 0x0040000; + FFDLY = 0x0100000; + FF0 = 0x0000000; + FF1 = 0x0100000; + + CBAUD = 0x0010017; + B0 = 0x0000000; + B50 = 0x0000001; + B75 = 0x0000002; + B110 = 0x0000003; + B134 = 0x0000004; + B150 = 0x0000005; + B200 = 0x0000006; + B300 = 0x0000007; + B600 = 0x0000010; + B1200 = 0x0000011; + B1800 = 0x0000012; + B2400 = 0x0000013; + B4800 = 0x0000014; + B9600 = 0x0000015; + B19200 = 0x0000016; + B38400 = 0x0000017; + EXTA = 0xB19200; + EXTB = 0xB38400; + CSIZE = 0x0000060; + CS5 = 0x0000000; + CS6 = 0x0000020; + CS7 = 0x0000040; + CS8 = 0x0000060; + CSTOPB = 0x0000100; + CREAD = 0x0000200; + PARENB = 0x0000400; + PARODD = 0x0001000; + HUPCL = 0x0002000; + CLOCAL = 0x0004000; + + ISIG = 0x0000001; + ICANON = 0x0000002; + XCASE = 0x0000004; + ECHO = 0x0000010; + ECHOE = 0x0000020; + ECHOK = 0x0000040; + ECHONL = 0x0000100; + NOFLSH = 0x0000200; + TOSTOP = 0x0000400; + ECHOCTL = 0x0001000; + ECHOPRT = 0x0002000; + ECHOKE = 0x0004000; + FLUSHO = 0x0010000; + PENDIN = 0x0040000; + IEXTEN = 0x0100000; + EXTPROC = 0x0200000; + } else if (osName.startsWith("Mac") || osName.startsWith("Darwin")) { + TIOCGWINSZ = 0x40087468; + TIOCSWINSZ = 0x80087467; + + TCSANOW = 0x00000000; + + VEOF = 0; + VEOL = 1; + VEOL2 = 2; + VERASE = 3; + VWERASE = 4; + VKILL = 5; + VREPRINT = 6; + VINTR = 8; + VQUIT = 9; + VSUSP = 10; + VDSUSP = 11; + VSTART = 12; + VSTOP = 13; + VLNEXT = 14; + VDISCARD = 15; + VMIN = 16; + VTIME = 17; + VSTATUS = 18; + + IGNBRK = 0x00000001; + BRKINT = 0x00000002; + IGNPAR = 0x00000004; + PARMRK = 0x00000008; + INPCK = 0x00000010; + ISTRIP = 0x00000020; + INLCR = 0x00000040; + IGNCR = 0x00000080; + ICRNL = 0x00000100; + IXON = 0x00000200; + IXOFF = 0x00000400; + IXANY = 0x00000800; + IMAXBEL = 0x00002000; + IUTF8 = 0x00004000; + + OPOST = 0x00000001; + ONLCR = 0x00000002; + OXTABS = 0x00000004; + ONOEOT = 0x00000008; + OCRNL = 0x00000010; + ONOCR = 0x00000020; + ONLRET = 0x00000040; + OFILL = 0x00000080; + NLDLY = 0x00000300; + TABDLY = 0x00000c04; + CRDLY = 0x00003000; + FFDLY = 0x00004000; + BSDLY = 0x00008000; + VTDLY = 0x00010000; + OFDEL = 0x00020000; + + CIGNORE = 0x00000001; + CS5 = 0x00000000; + CS6 = 0x00000100; + CS7 = 0x00000200; + CS8 = 0x00000300; + CSTOPB = 0x00000400; + CREAD = 0x00000800; + PARENB = 0x00001000; + PARODD = 0x00002000; + HUPCL = 0x00004000; + CLOCAL = 0x00008000; + CCTS_OFLOW = 0x00010000; + CRTS_IFLOW = 0x00020000; + CDTR_IFLOW = 0x00040000; + CDSR_OFLOW = 0x00080000; + CCAR_OFLOW = 0x00100000; + + ECHOKE = 0x00000001; + ECHOE = 0x00000002; + ECHOK = 0x00000004; + ECHO = 0x00000008; + ECHONL = 0x00000010; + ECHOPRT = 0x00000020; + ECHOCTL = 0x00000040; + ISIG = 0x00000080; + ICANON = 0x00000100; + ALTWERASE = 0x00000200; + IEXTEN = 0x00000400; + EXTPROC = 0x00000800; + TOSTOP = 0x00400000; + FLUSHO = 0x00800000; + NOKERNINFO = 0x02000000; + PENDIN = 0x20000000; + NOFLSH = 0x80000000; + } else if (osName.startsWith("FreeBSD")) { + TIOCGWINSZ = 0x40087468; + TIOCSWINSZ = 0x80087467; + + TCSANOW = 0x0; + TCSADRAIN = 0x1; + TCSAFLUSH = 0x2; + + VEOF = 0; + VEOL = 1; + VEOL2 = 2; + VERASE = 3; + VWERASE = 4; + VKILL = 5; + VREPRINT = 6; + VERASE2 = 7; + VINTR = 8; + VQUIT = 9; + VSUSP = 10; + VDSUSP = 11; + VSTART = 12; + VSTOP = 13; + VLNEXT = 14; + VDISCARD = 15; + VMIN = 16; + VTIME = 17; + VSTATUS = 18; + + IGNBRK = 0x0000001; + BRKINT = 0x0000002; + IGNPAR = 0x0000004; + PARMRK = 0x0000008; + INPCK = 0x0000010; + ISTRIP = 0x0000020; + INLCR = 0x0000040; + IGNCR = 0x0000080; + ICRNL = 0x0000100; + IXON = 0x0000200; + IXOFF = 0x0000400; + IXANY = 0x0000800; + IMAXBEL = 0x0002000; + + OPOST = 0x0000001; + ONLCR = 0x0000002; + TABDLY = 0x0000004; + TAB0 = 0x0000000; + TAB3 = 0x0000004; + ONOEOT = 0x0000008; + OCRNL = 0x0000010; + ONLRET = 0x0000040; + + CIGNORE = 0x0000001; + CSIZE = 0x0000300; + CS5 = 0x0000000; + CS6 = 0x0000100; + CS7 = 0x0000200; + CS8 = 0x0000300; + CSTOPB = 0x0000400; + CREAD = 0x0000800; + PARENB = 0x0001000; + PARODD = 0x0002000; + HUPCL = 0x0004000; + CLOCAL = 0x0008000; + + ECHOKE = 0x0000001; + ECHOE = 0x0000002; + ECHOK = 0x0000004; + ECHO = 0x0000008; + ECHONL = 0x0000010; + ECHOPRT = 0x0000020; + ECHOCTL = 0x0000040; + ISIG = 0x0000080; + ICANON = 0x0000100; + ALTWERASE = 0x000200; + IEXTEN = 0x0000400; + EXTPROC = 0x0000800; + TOSTOP = 0x0400000; + FLUSHO = 0x0800000; + PENDIN = 0x2000000; + NOFLSH = 0x8000000; + } else { + throw new UnsupportedOperationException(); + } + } +} diff --git a/terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/FfmNativePty.java b/terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/FfmNativePty.java new file mode 100644 index 000000000..5ede7ed1b --- /dev/null +++ b/terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/FfmNativePty.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2022-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl.ffm; + +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.jline.terminal.Attributes; +import org.jline.terminal.Size; +import org.jline.terminal.impl.AbstractPty; +import org.jline.terminal.spi.TerminalProvider; + +class FfmNativePty extends AbstractPty { + private final int master; + private final int slave; + private final int slaveOut; + private final String name; + private final FileDescriptor masterFD; + private final FileDescriptor slaveFD; + private final FileDescriptor slaveOutFD; + + public FfmNativePty(int master, int slave, String name) { + this(master, newDescriptor(master), slave, newDescriptor(slave), slave, newDescriptor(slave), name); + } + + public FfmNativePty( + int master, + FileDescriptor masterFD, + int slave, + FileDescriptor slaveFD, + int slaveOut, + FileDescriptor slaveOutFD, + String name) { + this.master = master; + this.slave = slave; + this.slaveOut = slaveOut; + this.name = name; + this.masterFD = masterFD; + this.slaveFD = slaveFD; + this.slaveOutFD = slaveOutFD; + } + + @Override + public void close() throws IOException { + if (master > 0) { + getMasterInput().close(); + } + if (slave > 0) { + getSlaveInput().close(); + } + } + + public int getMaster() { + return master; + } + + public int getSlave() { + return slave; + } + + public int getSlaveOut() { + return slaveOut; + } + + public String getName() { + return name; + } + + public FileDescriptor getMasterFD() { + return masterFD; + } + + public FileDescriptor getSlaveFD() { + return slaveFD; + } + + public FileDescriptor getSlaveOutFD() { + return slaveOutFD; + } + + public InputStream getMasterInput() { + return new FileInputStream(getMasterFD()); + } + + public OutputStream getMasterOutput() { + return new FileOutputStream(getMasterFD()); + } + + protected InputStream doGetSlaveInput() { + return new FileInputStream(getSlaveFD()); + } + + public OutputStream getSlaveOutput() { + return new FileOutputStream(getSlaveOutFD()); + } + + @Override + public Attributes getAttr() throws IOException { + return CLibrary.getAttributes(slave); + } + + @Override + protected void doSetAttr(Attributes attr) throws IOException { + CLibrary.setAttributes(slave, attr); + } + + @Override + public Size getSize() throws IOException { + return CLibrary.getTerminalSize(slave); + } + + @Override + public void setSize(Size size) throws IOException { + CLibrary.setTerminalSize(slave, size); + } + + @Override + public String toString() { + return "FfmNativePty[" + getName() + "]"; + } + + public static boolean isPosixSystemStream(TerminalProvider.Stream stream) { + switch (stream) { + case Input: + return CLibrary.isTty(0); + case Output: + return CLibrary.isTty(1); + case Error: + return CLibrary.isTty(2); + default: + throw new IllegalArgumentException(); + } + } + + public static String posixSystemStreamName(TerminalProvider.Stream stream) { + switch (stream) { + case Input: + return CLibrary.ttyName(0); + case Output: + return CLibrary.ttyName(1); + case Error: + return CLibrary.ttyName(2); + default: + throw new IllegalArgumentException(); + } + } +} diff --git a/terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/FfmTerminalProvider.java b/terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/FfmTerminalProvider.java new file mode 100644 index 000000000..a0e94a9d3 --- /dev/null +++ b/terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/FfmTerminalProvider.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2022-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl.ffm; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; + +import org.jline.terminal.Attributes; +import org.jline.terminal.Size; +import org.jline.terminal.Terminal; +import org.jline.terminal.impl.PosixPtyTerminal; +import org.jline.terminal.impl.PosixSysTerminal; +import org.jline.terminal.spi.Pty; +import org.jline.terminal.spi.TerminalProvider; +import org.jline.utils.OSUtils; + +public class FfmTerminalProvider implements TerminalProvider { + + public FfmTerminalProvider() { + if (!FfmTerminalProvider.class.getModule().isNativeAccessEnabled()) { + throw new UnsupportedOperationException( + "Native access is not enabled for the current module: " + FfmTerminalProvider.class.getModule()); + } + } + + @Override + public String name() { + return "ffm"; + } + + @Override + public Terminal sysTerminal( + String name, + String type, + boolean ansiPassThrough, + Charset encoding, + boolean nativeSignals, + Terminal.SignalHandler signalHandler, + boolean paused, + Stream consoleStream) + throws IOException { + if (OSUtils.IS_WINDOWS) { + return NativeWinSysTerminal.createTerminal( + name, type, ansiPassThrough, encoding, nativeSignals, signalHandler, paused, consoleStream); + } else { + Pty pty = new FfmNativePty( + -1, + null, + 0, + FileDescriptor.in, + consoleStream == Stream.Output ? 1 : 2, + consoleStream == Stream.Output ? FileDescriptor.out : FileDescriptor.err, + CLibrary.ttyName(0)); + return new PosixSysTerminal(name, type, pty, encoding, nativeSignals, signalHandler); + } + } + + @Override + public Terminal newTerminal( + String name, + String type, + InputStream in, + OutputStream out, + Charset encoding, + Terminal.SignalHandler signalHandler, + boolean paused, + Attributes attributes, + Size size) + throws IOException { + Pty pty = CLibrary.openpty(attributes, size); + return new PosixPtyTerminal(name, type, pty, in, out, encoding, signalHandler, paused); + } + + @Override + public boolean isSystemStream(Stream stream) { + if (OSUtils.IS_WINDOWS) { + return isWindowsSystemStream(stream); + } else { + return isPosixSystemStream(stream); + } + } + + public boolean isWindowsSystemStream(Stream stream) { + return NativeWinSysTerminal.isWindowsSystemStream(stream); + } + + public boolean isPosixSystemStream(Stream stream) { + return FfmNativePty.isPosixSystemStream(stream); + } + + @Override + public String systemStreamName(Stream stream) { + return FfmNativePty.posixSystemStreamName(stream); + } +} diff --git a/terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/Kernel32.java b/terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/Kernel32.java new file mode 100644 index 000000000..1a2d313d7 --- /dev/null +++ b/terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/Kernel32.java @@ -0,0 +1,925 @@ +/* + * Copyright (c) 2009-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl.ffm; + +import java.io.IOException; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.VarHandle; +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +@SuppressWarnings({"unused", "preview"}) +final class Kernel32 { + + public static final int FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000; + + public static final int INVALID_HANDLE_VALUE = -1; + public static final int STD_INPUT_HANDLE = -10; + public static final int STD_OUTPUT_HANDLE = -11; + public static final int STD_ERROR_HANDLE = -12; + + public static final int ENABLE_PROCESSED_INPUT = 0x0001; + public static final int ENABLE_LINE_INPUT = 0x0002; + public static final int ENABLE_ECHO_INPUT = 0x0004; + public static final int ENABLE_WINDOW_INPUT = 0x0008; + public static final int ENABLE_MOUSE_INPUT = 0x0010; + public static final int ENABLE_INSERT_MODE = 0x0020; + public static final int ENABLE_QUICK_EDIT_MODE = 0x0040; + public static final int ENABLE_EXTENDED_FLAGS = 0x0080; + + public static final int RIGHT_ALT_PRESSED = 0x0001; + public static final int LEFT_ALT_PRESSED = 0x0002; + public static final int RIGHT_CTRL_PRESSED = 0x0004; + public static final int LEFT_CTRL_PRESSED = 0x0008; + public static final int SHIFT_PRESSED = 0x0010; + + public static final int FOREGROUND_BLUE = 0x0001; + public static final int FOREGROUND_GREEN = 0x0002; + public static final int FOREGROUND_RED = 0x0004; + public static final int FOREGROUND_INTENSITY = 0x0008; + public static final int BACKGROUND_BLUE = 0x0010; + public static final int BACKGROUND_GREEN = 0x0020; + public static final int BACKGROUND_RED = 0x0040; + public static final int BACKGROUND_INTENSITY = 0x0080; + + // Button state + public static final int FROM_LEFT_1ST_BUTTON_PRESSED = 0x0001; + public static final int RIGHTMOST_BUTTON_PRESSED = 0x0002; + public static final int FROM_LEFT_2ND_BUTTON_PRESSED = 0x0004; + public static final int FROM_LEFT_3RD_BUTTON_PRESSED = 0x0008; + public static final int FROM_LEFT_4TH_BUTTON_PRESSED = 0x0010; + + // Event flags + public static final int MOUSE_MOVED = 0x0001; + public static final int DOUBLE_CLICK = 0x0002; + public static final int MOUSE_WHEELED = 0x0004; + public static final int MOUSE_HWHEELED = 0x0008; + + // Event types + public static final short KEY_EVENT = 0x0001; + public static final short MOUSE_EVENT = 0x0002; + public static final short WINDOW_BUFFER_SIZE_EVENT = 0x0004; + public static final short MENU_EVENT = 0x0008; + public static final short FOCUS_EVENT = 0x0010; + + public static int WaitForSingleObject(java.lang.foreign.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 java.lang.foreign.MemorySegment GetStdHandle(int nStdHandle) { + MethodHandle mh$ = requireNonNull(GetStdHandle$MH, "GetStdHandle"); + try { + return (java.lang.foreign.MemorySegment) mh$.invokeExact(nStdHandle); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int FormatMessageW( + int dwFlags, + java.lang.foreign.MemorySegment lpSource, + int dwMessageId, + int dwLanguageId, + java.lang.foreign.MemorySegment lpBuffer, + int nSize, + java.lang.foreign.MemorySegment Arguments) { + MethodHandle mh$ = requireNonNull(FormatMessageW$MH, "FormatMessageW"); + try { + return (int) mh$.invokeExact(dwFlags, lpSource, dwMessageId, dwLanguageId, lpBuffer, nSize, Arguments); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int SetConsoleTextAttribute(java.lang.foreign.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(java.lang.foreign.MemorySegment hConsoleHandle, int dwMode) { + MethodHandle mh$ = requireNonNull(SetConsoleMode$MH, "SetConsoleMode"); + try { + return (int) mh$.invokeExact(hConsoleHandle, dwMode); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int GetConsoleMode( + java.lang.foreign.MemorySegment hConsoleHandle, java.lang.foreign.MemorySegment lpMode) { + MethodHandle mh$ = requireNonNull(GetConsoleMode$MH, "GetConsoleMode"); + try { + return (int) mh$.invokeExact(hConsoleHandle, lpMode); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int SetConsoleTitleW(java.lang.foreign.MemorySegment lpConsoleTitle) { + MethodHandle mh$ = requireNonNull(SetConsoleTitleW$MH, "SetConsoleTitleW"); + try { + return (int) mh$.invokeExact(lpConsoleTitle); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int SetConsoleCursorPosition(java.lang.foreign.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( + java.lang.foreign.MemorySegment hConsoleOutput, + char cCharacter, + int nLength, + COORD dwWriteCoord, + java.lang.foreign.MemorySegment lpNumberOfCharsWritten) { + MethodHandle mh$ = requireNonNull(FillConsoleOutputCharacterW$MH, "FillConsoleOutputCharacterW"); + try { + return (int) mh$.invokeExact(hConsoleOutput, cCharacter, nLength, dwWriteCoord.seg, lpNumberOfCharsWritten); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int FillConsoleOutputAttribute( + java.lang.foreign.MemorySegment hConsoleOutput, + short wAttribute, + int nLength, + COORD dwWriteCoord, + java.lang.foreign.MemorySegment lpNumberOfAttrsWritten) { + MethodHandle mh$ = requireNonNull(FillConsoleOutputAttribute$MH, "FillConsoleOutputAttribute"); + try { + return (int) mh$.invokeExact(hConsoleOutput, wAttribute, nLength, dwWriteCoord.seg, lpNumberOfAttrsWritten); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int WriteConsoleW( + java.lang.foreign.MemorySegment hConsoleOutput, + java.lang.foreign.MemorySegment lpBuffer, + int nNumberOfCharsToWrite, + java.lang.foreign.MemorySegment lpNumberOfCharsWritten, + java.lang.foreign.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( + java.lang.foreign.MemorySegment hConsoleInput, + java.lang.foreign.MemorySegment lpBuffer, + int nLength, + java.lang.foreign.MemorySegment lpNumberOfEventsRead) { + MethodHandle mh$ = requireNonNull(ReadConsoleInputW$MH, "ReadConsoleInputW"); + try { + return (int) mh$.invokeExact(hConsoleInput, lpBuffer, nLength, lpNumberOfEventsRead); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int PeekConsoleInputW( + java.lang.foreign.MemorySegment hConsoleInput, + java.lang.foreign.MemorySegment lpBuffer, + int nLength, + java.lang.foreign.MemorySegment lpNumberOfEventsRead) { + MethodHandle mh$ = requireNonNull(PeekConsoleInputW$MH, "PeekConsoleInputW"); + try { + return (int) mh$.invokeExact(hConsoleInput, lpBuffer, nLength, lpNumberOfEventsRead); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int GetConsoleScreenBufferInfo( + java.lang.foreign.MemorySegment hConsoleOutput, CONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo) { + MethodHandle mh$ = requireNonNull(GetConsoleScreenBufferInfo$MH, "GetConsoleScreenBufferInfo"); + try { + return (int) mh$.invokeExact(hConsoleOutput, lpConsoleScreenBufferInfo.seg); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int ScrollConsoleScreenBuffer( + java.lang.foreign.MemorySegment hConsoleOutput, + SMALL_RECT lpScrollRectangle, + SMALL_RECT lpClipRectangle, + COORD dwDestinationOrigin, + CHAR_INFO lpFill) { + MethodHandle mh$ = requireNonNull(ScrollConsoleScreenBufferW$MH, "ScrollConsoleScreenBuffer"); + try { + return (int) + mh$.invokeExact(hConsoleOutput, lpScrollRectangle, lpClipRectangle, dwDestinationOrigin, lpFill); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int GetLastError() { + MethodHandle mh$ = requireNonNull(GetLastError$MH, "GetLastError"); + try { + return (int) mh$.invokeExact(); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int GetFileType(java.lang.foreign.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 java.lang.foreign.MemorySegment _get_osfhandle(int fd) { + MethodHandle mh$ = requireNonNull(_get_osfhandle$MH, "_get_osfhandle"); + try { + return (java.lang.foreign.MemorySegment) mh$.invokeExact(fd); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static INPUT_RECORD[] readConsoleInputHelper(java.lang.foreign.MemorySegment handle, int count, boolean peek) + throws IOException { + return readConsoleInputHelper(java.lang.foreign.Arena.ofAuto(), handle, count, peek); + } + + public static INPUT_RECORD[] readConsoleInputHelper( + java.lang.foreign.Arena arena, java.lang.foreign.MemorySegment handle, int count, boolean peek) + throws IOException { + java.lang.foreign.MemorySegment inputRecordPtr = arena.allocateArray(INPUT_RECORD.LAYOUT, count); + java.lang.foreign.MemorySegment length = arena.allocate(java.lang.foreign.ValueLayout.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.lang.foreign.ValueLayout.JAVA_INT, 0); + return inputRecordPtr + .elements(INPUT_RECORD.LAYOUT) + .map(INPUT_RECORD::new) + .limit(len) + .toArray(INPUT_RECORD[]::new); + } + + public static String getLastErrorMessage() { + int errorCode = GetLastError(); + return getErrorMessage(errorCode); + } + + public static String getErrorMessage(int errorCode) { + int bufferSize = 160; + try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { + java.lang.foreign.MemorySegment data = arena.allocate(bufferSize); + FormatMessageW( + FORMAT_MESSAGE_FROM_SYSTEM, + java.lang.foreign.MemorySegment.NULL, + errorCode, + 0, + data, + bufferSize, + java.lang.foreign.MemorySegment.NULL); + return new String(data.toArray(java.lang.foreign.ValueLayout.JAVA_BYTE), StandardCharsets.UTF_16LE).trim(); + } + } + + private static final java.lang.foreign.SymbolLookup SYMBOL_LOOKUP; + + static { + System.loadLibrary("msvcrt"); + System.loadLibrary("Kernel32"); + SYMBOL_LOOKUP = java.lang.foreign.SymbolLookup.loaderLookup(); + } + + static MethodHandle downcallHandle(String name, java.lang.foreign.FunctionDescriptor fdesc) { + return SYMBOL_LOOKUP + .find(name) + .map(addr -> java.lang.foreign.Linker.nativeLinker().downcallHandle(addr, fdesc)) + .orElse(null); + } + + static final java.lang.foreign.ValueLayout.OfBoolean C_BOOL$LAYOUT = java.lang.foreign.ValueLayout.JAVA_BOOLEAN; + static final java.lang.foreign.ValueLayout.OfByte C_CHAR$LAYOUT = java.lang.foreign.ValueLayout.JAVA_BYTE; + static final java.lang.foreign.ValueLayout.OfChar C_WCHAR$LAYOUT = java.lang.foreign.ValueLayout.JAVA_CHAR; + static final java.lang.foreign.ValueLayout.OfShort C_SHORT$LAYOUT = java.lang.foreign.ValueLayout.JAVA_SHORT; + static final java.lang.foreign.ValueLayout.OfShort C_WORD$LAYOUT = java.lang.foreign.ValueLayout.JAVA_SHORT; + static final java.lang.foreign.ValueLayout.OfInt C_DWORD$LAYOUT = java.lang.foreign.ValueLayout.JAVA_INT; + static final java.lang.foreign.ValueLayout.OfInt C_INT$LAYOUT = java.lang.foreign.ValueLayout.JAVA_INT; + static final java.lang.foreign.ValueLayout.OfLong C_LONG$LAYOUT = java.lang.foreign.ValueLayout.JAVA_LONG; + static final java.lang.foreign.ValueLayout.OfLong C_LONG_LONG$LAYOUT = java.lang.foreign.ValueLayout.JAVA_LONG; + static final java.lang.foreign.ValueLayout.OfFloat C_FLOAT$LAYOUT = java.lang.foreign.ValueLayout.JAVA_FLOAT; + static final java.lang.foreign.ValueLayout.OfDouble C_DOUBLE$LAYOUT = java.lang.foreign.ValueLayout.JAVA_DOUBLE; + static final java.lang.foreign.AddressLayout C_POINTER$LAYOUT = java.lang.foreign.ValueLayout.ADDRESS; + + static final MethodHandle WaitForSingleObject$MH = downcallHandle( + "WaitForSingleObject", + java.lang.foreign.FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT)); + static final MethodHandle GetStdHandle$MH = + downcallHandle("GetStdHandle", java.lang.foreign.FunctionDescriptor.of(C_POINTER$LAYOUT, C_INT$LAYOUT)); + static final MethodHandle FormatMessageW$MH = downcallHandle( + "FormatMessageW", + java.lang.foreign.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", + java.lang.foreign.FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_SHORT$LAYOUT)); + static final MethodHandle SetConsoleMode$MH = downcallHandle( + "SetConsoleMode", java.lang.foreign.FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT)); + static final MethodHandle GetConsoleMode$MH = downcallHandle( + "GetConsoleMode", + java.lang.foreign.FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT)); + + static final MethodHandle SetConsoleTitleW$MH = + downcallHandle("SetConsoleTitleW", java.lang.foreign.FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT)); + static final MethodHandle SetConsoleCursorPosition$MH = downcallHandle( + "SetConsoleCursorPosition", + java.lang.foreign.FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, COORD.LAYOUT)); + static final MethodHandle FillConsoleOutputCharacterW$MH = downcallHandle( + "FillConsoleOutputCharacterW", + java.lang.foreign.FunctionDescriptor.of( + C_INT$LAYOUT, C_POINTER$LAYOUT, C_WCHAR$LAYOUT, C_INT$LAYOUT, COORD.LAYOUT, C_POINTER$LAYOUT)); + static final MethodHandle FillConsoleOutputAttribute$MH = downcallHandle( + "FillConsoleOutputAttribute", + java.lang.foreign.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", + java.lang.foreign.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", + java.lang.foreign.FunctionDescriptor.of( + C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT, C_POINTER$LAYOUT)); + static final MethodHandle PeekConsoleInputW$MH = downcallHandle( + "PeekConsoleInputW", + java.lang.foreign.FunctionDescriptor.of( + C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT, C_POINTER$LAYOUT)); + + static final MethodHandle GetConsoleScreenBufferInfo$MH = downcallHandle( + "GetConsoleScreenBufferInfo", + java.lang.foreign.FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT)); + + static final MethodHandle ScrollConsoleScreenBufferW$MH = downcallHandle( + "ScrollConsoleScreenBufferW", + java.lang.foreign.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", java.lang.foreign.FunctionDescriptor.of(C_INT$LAYOUT)); + static final MethodHandle GetFileType$MH = + downcallHandle("GetFileType", java.lang.foreign.FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT)); + static final MethodHandle _get_osfhandle$MH = + downcallHandle("_get_osfhandle", java.lang.foreign.FunctionDescriptor.of(C_POINTER$LAYOUT, C_INT$LAYOUT)); + + public static final class INPUT_RECORD { + static final java.lang.foreign.MemoryLayout LAYOUT = java.lang.foreign.MemoryLayout.structLayout( + java.lang.foreign.ValueLayout.JAVA_SHORT.withName("EventType"), + java.lang.foreign.ValueLayout.JAVA_SHORT, // padding + java.lang.foreign.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 java.lang.foreign.MemorySegment seg; + + public INPUT_RECORD() { + this(java.lang.foreign.Arena.ofAuto()); + } + + public INPUT_RECORD(java.lang.foreign.Arena arena) { + this(arena.allocate(LAYOUT)); + } + + public INPUT_RECORD(java.lang.foreign.MemorySegment seg) { + this.seg = seg; + } + + public short eventType() { + return (short) EventType$VH.get(seg); + } + + public KEY_EVENT_RECORD keyEvent() { + return new KEY_EVENT_RECORD(seg, Event$OFFSET); + } + + public MOUSE_EVENT_RECORD mouseEvent() { + return new MOUSE_EVENT_RECORD(seg, Event$OFFSET); + } + + public FOCUS_EVENT_RECORD focusEvent() { + return new FOCUS_EVENT_RECORD(seg, Event$OFFSET); + } + } + + public static final class MENU_EVENT_RECORD { + + static final java.lang.foreign.GroupLayout LAYOUT = + java.lang.foreign.MemoryLayout.structLayout(C_DWORD$LAYOUT.withName("dwCommandId")); + static final VarHandle COMMAND_ID = varHandle(LAYOUT, "dwCommandId"); + + private final java.lang.foreign.MemorySegment seg; + + public MENU_EVENT_RECORD() { + this(java.lang.foreign.Arena.ofAuto()); + } + + public MENU_EVENT_RECORD(java.lang.foreign.Arena arena) { + this(arena.allocate(LAYOUT)); + } + + public MENU_EVENT_RECORD(java.lang.foreign.MemorySegment seg) { + this.seg = seg; + } + + public int commandId() { + return (int) MENU_EVENT_RECORD.COMMAND_ID.get(seg); + } + + public void commandId(int commandId) { + MENU_EVENT_RECORD.COMMAND_ID.set(seg, commandId); + } + } + + public static final class FOCUS_EVENT_RECORD { + + static final java.lang.foreign.GroupLayout LAYOUT = + java.lang.foreign.MemoryLayout.structLayout(C_INT$LAYOUT.withName("bSetFocus")); + static final VarHandle SET_FOCUS = varHandle(LAYOUT, "bSetFocus"); + + private final java.lang.foreign.MemorySegment seg; + + public FOCUS_EVENT_RECORD() { + this(java.lang.foreign.Arena.ofAuto()); + } + + public FOCUS_EVENT_RECORD(java.lang.foreign.Arena arena) { + this(arena.allocate(LAYOUT)); + } + + public FOCUS_EVENT_RECORD(java.lang.foreign.MemorySegment seg) { + this.seg = Objects.requireNonNull(seg); + } + + public FOCUS_EVENT_RECORD(java.lang.foreign.MemorySegment seg, long offset) { + this.seg = Objects.requireNonNull(seg).asSlice(offset, LAYOUT.byteSize()); + } + + public boolean setFocus() { + return ((int) FOCUS_EVENT_RECORD.SET_FOCUS.get(seg) != 0); + } + + public void setFocus(boolean setFocus) { + FOCUS_EVENT_RECORD.SET_FOCUS.set(seg, setFocus ? 1 : 0); + } + } + + public static final class WINDOW_BUFFER_SIZE_RECORD { + + static final java.lang.foreign.GroupLayout LAYOUT = + java.lang.foreign.MemoryLayout.structLayout(COORD.LAYOUT.withName("size")); + static final long SIZE_OFFSET = byteOffset(LAYOUT, "size"); + + private final java.lang.foreign.MemorySegment seg; + + public WINDOW_BUFFER_SIZE_RECORD() { + this(java.lang.foreign.Arena.ofAuto()); + } + + public WINDOW_BUFFER_SIZE_RECORD(java.lang.foreign.Arena arena) { + this(arena.allocate(LAYOUT)); + } + + public WINDOW_BUFFER_SIZE_RECORD(java.lang.foreign.MemorySegment seg) { + this.seg = seg; + } + + public COORD size() { + return new COORD(seg, SIZE_OFFSET); + } + + public String toString() { + return "WINDOW_BUFFER_SIZE_RECORD{size=" + this.size() + '}'; + } + } + + public static final class MOUSE_EVENT_RECORD { + + static final java.lang.foreign.MemoryLayout LAYOUT = java.lang.foreign.MemoryLayout.structLayout( + COORD.LAYOUT.withName("dwMousePosition"), + C_DWORD$LAYOUT.withName("dwButtonState"), + C_DWORD$LAYOUT.withName("dwControlKeyState"), + C_DWORD$LAYOUT.withName("dwEventFlags")); + static final long MOUSE_POSITION_OFFSET = byteOffset(LAYOUT, "dwMousePosition"); + static final VarHandle BUTTON_STATE = varHandle(LAYOUT, "dwButtonState"); + static final VarHandle CONTROL_KEY_STATE = varHandle(LAYOUT, "dwControlKeyState"); + static final VarHandle EVENT_FLAGS = varHandle(LAYOUT, "dwEventFlags"); + + private final java.lang.foreign.MemorySegment seg; + + public MOUSE_EVENT_RECORD() { + this(java.lang.foreign.Arena.ofAuto()); + } + + public MOUSE_EVENT_RECORD(java.lang.foreign.Arena arena) { + this(arena.allocate(LAYOUT)); + } + + public MOUSE_EVENT_RECORD(java.lang.foreign.MemorySegment seg) { + this.seg = Objects.requireNonNull(seg); + } + + public MOUSE_EVENT_RECORD(java.lang.foreign.MemorySegment seg, long offset) { + this.seg = Objects.requireNonNull(seg).asSlice(offset, LAYOUT.byteSize()); + } + + public COORD mousePosition() { + return new COORD(seg, MOUSE_POSITION_OFFSET); + } + + public int buttonState() { + return (int) BUTTON_STATE.get(seg); + } + + public int controlKeyState() { + return (int) CONTROL_KEY_STATE.get(seg); + } + + public int eventFlags() { + return (int) EVENT_FLAGS.get(seg); + } + + public String toString() { + return "MOUSE_EVENT_RECORD{mousePosition=" + mousePosition() + ", buttonState=" + buttonState() + + ", controlKeyState=" + controlKeyState() + ", eventFlags=" + eventFlags() + '}'; + } + } + + public static final class KEY_EVENT_RECORD { + + static final java.lang.foreign.MemoryLayout LAYOUT = java.lang.foreign.MemoryLayout.structLayout( + java.lang.foreign.ValueLayout.JAVA_INT.withName("bKeyDown"), + java.lang.foreign.ValueLayout.JAVA_SHORT.withName("wRepeatCount"), + java.lang.foreign.ValueLayout.JAVA_SHORT.withName("wVirtualKeyCode"), + java.lang.foreign.ValueLayout.JAVA_SHORT.withName("wVirtualScanCode"), + java.lang.foreign.MemoryLayout.unionLayout( + java.lang.foreign.ValueLayout.JAVA_CHAR.withName("UnicodeChar"), + java.lang.foreign.ValueLayout.JAVA_BYTE.withName("AsciiChar")) + .withName("uChar"), + java.lang.foreign.ValueLayout.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 java.lang.foreign.MemorySegment seg; + + public KEY_EVENT_RECORD() { + this(java.lang.foreign.Arena.ofAuto()); + } + + public KEY_EVENT_RECORD(java.lang.foreign.Arena arena) { + this(arena.allocate(LAYOUT)); + } + + public KEY_EVENT_RECORD(java.lang.foreign.MemorySegment seg) { + this.seg = seg; + } + + public KEY_EVENT_RECORD(java.lang.foreign.MemorySegment seg, long offset) { + this.seg = Objects.requireNonNull(seg).asSlice(offset, LAYOUT.byteSize()); + } + + public boolean keyDown() { + return ((int) bKeyDown$VH.get(seg)) != 0; + } + + public int repeatCount() { + return (int) wRepeatCount$VH.get(seg); + } + + public short keyCode() { + return (short) wVirtualKeyCode$VH.get(seg); + } + + public short scanCode() { + return (short) wVirtualScanCode$VH.get(seg); + } + + public char uchar() { + return (char) UnicodeChar$VH.get(seg); + } + + public int controlKeyState() { + return (int) dwControlKeyState$VH.get(seg); + } + + public String toString() { + return "KEY_EVENT_RECORD{keyDown=" + this.keyDown() + ", repeatCount=" + this.repeatCount() + ", keyCode=" + + this.keyCode() + ", scanCode=" + this.scanCode() + ", uchar=" + this.uchar() + + ", controlKeyState=" + + this.controlKeyState() + '}'; + } + } + + public static final class CHAR_INFO { + + static final java.lang.foreign.GroupLayout LAYOUT = java.lang.foreign.MemoryLayout.structLayout( + java.lang.foreign.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 java.lang.foreign.MemorySegment seg; + + public CHAR_INFO() { + this(java.lang.foreign.Arena.ofAuto()); + } + + public CHAR_INFO(java.lang.foreign.Arena arena) { + this(arena.allocate(LAYOUT)); + } + + public CHAR_INFO(java.lang.foreign.Arena arena, char c, short a) { + this(arena); + UnicodeChar$VH.set(seg, c); + Attributes$VH.set(seg, a); + } + + public CHAR_INFO(java.lang.foreign.MemorySegment seg) { + this.seg = seg; + } + + public char unicodeChar() { + return (char) UnicodeChar$VH.get(seg); + } + } + + public static final class CONSOLE_SCREEN_BUFFER_INFO { + static final java.lang.foreign.GroupLayout LAYOUT = java.lang.foreign.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 java.lang.foreign.MemorySegment seg; + + public CONSOLE_SCREEN_BUFFER_INFO() { + this(java.lang.foreign.Arena.ofAuto()); + } + + public CONSOLE_SCREEN_BUFFER_INFO(java.lang.foreign.Arena arena) { + this(arena.allocate(LAYOUT)); + } + + public CONSOLE_SCREEN_BUFFER_INFO(java.lang.foreign.MemorySegment seg) { + this.seg = seg; + } + + public COORD size() { + return new COORD(seg, dwSize$OFFSET); + } + + public COORD cursorPosition() { + return new COORD(seg, dwCursorPosition$OFFSET); + } + + public short attributes() { + return (short) wAttributes$VH.get(seg); + } + + public SMALL_RECT window() { + return new SMALL_RECT(seg, srWindow$OFFSET); + } + + public int windowWidth() { + return this.window().width() + 1; + } + + public int windowHeight() { + return this.window().height() + 1; + } + + public void attributes(short attr) { + wAttributes$VH.set(seg, attr); + } + } + + public static final class COORD { + + static final java.lang.foreign.GroupLayout LAYOUT = + java.lang.foreign.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 java.lang.foreign.MemorySegment seg; + + public COORD() { + this(java.lang.foreign.Arena.ofAuto()); + } + + public COORD(java.lang.foreign.Arena arena) { + this(arena.allocate(LAYOUT)); + } + + public COORD(java.lang.foreign.Arena arena, short x, short y) { + this(arena.allocate(LAYOUT)); + x(x); + y(y); + } + + public COORD(java.lang.foreign.MemorySegment seg) { + this.seg = seg; + } + + public COORD(java.lang.foreign.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(java.lang.foreign.Arena arena) { + return new COORD(arena.allocate(LAYOUT).copyFrom(seg)); + } + } + + public static final class SMALL_RECT { + + static final java.lang.foreign.GroupLayout LAYOUT = java.lang.foreign.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 java.lang.foreign.MemorySegment seg; + + public SMALL_RECT() { + this(java.lang.foreign.Arena.ofAuto()); + } + + public SMALL_RECT(java.lang.foreign.Arena arena) { + this(arena.allocate(LAYOUT)); + } + + public SMALL_RECT(java.lang.foreign.Arena arena, SMALL_RECT rect) { + this(arena); + left(rect.left()); + right(rect.right()); + top(rect.top()); + bottom(rect.bottom()); + } + + public SMALL_RECT(java.lang.foreign.MemorySegment seg, long offset) { + this(seg.asSlice(offset, LAYOUT.byteSize())); + } + + public SMALL_RECT(java.lang.foreign.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 void right(short r) { + Right$VH.set(seg, r); + } + + public void bottom(short b) { + Bottom$VH.set(seg, b); + } + + public SMALL_RECT copy(java.lang.foreign.Arena arena) { + return new SMALL_RECT(arena.allocate(LAYOUT).copyFrom(seg)); + } + } + + static T requireNonNull(T obj, String symbolName) { + if (obj == null) { + throw new UnsatisfiedLinkError("unresolved symbol: " + symbolName); + } + return obj; + } + + static VarHandle varHandle(java.lang.foreign.MemoryLayout layout, String name) { + return layout.varHandle(java.lang.foreign.MemoryLayout.PathElement.groupElement(name)); + } + + static VarHandle varHandle(java.lang.foreign.MemoryLayout layout, String e1, String name) { + return layout.varHandle( + java.lang.foreign.MemoryLayout.PathElement.groupElement(e1), + java.lang.foreign.MemoryLayout.PathElement.groupElement(name)); + } + + static long byteOffset(java.lang.foreign.MemoryLayout layout, String name) { + return layout.byteOffset(java.lang.foreign.MemoryLayout.PathElement.groupElement(name)); + } +} diff --git a/terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/NativeWinConsoleWriter.java b/terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/NativeWinConsoleWriter.java new file mode 100644 index 000000000..38573f954 --- /dev/null +++ b/terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/NativeWinConsoleWriter.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl.ffm; + +import java.io.IOException; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; + +import org.jline.terminal.impl.AbstractWindowsConsoleWriter; + +import static org.jline.terminal.impl.ffm.Kernel32.GetStdHandle; +import static org.jline.terminal.impl.ffm.Kernel32.STD_OUTPUT_HANDLE; +import static org.jline.terminal.impl.ffm.Kernel32.WriteConsoleW; +import static org.jline.terminal.impl.ffm.Kernel32.getLastErrorMessage; + +@SuppressWarnings("preview") +class NativeWinConsoleWriter extends AbstractWindowsConsoleWriter { + + private final java.lang.foreign.MemorySegment console = GetStdHandle(STD_OUTPUT_HANDLE); + + @Override + protected void writeConsole(char[] text, int len) throws IOException { + try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { + java.lang.foreign.MemorySegment txt = arena.allocateArray(ValueLayout.JAVA_CHAR, text); + if (WriteConsoleW(console, txt, len, MemorySegment.NULL, MemorySegment.NULL) == 0) { + throw new IOException("Failed to write to console: " + getLastErrorMessage()); + } + } + } +} diff --git a/terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/NativeWinSysTerminal.java b/terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/NativeWinSysTerminal.java new file mode 100644 index 000000000..411f0768f --- /dev/null +++ b/terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/NativeWinSysTerminal.java @@ -0,0 +1,289 @@ +/* + * Copyright (c) 2022-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl.ffm; + +import java.io.BufferedWriter; +import java.io.IOError; +import java.io.IOException; +import java.io.Writer; +import java.nio.charset.Charset; +import java.util.function.IntConsumer; + +import org.jline.terminal.Cursor; +import org.jline.terminal.Size; +import org.jline.terminal.impl.AbstractWindowsTerminal; +import org.jline.terminal.spi.TerminalProvider; +import org.jline.utils.OSUtils; + +import static org.jline.terminal.impl.ffm.Kernel32.*; +import static org.jline.terminal.impl.ffm.Kernel32.GetConsoleMode; +import static org.jline.terminal.impl.ffm.Kernel32.GetConsoleScreenBufferInfo; +import static org.jline.terminal.impl.ffm.Kernel32.GetStdHandle; +import static org.jline.terminal.impl.ffm.Kernel32.INPUT_RECORD; +import static org.jline.terminal.impl.ffm.Kernel32.INVALID_HANDLE_VALUE; +import static org.jline.terminal.impl.ffm.Kernel32.KEY_EVENT_RECORD; +import static org.jline.terminal.impl.ffm.Kernel32.MOUSE_EVENT_RECORD; +import static org.jline.terminal.impl.ffm.Kernel32.STD_ERROR_HANDLE; +import static org.jline.terminal.impl.ffm.Kernel32.STD_INPUT_HANDLE; +import static org.jline.terminal.impl.ffm.Kernel32.STD_OUTPUT_HANDLE; +import static org.jline.terminal.impl.ffm.Kernel32.SetConsoleMode; +import static org.jline.terminal.impl.ffm.Kernel32.WaitForSingleObject; +import static org.jline.terminal.impl.ffm.Kernel32.getLastErrorMessage; +import static org.jline.terminal.impl.ffm.Kernel32.readConsoleInputHelper; + +@SuppressWarnings("preview") +public class NativeWinSysTerminal extends AbstractWindowsTerminal { + + public static NativeWinSysTerminal createTerminal( + String name, + String type, + boolean ansiPassThrough, + Charset encoding, + boolean nativeSignals, + SignalHandler signalHandler, + boolean paused, + TerminalProvider.Stream consoleStream) + throws IOException { + try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { + // Get input console mode + java.lang.foreign.MemorySegment consoleIn = GetStdHandle(STD_INPUT_HANDLE); + java.lang.foreign.MemorySegment inMode = allocateInt(arena); + if (GetConsoleMode(consoleIn, inMode) == 0) { + throw new IOException("Failed to get console mode: " + getLastErrorMessage()); + } + // Get output console and mode + java.lang.foreign.MemorySegment console; + switch (consoleStream) { + case Output: + console = GetStdHandle(STD_OUTPUT_HANDLE); + break; + case Error: + console = GetStdHandle(STD_ERROR_HANDLE); + break; + default: + throw new IllegalArgumentException("Unsupport stream for console: " + consoleStream); + } + java.lang.foreign.MemorySegment outMode = allocateInt(arena); + if (GetConsoleMode(console, outMode) == 0) { + throw new IOException("Failed to get console mode: " + getLastErrorMessage()); + } + // Create writer + Writer writer; + if (ansiPassThrough) { + type = type != null ? type : OSUtils.IS_CONEMU ? TYPE_WINDOWS_CONEMU : TYPE_WINDOWS; + writer = new NativeWinConsoleWriter(); + } else { + int m = inMode.get(java.lang.foreign.ValueLayout.JAVA_INT, 0); + if (enableVtp(console, m)) { + type = type != null ? type : TYPE_WINDOWS_VTP; + writer = new NativeWinConsoleWriter(); + } else if (OSUtils.IS_CONEMU) { + type = type != null ? type : TYPE_WINDOWS_CONEMU; + writer = new NativeWinConsoleWriter(); + } else { + type = type != null ? type : TYPE_WINDOWS; + writer = new WindowsAnsiWriter(new BufferedWriter(new NativeWinConsoleWriter())); + } + } + // Create terminal + NativeWinSysTerminal terminal = new NativeWinSysTerminal( + writer, + name, + type, + encoding, + nativeSignals, + signalHandler, + consoleIn, + inMode.get(java.lang.foreign.ValueLayout.JAVA_INT, 0), + console, + outMode.get(java.lang.foreign.ValueLayout.JAVA_INT, 0)); + // Start input pump thread + if (!paused) { + terminal.resume(); + } + return terminal; + } + } + + private static boolean enableVtp(java.lang.foreign.MemorySegment console, int m) { + return SetConsoleMode(console, m | AbstractWindowsTerminal.ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0; + } + + public static boolean isWindowsSystemStream(TerminalProvider.Stream stream) { + try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { + java.lang.foreign.MemorySegment console; + java.lang.foreign.MemorySegment mode = allocateInt(arena); + switch (stream) { + case Input: + console = GetStdHandle(STD_INPUT_HANDLE); + break; + case Output: + console = GetStdHandle(STD_OUTPUT_HANDLE); + break; + case Error: + console = GetStdHandle(STD_ERROR_HANDLE); + break; + default: + return false; + } + return GetConsoleMode(console, mode) != 0; + } + } + + private static java.lang.foreign.MemorySegment allocateInt(java.lang.foreign.Arena arena) { + return arena.allocate(java.lang.foreign.ValueLayout.JAVA_INT); + } + + NativeWinSysTerminal( + Writer writer, + String name, + String type, + Charset encoding, + boolean nativeSignals, + SignalHandler signalHandler, + java.lang.foreign.MemorySegment inConsole, + int inConsoleMode, + java.lang.foreign.MemorySegment outConsole, + int outConsoleMode) + throws IOException { + super( + writer, + name, + type, + encoding, + nativeSignals, + signalHandler, + inConsole, + inConsoleMode, + outConsole, + outConsoleMode); + } + + @Override + protected int getConsoleMode(java.lang.foreign.MemorySegment console) { + try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { + java.lang.foreign.MemorySegment mode = arena.allocate(java.lang.foreign.ValueLayout.JAVA_INT); + if (GetConsoleMode(console, mode) == 0) { + return -1; + } + return mode.get(java.lang.foreign.ValueLayout.JAVA_INT, 0); + } + } + + @Override + protected void setConsoleMode(java.lang.foreign.MemorySegment console, int mode) { + SetConsoleMode(console, mode); + } + + public Size getSize() { + try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { + CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(arena); + GetConsoleScreenBufferInfo(outConsole, info); + return new Size(info.windowWidth(), info.windowHeight()); + } + } + + @Override + public Size getBufferSize() { + try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { + CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(arena); + GetConsoleScreenBufferInfo(outConsole, info); + return new Size(info.size().x(), info.size().y()); + } + } + + protected boolean processConsoleInput() throws IOException { + try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { + INPUT_RECORD[] events; + if (inConsole != null + && inConsole.address() != INVALID_HANDLE_VALUE + && WaitForSingleObject(inConsole, 100) == 0) { + events = readConsoleInputHelper(arena, inConsole, 1, false); + } else { + return false; + } + + boolean flush = false; + for (INPUT_RECORD event : events) { + int eventType = event.eventType(); + if (eventType == KEY_EVENT) { + KEY_EVENT_RECORD keyEvent = event.keyEvent(); + processKeyEvent( + keyEvent.keyDown(), keyEvent.keyCode(), keyEvent.uchar(), keyEvent.controlKeyState()); + flush = true; + } else if (eventType == WINDOW_BUFFER_SIZE_EVENT) { + raise(Signal.WINCH); + } else if (eventType == MOUSE_EVENT) { + processMouseEvent(event.mouseEvent()); + flush = true; + } else if (eventType == FOCUS_EVENT) { + processFocusEvent(event.focusEvent().setFocus()); + } + } + + return flush; + } + } + + private final char[] focus = new char[] {'\033', '[', ' '}; + + private void processFocusEvent(boolean hasFocus) throws IOException { + if (focusTracking) { + focus[2] = hasFocus ? 'I' : 'O'; + slaveInputPipe.write(focus); + } + } + + private final char[] mouse = new char[] {'\033', '[', 'M', ' ', ' ', ' '}; + + private void processMouseEvent(MOUSE_EVENT_RECORD mouseEvent) throws IOException { + int dwEventFlags = mouseEvent.eventFlags(); + int dwButtonState = mouseEvent.buttonState(); + if (tracking == MouseTracking.Off + || tracking == MouseTracking.Normal && dwEventFlags == MOUSE_MOVED + || tracking == MouseTracking.Button && dwEventFlags == MOUSE_MOVED && dwButtonState == 0) { + return; + } + int cb = 0; + dwEventFlags &= ~DOUBLE_CLICK; // Treat double-clicks as normal + if (dwEventFlags == MOUSE_WHEELED) { + cb |= 64; + if ((dwButtonState >> 16) < 0) { + cb |= 1; + } + } else if (dwEventFlags == MOUSE_HWHEELED) { + return; + } else if ((dwButtonState & FROM_LEFT_1ST_BUTTON_PRESSED) != 0) { + cb |= 0x00; + } else if ((dwButtonState & RIGHTMOST_BUTTON_PRESSED) != 0) { + cb |= 0x01; + } else if ((dwButtonState & FROM_LEFT_2ND_BUTTON_PRESSED) != 0) { + cb |= 0x02; + } else { + cb |= 0x03; + } + int cx = mouseEvent.mousePosition().x(); + int cy = mouseEvent.mousePosition().y(); + mouse[3] = (char) (' ' + cb); + mouse[4] = (char) (' ' + cx + 1); + mouse[5] = (char) (' ' + cy + 1); + slaveInputPipe.write(mouse); + } + + @Override + public Cursor getCursorPosition(IntConsumer discarded) { + try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { + CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(arena); + if (GetConsoleScreenBufferInfo(outConsole, info) == 0) { + throw new IOError(new IOException("Could not get the cursor position: " + getLastErrorMessage())); + } + return new Cursor(info.cursorPosition().x(), info.cursorPosition().y()); + } + } +} diff --git a/terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/WindowsAnsiWriter.java b/terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/WindowsAnsiWriter.java new file mode 100644 index 000000000..ea6b89d67 --- /dev/null +++ b/terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/WindowsAnsiWriter.java @@ -0,0 +1,408 @@ +/* + * Copyright (c) 2022-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl.ffm; + +import java.io.IOException; +import java.io.Writer; + +import org.jline.utils.AnsiWriter; +import org.jline.utils.Colors; + +import static org.jline.terminal.impl.ffm.Kernel32.BACKGROUND_BLUE; +import static org.jline.terminal.impl.ffm.Kernel32.BACKGROUND_GREEN; +import static org.jline.terminal.impl.ffm.Kernel32.BACKGROUND_INTENSITY; +import static org.jline.terminal.impl.ffm.Kernel32.BACKGROUND_RED; +import static org.jline.terminal.impl.ffm.Kernel32.CHAR_INFO; +import static org.jline.terminal.impl.ffm.Kernel32.CONSOLE_SCREEN_BUFFER_INFO; +import static org.jline.terminal.impl.ffm.Kernel32.COORD; +import static org.jline.terminal.impl.ffm.Kernel32.FOREGROUND_BLUE; +import static org.jline.terminal.impl.ffm.Kernel32.FOREGROUND_GREEN; +import static org.jline.terminal.impl.ffm.Kernel32.FOREGROUND_INTENSITY; +import static org.jline.terminal.impl.ffm.Kernel32.FOREGROUND_RED; +import static org.jline.terminal.impl.ffm.Kernel32.FillConsoleOutputAttribute; +import static org.jline.terminal.impl.ffm.Kernel32.FillConsoleOutputCharacterW; +import static org.jline.terminal.impl.ffm.Kernel32.GetConsoleScreenBufferInfo; +import static org.jline.terminal.impl.ffm.Kernel32.GetStdHandle; +import static org.jline.terminal.impl.ffm.Kernel32.SMALL_RECT; +import static org.jline.terminal.impl.ffm.Kernel32.STD_OUTPUT_HANDLE; +import static org.jline.terminal.impl.ffm.Kernel32.ScrollConsoleScreenBuffer; +import static org.jline.terminal.impl.ffm.Kernel32.SetConsoleCursorPosition; +import static org.jline.terminal.impl.ffm.Kernel32.SetConsoleTextAttribute; +import static org.jline.terminal.impl.ffm.Kernel32.SetConsoleTitleW; +import static org.jline.terminal.impl.ffm.Kernel32.getLastErrorMessage; + +@SuppressWarnings("preview") +class WindowsAnsiWriter extends AnsiWriter { + + private static final java.lang.foreign.MemorySegment console = GetStdHandle(STD_OUTPUT_HANDLE); + + 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(java.lang.foreign.Arena.ofAuto()); + private final short originalColors; + + private boolean negative; + private boolean bold; + private boolean underline; + private short savedX = -1; + private short savedY = -1; + + public WindowsAnsiWriter(Writer out) throws IOException { + super(out); + getConsoleInfo(); + originalColors = info.attributes(); + } + + private void getConsoleInfo() throws IOException { + out.flush(); + if (GetConsoleScreenBufferInfo(console, info) == 0) { + throw new IOException("Could not get the screen info: " + getLastErrorMessage()); + } + if (negative) { + info.attributes(invertAttributeColors(info.attributes())); + } + } + + private void applyAttribute() throws IOException { + out.flush(); + short attributes = info.attributes(); + // bold is simulated by high foreground intensity + if (bold) { + attributes |= FOREGROUND_INTENSITY; + } + // underline is simulated by high foreground intensity + if (underline) { + attributes |= BACKGROUND_INTENSITY; + } + if (negative) { + attributes = invertAttributeColors(attributes); + } + if (SetConsoleTextAttribute(console, attributes) == 0) { + throw new IOException(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 { + info.cursorPosition().x((short) + Math.max(0, Math.min(info.size().x() - 1, info.cursorPosition().x()))); + info.cursorPosition().y((short) + Math.max(0, Math.min(info.size().y() - 1, info.cursorPosition().y()))); + if (SetConsoleCursorPosition(console, info.cursorPosition()) == 0) { + throw new IOException(getLastErrorMessage()); + } + } + + @Override + protected void processEraseScreen(int eraseOption) throws IOException { + getConsoleInfo(); + try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { + java.lang.foreign.MemorySegment written = arena.allocate(java.lang.foreign.ValueLayout.JAVA_INT); + switch (eraseOption) { + case ERASE_SCREEN -> { + COORD topLeft = new COORD(arena, (short) 0, info.window().top()); + int screenLength = info.window().height() * info.size().x(); + FillConsoleOutputAttribute(console, originalColors, screenLength, topLeft, written); + FillConsoleOutputCharacterW(console, ' ', screenLength, topLeft, written); + } + case ERASE_SCREEN_TO_BEGINING -> { + COORD topLeft2 = new COORD(arena, (short) 0, info.window().top()); + int lengthToCursor = + (info.cursorPosition().y() - info.window().top()) + * info.size().x() + + info.cursorPosition().x(); + FillConsoleOutputAttribute(console, originalColors, lengthToCursor, topLeft2, written); + FillConsoleOutputCharacterW(console, ' ', lengthToCursor, topLeft2, written); + } + case ERASE_SCREEN_TO_END -> { + int lengthToEnd = + (info.window().bottom() - info.cursorPosition().y()) + * info.size().x() + + (info.size().x() - info.cursorPosition().x()); + FillConsoleOutputAttribute(console, originalColors, lengthToEnd, info.cursorPosition(), written); + FillConsoleOutputCharacterW(console, ' ', lengthToEnd, info.cursorPosition(), written); + } + default -> {} + } + } + } + + @Override + protected void processEraseLine(int eraseOption) throws IOException { + getConsoleInfo(); + try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { + java.lang.foreign.MemorySegment written = arena.allocate(java.lang.foreign.ValueLayout.JAVA_INT); + switch (eraseOption) { + case ERASE_LINE -> { + COORD leftColCurrRow = + new COORD(arena, (short) 0, info.cursorPosition().y()); + FillConsoleOutputAttribute( + console, originalColors, info.size().x(), leftColCurrRow, written); + FillConsoleOutputCharacterW(console, ' ', info.size().x(), leftColCurrRow, written); + } + case ERASE_LINE_TO_BEGINING -> { + COORD leftColCurrRow2 = + new COORD(arena, (short) 0, info.cursorPosition().y()); + FillConsoleOutputAttribute( + console, originalColors, info.cursorPosition().x(), leftColCurrRow2, written); + FillConsoleOutputCharacterW( + console, ' ', info.cursorPosition().x(), leftColCurrRow2, written); + } + case ERASE_LINE_TO_END -> { + int lengthToLastCol = + info.size().x() - info.cursorPosition().x(); + FillConsoleOutputAttribute( + console, originalColors, lengthToLastCol, info.cursorPosition(), written); + FillConsoleOutputCharacterW(console, ' ', lengthToLastCol, info.cursorPosition(), written); + } + default -> {} + } + } + } + + protected void processCursorUpLine(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition().x((short) 0); + info.cursorPosition().y((short) (info.cursorPosition().y() - count)); + applyCursorPosition(); + } + + protected void processCursorDownLine(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition().x((short) 0); + info.cursorPosition().y((short) (info.cursorPosition().y() + count)); + applyCursorPosition(); + } + + @Override + protected void processCursorLeft(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition().x((short) (info.cursorPosition().x() - count)); + applyCursorPosition(); + } + + @Override + protected void processCursorRight(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition().x((short) (info.cursorPosition().x() + count)); + applyCursorPosition(); + } + + @Override + protected void processCursorDown(int count) throws IOException { + getConsoleInfo(); + int nb = Math.max(0, info.cursorPosition().y() + count - info.size().y() + 1); + if (nb != count) { + info.cursorPosition().y((short) (info.cursorPosition().y() + count)); + applyCursorPosition(); + } + if (nb > 0) { + try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { + SMALL_RECT scroll = new SMALL_RECT(arena, info.window()); + scroll.top((short) 0); + COORD org = new COORD(arena); + org.x((short) 0); + org.y((short) (-nb)); + CHAR_INFO info = new CHAR_INFO(arena, ' ', originalColors); + ScrollConsoleScreenBuffer(console, scroll, scroll, org, info); + } + } + } + + @Override + protected void processCursorUp(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition().y((short) (info.cursorPosition().y() - count)); + applyCursorPosition(); + } + + @Override + protected void processCursorTo(int row, int col) throws IOException { + getConsoleInfo(); + info.cursorPosition().y((short) (info.window().top() + row - 1)); + info.cursorPosition().x((short) (col - 1)); + applyCursorPosition(); + } + + @Override + protected void processCursorToColumn(int x) throws IOException { + getConsoleInfo(); + info.cursorPosition().x((short) (x - 1)); + applyCursorPosition(); + } + + @Override + protected void processSetForegroundColorExt(int paletteIndex) throws IOException { + int color = Colors.roundColor(paletteIndex, 16); + info.attributes((short) ((info.attributes() & ~0x0007) | ANSI_FOREGROUND_COLOR_MAP[color & 0x07])); + info.attributes( + (short) ((info.attributes() & ~FOREGROUND_INTENSITY) | (color >= 8 ? FOREGROUND_INTENSITY : 0))); + applyAttribute(); + } + + @Override + protected void processSetBackgroundColorExt(int paletteIndex) throws IOException { + int color = Colors.roundColor(paletteIndex, 16); + info.attributes((short) ((info.attributes() & ~0x0070) | ANSI_BACKGROUND_COLOR_MAP[color & 0x07])); + info.attributes( + (short) ((info.attributes() & ~BACKGROUND_INTENSITY) | (color >= 8 ? BACKGROUND_INTENSITY : 0))); + applyAttribute(); + } + + @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 processAttributeRest() throws IOException { + info.attributes((short) ((info.attributes() & ~0x00FF) | originalColors)); + this.negative = false; + this.bold = false; + this.underline = false; + applyAttribute(); + } + + @Override + protected void processSetAttribute(int attribute) throws IOException { + switch (attribute) { + case ATTRIBUTE_INTENSITY_BOLD -> { + bold = true; + applyAttribute(); + } + case ATTRIBUTE_INTENSITY_NORMAL -> { + bold = false; + applyAttribute(); + } + case ATTRIBUTE_UNDERLINE -> { + underline = true; + applyAttribute(); + } + case ATTRIBUTE_UNDERLINE_OFF -> { + underline = false; + applyAttribute(); + } + case ATTRIBUTE_NEGATIVE_ON -> { + negative = true; + applyAttribute(); + } + case ATTRIBUTE_NEGATIVE_OFF -> { + negative = false; + applyAttribute(); + } + default -> {} + } + } + + @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) { + out.flush(); + info.cursorPosition().x(savedX); + info.cursorPosition().y(savedY); + applyCursorPosition(); + } + } + + @Override + protected void processInsertLine(int optionInt) throws IOException { + try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { + getConsoleInfo(); + SMALL_RECT scroll = info.window().copy(arena); + scroll.top(info.cursorPosition().y()); + COORD org = + new COORD(arena, (short) 0, (short) (info.cursorPosition().y() + optionInt)); + CHAR_INFO info = new CHAR_INFO(arena, ' ', originalColors); + if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { + throw new IOException(getLastErrorMessage()); + } + } + } + + @Override + protected void processDeleteLine(int optionInt) throws IOException { + try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { + getConsoleInfo(); + SMALL_RECT scroll = info.window().copy(arena); + scroll.top(info.cursorPosition().y()); + COORD org = + new COORD(arena, (short) 0, (short) (info.cursorPosition().y() - optionInt)); + CHAR_INFO info = new CHAR_INFO(arena, ' ', originalColors); + if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { + throw new IOException(getLastErrorMessage()); + } + } + } + + @Override + protected void processChangeWindowTitle(String title) { + try (java.lang.foreign.Arena session = java.lang.foreign.Arena.ofConfined()) { + java.lang.foreign.MemorySegment str = session.allocateUtf8String(title); + SetConsoleTitleW(str); + } + } +} diff --git a/terminal-ffm/src/main/resources/META-INF/services/org/jline/terminal/provider/ffm b/terminal-ffm/src/main/resources/META-INF/services/org/jline/terminal/provider/ffm new file mode 100644 index 000000000..0932a413b --- /dev/null +++ b/terminal-ffm/src/main/resources/META-INF/services/org/jline/terminal/provider/ffm @@ -0,0 +1,16 @@ +# +# Copyright (C) 2022 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. +# +class = org.jline.terminal.impl.ffm.FfmTerminalProvider diff --git a/terminal-ffm/src/test/java/org/jline/terminal/impl/ffm/FfmTest.java b/terminal-ffm/src/test/java/org/jline/terminal/impl/ffm/FfmTest.java new file mode 100644 index 000000000..90f14362a --- /dev/null +++ b/terminal-ffm/src/test/java/org/jline/terminal/impl/ffm/FfmTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl.ffm; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.Charset; + +import org.jline.terminal.Attributes; +import org.jline.terminal.Size; +import org.jline.terminal.Terminal; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; + +public class FfmTest { + + @Test + @DisabledOnOs(OS.WINDOWS) // non system terminals are not supported on windows + public void testNewTerminalWithNull() throws IOException { + Terminal terminal = new FfmTerminalProvider() + .newTerminal( + "name", + "xterm", + new ByteArrayInputStream(new byte[0]), + new ByteArrayOutputStream(), + Charset.defaultCharset(), + Terminal.SignalHandler.SIG_DFL, + false, + null, + null); + // terminal.close(); + } + + @Test + @DisabledOnOs(OS.WINDOWS) // non system terminals are not supported on windows + public void testNewTerminalNoNull() throws IOException { + Terminal terminal = new FfmTerminalProvider() + .newTerminal( + "name", + "xterm", + new ByteArrayInputStream(new byte[0]), + new ByteArrayOutputStream(), + Charset.defaultCharset(), + Terminal.SignalHandler.SIG_DFL, + false, + new Attributes(), + new Size()); + Size size = terminal.getSize(); + // terminal.close(); + } + + @Test + @EnabledOnOs(OS.WINDOWS) + void checkStructLayout() { + try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { + new Kernel32.KEY_EVENT_RECORD(arena); + new Kernel32.MOUSE_EVENT_RECORD(arena); + new Kernel32.WINDOW_BUFFER_SIZE_RECORD(arena); + new Kernel32.MENU_EVENT_RECORD(arena); + new Kernel32.FOCUS_EVENT_RECORD(arena); + new Kernel32.INPUT_RECORD(arena); + new Kernel32.SMALL_RECT(arena); + } + } +} diff --git a/terminal/pom.xml b/terminal/pom.xml index 06f4eda41..92137dfa2 100644 --- a/terminal/pom.xml +++ b/terminal/pom.xml @@ -24,6 +24,7 @@ org.jline.terminal + --enable-preview --release 21 diff --git a/terminal/src/main/java/org/jline/terminal/TerminalBuilder.java b/terminal/src/main/java/org/jline/terminal/TerminalBuilder.java index 8797f7845..d1fdae166 100644 --- a/terminal/src/main/java/org/jline/terminal/TerminalBuilder.java +++ b/terminal/src/main/java/org/jline/terminal/TerminalBuilder.java @@ -50,10 +50,11 @@ public final class TerminalBuilder { public static final String PROP_CODEPAGE = "org.jline.terminal.codepage"; public static final String PROP_TYPE = "org.jline.terminal.type"; public static final String PROP_PROVIDERS = "org.jline.terminal.providers"; - public static final String PROP_PROVIDERS_DEFAULT = "jansi,jna,exec"; + public static final String PROP_PROVIDERS_DEFAULT = "ffm,jansi,jna,exec"; public static final String PROP_JNA = "org.jline.terminal.jna"; public static final String PROP_JANSI = "org.jline.terminal.jansi"; public static final String PROP_EXEC = "org.jline.terminal.exec"; + public static final String PROP_FFM = "org.jline.terminal.ffm"; public static final String PROP_DUMB = "org.jline.terminal.dumb"; public static final String PROP_DUMB_COLOR = "org.jline.terminal.dumb.color"; public static final String PROP_OUTPUT = "org.jline.terminal.output"; @@ -138,6 +139,7 @@ public static TerminalBuilder builder() { private Boolean jna; private Boolean jansi; private Boolean exec; + private Boolean ffm; private Boolean dumb; private Boolean color; private Attributes attributes; @@ -193,6 +195,11 @@ public TerminalBuilder exec(boolean exec) { return this; } + public TerminalBuilder ffm(boolean ffm) { + this.ffm = ffm; + return this; + } + public TerminalBuilder dumb(boolean dumb) { this.dumb = dumb; return this; @@ -377,15 +384,30 @@ private Terminal doBuild() throws IOException { if (exec == null) { exec = getBoolean(PROP_EXEC, true); } + Boolean ffm = this.ffm; + if (ffm == null) { + ffm = getBoolean(PROP_FFM, true); + } Boolean dumb = this.dumb; if (dumb == null) { dumb = getBoolean(PROP_DUMB, null); } IllegalStateException exception = new IllegalStateException("Unable to create a terminal"); List providers = new ArrayList<>(); + if (ffm) { + try { + TerminalProvider provider = TerminalProvider.load("ffm"); + provider.isSystemStream(TerminalProvider.Stream.Output); + providers.add(provider); + } catch (Throwable t) { + Log.debug("Unable to load FFM support: ", t); + exception.addSuppressed(t); + } + } if (jna) { try { TerminalProvider provider = TerminalProvider.load("jna"); + provider.isSystemStream(TerminalProvider.Stream.Output); providers.add(provider); } catch (Throwable t) { Log.debug("Unable to load JNA support: ", t); @@ -395,6 +417,7 @@ private Terminal doBuild() throws IOException { if (jansi) { try { TerminalProvider provider = TerminalProvider.load("jansi"); + provider.isSystemStream(TerminalProvider.Stream.Output); providers.add(provider); } catch (Throwable t) { Log.debug("Unable to load JANSI support: ", t); diff --git a/terminal/src/main/java/org/jline/terminal/impl/Diag.java b/terminal/src/main/java/org/jline/terminal/impl/Diag.java index 1b6133fd0..6bca5cc1b 100644 --- a/terminal/src/main/java/org/jline/terminal/impl/Diag.java +++ b/terminal/src/main/java/org/jline/terminal/impl/Diag.java @@ -50,6 +50,17 @@ static void diag(PrintStream out) { out.println("IS_OSX = " + OSUtils.IS_OSX); out.println(); + // FFM + out.println("FFM Support"); + out.println("================="); + try { + TerminalProvider provider = TerminalProvider.load("ffm"); + testProvider(out, provider); + } catch (Throwable t) { + out.println("FFM support not available: " + t); + } + out.println(); + out.println("JnaSupport"); out.println("================="); try {