From 11c0e61cbee070e7a951b7d008dc9dd8d52356c3 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Wed, 8 Jan 2025 14:20:38 +0100 Subject: [PATCH] Some progress. --- .../internal/io/BaseNativeConsoleReader.java | 192 ++++ .../classes/jdk/internal/io/CLibrary.java | 18 + .../jdk/internal/io/JdkConsoleImpl.java | 282 +----- .../jdk/internal/io/NativeConsoleReader.java | 35 + .../jdk/internal/io/SimpleConsoleReader.java | 213 ++++ .../internal/io/NativeConsoleReaderImpl.java | 69 ++ .../classes/jdk/internal/io/Kernel32.java | 942 ++++++++++++++++++ .../internal/io/NativeConsoleReaderImpl.java | 254 +++++ 8 files changed, 1736 insertions(+), 269 deletions(-) create mode 100644 src/java.base/share/classes/jdk/internal/io/BaseNativeConsoleReader.java create mode 100644 src/java.base/share/classes/jdk/internal/io/NativeConsoleReader.java create mode 100644 src/java.base/share/classes/jdk/internal/io/SimpleConsoleReader.java create mode 100644 src/java.base/unix/classes/jdk/internal/io/NativeConsoleReaderImpl.java create mode 100644 src/java.base/windows/classes/jdk/internal/io/Kernel32.java create mode 100644 src/java.base/windows/classes/jdk/internal/io/NativeConsoleReaderImpl.java diff --git a/src/java.base/share/classes/jdk/internal/io/BaseNativeConsoleReader.java b/src/java.base/share/classes/jdk/internal/io/BaseNativeConsoleReader.java new file mode 100644 index 0000000000000..bbf356ff2f542 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/io/BaseNativeConsoleReader.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.io; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.util.Arrays; +import sun.nio.cs.StreamDecoder; + +public class BaseNativeConsoleReader implements NativeConsoleReader { + + private final Object readLock; + private char[] rcb; + + public BaseNativeConsoleReader(Object readLock) { + this.readLock = readLock; + this.rcb = new char[1024]; + } + + public char[] readline(Reader reader, Writer out, boolean zeroOut) throws IOException { + reader = new LineReader(reader); + int len = reader.read(rcb, 0, rcb.length); + if (len < 0) + return null; //EOL + if (rcb[len-1] == '\r') + len--; //remove CR at end; + else if (rcb[len-1] == '\n') { + len--; //remove LF at end; + if (len > 0 && rcb[len-1] == '\r') + len--; //remove the CR, if there is one + } + char[] b = new char[len]; + if (len > 0) { + System.arraycopy(rcb, 0, b, 0, len); + if (zeroOut) { + Arrays.fill(rcb, 0, len, ' '); + if (reader instanceof LineReader lr) { + lr.zeroOut(); + } + } + } + return b; + } + + private char[] grow() { + assert Thread.holdsLock(readLock); + char[] t = new char[rcb.length * 2]; + System.arraycopy(rcb, 0, t, 0, rcb.length); + rcb = t; + return rcb; + } + + class LineReader extends Reader { + private final Reader in; + private final char[] cb; + private int nChars, nextChar; + boolean leftoverLF; + LineReader(Reader in) { + this.in = in; + cb = new char[1024]; + nextChar = nChars = 0; + leftoverLF = false; + } + public void zeroOut() throws IOException { + if (in instanceof StreamDecoder sd) { + sd.fillZeroToPosition(); + } + } + public void close () {} + public boolean ready() throws IOException { + //in.ready synchronizes on readLock already + return in.ready(); + } + + public int read(char[] cbuf, int offset, int length) + throws IOException + { + int off = offset; + int end = offset + length; + if (offset < 0 || offset > cbuf.length || length < 0 || + end < 0 || end > cbuf.length) { + throw new IndexOutOfBoundsException(); + } + synchronized(readLock) { + boolean eof = false; + char c; + for (;;) { + if (nextChar >= nChars) { //fill + int n; + do { + n = in.read(cb, 0, cb.length); + } while (n == 0); + if (n > 0) { + nChars = n; + nextChar = 0; + if (n < cb.length && + cb[n-1] != '\n' && cb[n-1] != '\r') { + /* + * we're in canonical mode so each "fill" should + * come back with an eol. if there is no lf or nl at + * the end of returned bytes we reached an eof. + */ + eof = true; + } + } else { /*EOF*/ + if (off - offset == 0) + return -1; + return off - offset; + } + } + if (leftoverLF && cbuf == rcb && cb[nextChar] == '\n') { + /* + * if invoked by our readline, skip the leftover, otherwise + * return the LF. + */ + nextChar++; + } + leftoverLF = false; + while (nextChar < nChars) { + c = cbuf[off++] = cb[nextChar]; + cb[nextChar++] = 0; + if (c == '\n') { + return off - offset; + } else if (c == '\r') { + if (off == end) { + /* no space left even the next is LF, so return + * whatever we have if the invoker is not our + * readLine() + */ + if (cbuf == rcb) { + cbuf = grow(); + } else { + leftoverLF = true; + return off - offset; + } + } + if (nextChar == nChars && in.ready()) { + /* + * we have a CR and we reached the end of + * the read in buffer, fill to make sure we + * don't miss a LF, if there is one, it's possible + * that it got cut off during last round reading + * simply because the read in buffer was full. + */ + nChars = in.read(cb, 0, cb.length); + nextChar = 0; + } + if (nextChar < nChars && cb[nextChar] == '\n') { + cbuf[off++] = '\n'; + nextChar++; + } + return off - offset; + } else if (off == end) { + if (cbuf == rcb) { + cbuf = grow(); + end = cbuf.length; + } else { + return off - offset; + } + } + } + if (eof) + return off - offset; + } + } + } + } + +} diff --git a/src/java.base/share/classes/jdk/internal/io/CLibrary.java b/src/java.base/share/classes/jdk/internal/io/CLibrary.java index aa10a2e8b6871..adf07573e4954 100644 --- a/src/java.base/share/classes/jdk/internal/io/CLibrary.java +++ b/src/java.base/share/classes/jdk/internal/io/CLibrary.java @@ -18,6 +18,7 @@ import java.lang.invoke.VarHandle; import java.util.EnumMap; import java.util.EnumSet; +import jdk.internal.io.SimpleConsoleReader.Size; @SuppressWarnings("restricted") class CLibrary { @@ -465,6 +466,15 @@ private static String readFully(InputStream in) throws IOException { return b.toString(); } + 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 Attributes getAttributes(int fd) { try { termios t = new termios(); @@ -494,6 +504,9 @@ static boolean isTty(int fd) { // CONSTANTS + private static final int TIOCGWINSZ; +// private static final int TIOCSWINSZ; + private static final int TCSANOW; private static int TCSADRAIN; private static int TCSAFLUSH; @@ -634,6 +647,9 @@ static boolean isTty(int fd) { if (osName.startsWith("Linux")) { IS_LINUX = true; IS_OSX = false; + + TIOCGWINSZ = 0x00005413; + TCSANOW = 0x0; TCSADRAIN = 0x1; TCSAFLUSH = 0x2; @@ -758,6 +774,8 @@ static boolean isTty(int fd) { IS_LINUX = false; IS_OSX = true; + TIOCGWINSZ = 0x40087468; + TCSANOW = 0x00000000; VEOF = 0; diff --git a/src/java.base/share/classes/jdk/internal/io/JdkConsoleImpl.java b/src/java.base/share/classes/jdk/internal/io/JdkConsoleImpl.java index 251cbaf59c4eb..f75014630736c 100644 --- a/src/java.base/share/classes/jdk/internal/io/JdkConsoleImpl.java +++ b/src/java.base/share/classes/jdk/internal/io/JdkConsoleImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -30,21 +30,15 @@ import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileOutputStream; -import java.io.PrintStream; import java.io.PrintWriter; import java.io.Reader; import java.io.Writer; import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Arrays; import java.util.Formatter; -import java.util.List; import java.util.Locale; import java.util.Objects; -import java.util.ServiceLoader; import jdk.internal.access.SharedSecrets; -import jdk.internal.loader.ClassLoaders; import sun.nio.cs.StreamDecoder; import sun.nio.cs.StreamEncoder; @@ -174,14 +168,15 @@ public char[] readPassword(Locale locale, String format, Object ... args) { ioe.addSuppressed(x); } if (ioe != null) { - Arrays.fill(passwd, ' '); - try { - if (reader instanceof LineReader lr) { - lr.zeroOut(); - } - } catch (IOException _) { - // ignore - } + //TODO: +// Arrays.fill(passwd, ' '); +// try { +// if (reader instanceof LineReader lr) { +// lr.zeroOut(); +// } +// } catch (IOException _) { +// // ignore +// } throw ioe; } } @@ -240,145 +235,14 @@ public Charset charset() { private final Object restoreEchoLock; private final Reader reader; private final Writer out; + private final NativeConsoleReader nativeConsoleReader; private final PrintWriter pw; private final Formatter formatter; - private char[] rcb; private boolean restoreEcho; private boolean shutdownHookInstalled; private char[] readline(boolean zeroOut) throws IOException { - JdkConsoleImpl.class.getModule().addUses(ConsoleReader.class); - for (ConsoleReader consoleReader : ServiceLoader.load(ConsoleReader.class, ClassLoaders.appClassLoader())) { - return consoleReader.readline(zeroOut); - } - - Attributes originalAttributes = CLibrary.getAttributes(0); - Attributes rawAttributes = new Attributes(originalAttributes); - rawAttributes.setInputFlag(Attributes.InputFlag.BRKINT, false); - rawAttributes.setInputFlag(Attributes.InputFlag.IGNPAR, false); - rawAttributes.setInputFlag(Attributes.InputFlag.ICRNL, false); - rawAttributes.setInputFlag(Attributes.InputFlag.IXON, false); - rawAttributes.setInputFlag(Attributes.InputFlag.IXOFF, true); - rawAttributes.setInputFlag(Attributes.InputFlag.IMAXBEL, false); - rawAttributes.setLocalFlag(Attributes.LocalFlag.ICANON, false); - rawAttributes.setLocalFlag(Attributes.LocalFlag.ECHO, false); - Thread restoreConsole = new Thread(() -> { - CLibrary.setAttributes(0, originalAttributes); - }); - try { - Runtime.getRuntime().addShutdownHook(restoreConsole); - CLibrary.setAttributes(0, rawAttributes); - return doRead(reader, System.out); - } finally { - restoreConsole.run(); - Runtime.getRuntime().removeShutdownHook(restoreConsole); - } - } - - //public, to simplify access from tests: - public static char[] doRead(Reader reader, PrintStream out) throws IOException { - StringBuilder result = new StringBuilder(); - int caret = 0; - int r; - READ: while (true) { - //paint: - out.print("\r"); - out.print(result); - out.print("\033[J"); - for (int i = result.length(); i > caret; i--) { - out.print("\033[D"); - } - - //read - r = reader.read(); - switch (r) { - case -1: continue READ; - case '\r': break READ; - case 4: break READ; //EOF/Ctrl-D - case 127: - if (caret > 0) { - result.delete(caret - 1, caret); - caret--; - } - continue READ; - case '\033': - r = reader.read(); - switch (r) { - case '[': - r = reader.read(); - - StringBuilder firstNumber = new StringBuilder(); - - r = readNumber(reader, r, firstNumber); - - String modifier; - String key; - - switch (r) { - case '~' -> { - key = firstNumber.toString(); - modifier = "1"; - } - case ';' -> { - key = firstNumber.toString(); - - StringBuilder modifierBuilder = new StringBuilder(); - - r = readNumber(reader, r, modifierBuilder); - modifier = modifierBuilder.toString(); - - if (r != '~') { - //TODO: unexpected, anything that can be done? - } - } - default -> { - key = Character.toString(r); - modifier = firstNumber.isEmpty() ? "1" - : firstNumber.toString(); - } - } - - if ("1".equals(modifier)) { - switch (key) { - case "C": if (caret < result.length()) caret++; break; - case "D": if (caret > 0) caret--; break; - case "1", "H": caret = 0; break; - case "4", "F": caret = result.length(); break; - case "3": - //delete - result.delete(caret, caret + 1); - continue READ; - } - } - } - continue READ; - } - - result.insert(caret, (char) r); - caret++; - } - - //show the final state: - out.print("\r"); - out.println(result); - - return result.toString().toCharArray(); - } - - private static int readNumber(Reader reader, int r, StringBuilder number) throws IOException { - while (Character.isDigit(r)) { - number.append((char) r); - r = reader.read(); - } - return r; - } - - private char[] grow() { - assert Thread.holdsLock(readLock); - char[] t = new char[rcb.length * 2]; - System.arraycopy(rcb, 0, t, 0, rcb.length); - rcb = t; - return rcb; + return nativeConsoleReader.readline(reader, out, zeroOut); } /* @@ -390,122 +254,6 @@ private char[] grow() { */ private static native boolean echo(boolean on) throws IOException; - class LineReader extends Reader { - private final Reader in; - private final char[] cb; - private int nChars, nextChar; - boolean leftoverLF; - LineReader(Reader in) { - this.in = in; - cb = new char[1024]; - nextChar = nChars = 0; - leftoverLF = false; - } - public void zeroOut() throws IOException { - if (in instanceof StreamDecoder sd) { - sd.fillZeroToPosition(); - } - } - public void close () {} - public boolean ready() throws IOException { - //in.ready synchronizes on readLock already - return in.ready(); - } - - public int read(char[] cbuf, int offset, int length) - throws IOException - { - int off = offset; - int end = offset + length; - if (offset < 0 || offset > cbuf.length || length < 0 || - end < 0 || end > cbuf.length) { - throw new IndexOutOfBoundsException(); - } - synchronized(readLock) { - boolean eof = false; - char c; - for (;;) { - if (nextChar >= nChars) { //fill - int n; - do { - n = in.read(cb, 0, cb.length); - } while (n == 0); - if (n > 0) { - nChars = n; - nextChar = 0; - if (n < cb.length && - cb[n-1] != '\n' && cb[n-1] != '\r') { - /* - * we're in canonical mode so each "fill" should - * come back with an eol. if there is no lf or nl at - * the end of returned bytes we reached an eof. - */ - eof = true; - } - } else { /*EOF*/ - if (off - offset == 0) - return -1; - return off - offset; - } - } - if (leftoverLF && cbuf == rcb && cb[nextChar] == '\n') { - /* - * if invoked by our readline, skip the leftover, otherwise - * return the LF. - */ - nextChar++; - } - leftoverLF = false; - while (nextChar < nChars) { - c = cbuf[off++] = cb[nextChar]; - cb[nextChar++] = 0; - if (c == '\n') { - return off - offset; - } else if (c == '\r') { - if (off == end) { - /* no space left even the next is LF, so return - * whatever we have if the invoker is not our - * readLine() - */ - if (cbuf == rcb) { - cbuf = grow(); - } else { - leftoverLF = true; - return off - offset; - } - } - if (nextChar == nChars && in.ready()) { - /* - * we have a CR and we reached the end of - * the read in buffer, fill to make sure we - * don't miss a LF, if there is one, it's possible - * that it got cut off during last round reading - * simply because the read in buffer was full. - */ - nChars = in.read(cb, 0, cb.length); - nextChar = 0; - } - if (nextChar < nChars && cb[nextChar] == '\n') { - cbuf[off++] = '\n'; - nextChar++; - } - return off - offset; - } else if (off == end) { - if (cbuf == rcb) { - cbuf = grow(); - end = cbuf.length; - } else { - return off - offset; - } - } - } - if (eof) - return off - offset; - } - } - } - } - public JdkConsoleImpl(Charset charset) { Objects.requireNonNull(charset); this.charset = charset; @@ -525,11 +273,7 @@ public void close() { new FileInputStream(FileDescriptor.in), readLock, charset);//); - rcb = new char[1024]; + nativeConsoleReader = NativeConsoleReaderImpl.create(readLock); } - //this is mostly setup to allow a more convenient Window backend development: - public interface ConsoleReader { - public char[] readline(boolean zeroout) throws IOException; - } } diff --git a/src/java.base/share/classes/jdk/internal/io/NativeConsoleReader.java b/src/java.base/share/classes/jdk/internal/io/NativeConsoleReader.java new file mode 100644 index 0000000000000..ee0b75868cd59 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/io/NativeConsoleReader.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.io; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; + +public interface NativeConsoleReader { + + public char[] readline(Reader reader, Writer out, boolean zeroOut) throws IOException; + +} diff --git a/src/java.base/share/classes/jdk/internal/io/SimpleConsoleReader.java b/src/java.base/share/classes/jdk/internal/io/SimpleConsoleReader.java new file mode 100644 index 0000000000000..820259212473f --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/io/SimpleConsoleReader.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.io; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.IntSupplier; + +public class SimpleConsoleReader { + + //public, to simplify access from tests: + public static char[] doRead(Reader reader, Writer out, int firstLineOffset, IntSupplier terminalWidthSupplier) throws IOException { + StringBuilder result = new StringBuilder(); + int caret = 0; + int r; + PaintState prevState = new PaintState(); + + READ: while (true) { + //paint: + if (firstLineOffset != (-1)) { + prevState = repaint(out, firstLineOffset, terminalWidthSupplier, result.toString(), caret, prevState); + } + + //read + r = reader.read(); + switch (r) { + case -1: continue READ; + case '\r': break READ; + case 4: break READ; //EOF/Ctrl-D + case 127: + if (caret > 0) { + result.delete(caret - 1, caret); + caret--; + } + continue READ; + case '\033': + r = reader.read(); + switch (r) { + case '[': + r = reader.read(); + + StringBuilder firstNumber = new StringBuilder(); + + r = readNumber(reader, r, firstNumber); + + String modifier; + String key; + + switch (r) { + case '~' -> { + key = firstNumber.toString(); + modifier = "1"; + } + case ';' -> { + key = firstNumber.toString(); + + StringBuilder modifierBuilder = new StringBuilder(); + + r = readNumber(reader, reader.read(), modifierBuilder); + modifier = modifierBuilder.toString(); + + if (r == 'R') { + firstLineOffset = Integer.parseInt(modifier) - 1; + continue READ; + } + + if (r != '~') { + //TODO: unexpected, anything that can be done? + } + } + default -> { + key = Character.toString(r); + modifier = firstNumber.isEmpty() ? "1" + : firstNumber.toString(); + } + } + + if ("1".equals(modifier)) { + switch (key) { + case "C": if (caret < result.length()) caret++; break; + case "D": if (caret > 0) caret--; break; + case "1", "H": caret = 0; break; + case "4", "F": caret = result.length(); break; + case "3": + //delete + result.delete(caret, caret + 1); + continue READ; + } + } + } + continue READ; + } + + result.insert(caret, (char) r); + caret++; + } + + //show the final state: + repaint(out, firstLineOffset, terminalWidthSupplier, result.toString(), caret, prevState); + + out.append("\n\r").flush(); + + return result.toString().toCharArray(); + } + + private static PaintState repaint(Writer out, int firstLineOffset, IntSupplier terminalWidthSupplier, String toDisplay, int caret, PaintState prevPaintState) throws IOException { + //TODO: compute smaller (ideally minimal) changes, and apply them instead of repainting everything: + int terminalWidth = terminalWidthSupplier.getAsInt(); + List toDisplayLines = new ArrayList<>(); + int lineOffset = firstLineOffset; + + String remaining = toDisplay; + + while (remaining.length() > terminalWidth - lineOffset) { + toDisplayLines.add(remaining.substring(0, terminalWidth - lineOffset)); + remaining = remaining.substring(terminalWidth - lineOffset); + lineOffset = 0; + } + + toDisplayLines.add(remaining); + + for (int i = prevPaintState.caretLine() + 1; i < prevPaintState.lines(); i++) { + out.append("\033[B"); + } + for (int i = prevPaintState.lines() - 1; i >= 0; i--) { + if (i == 0) { + out.append("\033[" + (firstLineOffset + 1) + "G"); + out.append("\033[K"); + } else { + out.append("\r"); + out.append("\033[K"); + out.append("\033[A"); + } + } + for (Iterator it = toDisplayLines.iterator(); it.hasNext();) { + String line = it.next(); + out.append(line); + if (it.hasNext()) { + out.append("\n\r"); + } + } + + int prevCaretLine = prevPaintState.lines(); + + if (caret < toDisplay.length()) { + int currentPos = toDisplay.length(); + + prevCaretLine = prevPaintState.lines() - 1; + + while (caret < currentPos - toDisplayLines.get(prevCaretLine).length()) { + out.append("\033[A"); + currentPos -= toDisplayLines.get(prevCaretLine).length(); + prevCaretLine--; + } + + int currentLineStart = currentPos - toDisplayLines.get(prevCaretLine).length(); + int linePosition = caret - currentLineStart; + + if (prevCaretLine == 0) { + linePosition += firstLineOffset; + } + + out.append("\033[" + (linePosition + 1) + "G"); + } + out.flush(); + return new PaintState(toDisplayLines.size(), prevCaretLine); + } + + private static int readNumber(Reader reader, int r, StringBuilder number) throws IOException { + while (Character.isDigit(r)) { + number.append((char) r); + r = reader.read(); + } + return r; + } + + public record Size(int width, int height) {} + record PaintState(int lines, int caretLine) { + + public PaintState() { + this(1, 0); + } + + } + +} diff --git a/src/java.base/unix/classes/jdk/internal/io/NativeConsoleReaderImpl.java b/src/java.base/unix/classes/jdk/internal/io/NativeConsoleReaderImpl.java new file mode 100644 index 0000000000000..18488413667d9 --- /dev/null +++ b/src/java.base/unix/classes/jdk/internal/io/NativeConsoleReaderImpl.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.io; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import jdk.internal.io.SimpleConsoleReader.Size; + +public class NativeConsoleReaderImpl implements NativeConsoleReader { + + public static NativeConsoleReader create(Object readLock) { + if (CLibrary.isTty(0)) { + return new NativeConsoleReaderImpl(); + } else { + return new BaseNativeConsoleReader(readLock); + } + } + + //TODO: read lock + public char[] readline(Reader reader, Writer out, boolean zeroOut) throws IOException { + Attributes originalAttributes = CLibrary.getAttributes(0); + Attributes rawAttributes = new Attributes(originalAttributes); + rawAttributes.setInputFlag(Attributes.InputFlag.BRKINT, false); + rawAttributes.setInputFlag(Attributes.InputFlag.IGNPAR, false); + rawAttributes.setInputFlag(Attributes.InputFlag.ICRNL, false); + rawAttributes.setInputFlag(Attributes.InputFlag.IXON, false); + rawAttributes.setInputFlag(Attributes.InputFlag.IXOFF, true); + rawAttributes.setInputFlag(Attributes.InputFlag.IMAXBEL, false); + rawAttributes.setLocalFlag(Attributes.LocalFlag.ICANON, false); + rawAttributes.setLocalFlag(Attributes.LocalFlag.ECHO, false); + Thread restoreConsole = new Thread(() -> { + CLibrary.setAttributes(0, originalAttributes); + }); + try { + Runtime.getRuntime().addShutdownHook(restoreConsole); + CLibrary.setAttributes(0, rawAttributes); + Size size = CLibrary.getTerminalSize(0); + out.append("\033[6n").flush(); //ask the terminal to provide cursor location + return SimpleConsoleReader.doRead(reader, out, -1, () -> size.width()); + } finally { + restoreConsole.run(); + Runtime.getRuntime().removeShutdownHook(restoreConsole); + } + } + +} diff --git a/src/java.base/windows/classes/jdk/internal/io/Kernel32.java b/src/java.base/windows/classes/jdk/internal/io/Kernel32.java new file mode 100644 index 0000000000000..ae3ba7fd2c179 --- /dev/null +++ b/src/java.base/windows/classes/jdk/internal/io/Kernel32.java @@ -0,0 +1,942 @@ +/* + * 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 jdk.internal.io; + +import java.io.IOException; +import java.lang.foreign.MemoryLayout; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +@SuppressWarnings({"unused", "restricted"}) +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 SHIFT_FLAG = 0x01; + public static final int ALT_FLAG = 0x02; + public static final int CTRL_FLAG = 0x04; + + 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.seg, lpClipRectangle.seg, dwDestinationOrigin.seg, lpFill.seg); + } 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.allocate(INPUT_RECORD.LAYOUT, count); + java.lang.foreign.MemorySegment length = arena.allocate(java.lang.foreign.ValueLayout.JAVA_INT, 1); + 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 lookupVarHandle( + layout, java.lang.foreign.MemoryLayout.PathElement.groupElement(name)); + } + + static VarHandle varHandle(java.lang.foreign.MemoryLayout layout, String e1, String name) { + return lookupVarHandle( + layout, + 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)); + } + + static VarHandle lookupVarHandle(MemoryLayout layout, MemoryLayout.PathElement... element) { + VarHandle h = layout.varHandle(element); + + // the last parameter of the VarHandle is additional offset, hardcode zero: + h = MethodHandles.insertCoordinates(h, h.coordinateTypes().size() - 1, 0L); + + return h; + } +} diff --git a/src/java.base/windows/classes/jdk/internal/io/NativeConsoleReaderImpl.java b/src/java.base/windows/classes/jdk/internal/io/NativeConsoleReaderImpl.java new file mode 100644 index 0000000000000..ac3e4042183aa --- /dev/null +++ b/src/java.base/windows/classes/jdk/internal/io/NativeConsoleReaderImpl.java @@ -0,0 +1,254 @@ +package jdk.internal.io; + +import java.io.Reader; +import java.io.Writer; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.util.concurrent.atomic.AtomicInteger; +import static jdk.internal.io.Kernel32.ALT_FLAG; +import jdk.internal.io.Kernel32.CONSOLE_SCREEN_BUFFER_INFO; +import static jdk.internal.io.Kernel32.CTRL_FLAG; +import jdk.internal.io.Kernel32.INPUT_RECORD; +import static jdk.internal.io.Kernel32.INVALID_HANDLE_VALUE; +import static jdk.internal.io.Kernel32.KEY_EVENT; +import jdk.internal.io.Kernel32.KEY_EVENT_RECORD; +import static jdk.internal.io.Kernel32.LEFT_ALT_PRESSED; +import static jdk.internal.io.Kernel32.LEFT_CTRL_PRESSED; +import static jdk.internal.io.Kernel32.RIGHT_ALT_PRESSED; +import static jdk.internal.io.Kernel32.RIGHT_CTRL_PRESSED; +import static jdk.internal.io.Kernel32.SHIFT_FLAG; +import static jdk.internal.io.Kernel32.SHIFT_PRESSED; +import static jdk.internal.io.Kernel32.WINDOW_BUFFER_SIZE_EVENT; + +//partly based on JLine: +/* + * 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 + */ + +public class NativeConsoleReaderImpl implements NativeConsoleReader { + + public static NativeConsoleReader create(Object readLock) { + if (CLibrary.isTty(0)) { + return new NativeConsoleReaderImpl(); + } else { + return new BaseNativeConsoleReader(readLock); + } + } + + private static final int ENABLE_PROCESSED_INPUT = 0x0001; //for input + private static final int ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200; //for input + private static final int ENABLE_PROCESSED_OUTPUT = 0x0001; //for output + private static final int ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004; //for output + + @Override + public char[] readline(Reader reader, Writer out, boolean zeroOut) throws IOException { + try (Arena arena = Arena.ofConfined()) { + MemorySegment inConsole = Kernel32.GetStdHandle(Kernel32.STD_INPUT_HANDLE); + MemorySegment originalInModeRef = arena.allocate(java.lang.foreign.ValueLayout.JAVA_INT); + Kernel32.GetConsoleMode(inConsole, originalInModeRef); + int originalInMode = originalInModeRef.get(java.lang.foreign.ValueLayout.JAVA_INT, 0); + Kernel32.SetConsoleMode(inConsole, ENABLE_PROCESSED_INPUT); + MemorySegment outConsole = Kernel32.GetStdHandle(Kernel32.STD_OUTPUT_HANDLE); + MemorySegment originalOutModeRef = arena.allocate(java.lang.foreign.ValueLayout.JAVA_INT); + Kernel32.GetConsoleMode(outConsole, originalOutModeRef); + int originalOutMode = originalOutModeRef.get(java.lang.foreign.ValueLayout.JAVA_INT, 0); + Kernel32.SetConsoleMode(outConsole, ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_PROCESSED_OUTPUT); + CONSOLE_SCREEN_BUFFER_INFO consoleInfo = new Kernel32.CONSOLE_SCREEN_BUFFER_INFO(arena); + Kernel32.GetConsoleScreenBufferInfo(outConsole, consoleInfo); + try { + AtomicInteger width = new AtomicInteger(consoleInfo.size().x()); + int firstLineOffset = consoleInfo.cursorPosition().x(); + InputStream in = new ConsoleInputStream(inConsole, () -> { + Kernel32.GetConsoleScreenBufferInfo(outConsole, consoleInfo); + width.set(consoleInfo.size().x()); + }); + return SimpleConsoleReader.doRead(new InputStreamReader(in), out, firstLineOffset, () -> width.get()); + } finally { + Kernel32.SetConsoleMode(inConsole, originalInMode); + Kernel32.SetConsoleMode(outConsole, originalOutMode); + } + } + } + + private static final class ConsoleInputStream extends InputStream { + + private final MemorySegment inConsole; + private final Runnable refreshWidth; + private int[] backlog; + private int backlogIndex; + + public ConsoleInputStream(MemorySegment inConsole, Runnable refreshWidth) { + this.inConsole = inConsole; + this.refreshWidth = refreshWidth; + } + + @Override + public int read() throws IOException { + while (backlog == null || backlogIndex >= backlog.length) { + try (Arena arena = Arena.ofConfined()) { + INPUT_RECORD[] events; + if (inConsole != null + && inConsole.address() != INVALID_HANDLE_VALUE + && Kernel32.WaitForSingleObject(inConsole, 100) == 0) { //TODO: 100ms timeout sensible? + events = Kernel32.readConsoleInputHelper(arena, inConsole, 1, false); + } else { + return -1; + } + + 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()); + } else if (eventType == WINDOW_BUFFER_SIZE_EVENT) { + refreshWidth.run(); + return -1; //repaint - can be better? + } + } + } + } + return backlog[backlogIndex++]; + } + + protected void processKeyEvent( + final boolean isKeyDown, final short virtualKeyCode, char ch, final int controlKeyState) + throws IOException { + StringBuilder data = new StringBuilder(); + final boolean isCtrl = (controlKeyState & (RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED)) > 0; + final boolean isAlt = (controlKeyState & (RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED)) > 0; + final boolean isShift = (controlKeyState & SHIFT_PRESSED) > 0; + // key down event + if (isKeyDown && ch != '\3') { + // Pressing "Alt Gr" is translated to Alt-Ctrl, hence it has to be checked that Ctrl is _not_ pressed, + // otherwise inserting of "Alt Gr" codes on non-US keyboards would yield errors + if (ch != 0 + && (controlKeyState + & (RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED | RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED)) + == (RIGHT_ALT_PRESSED | LEFT_CTRL_PRESSED)) { + data.append(ch); + } else { + final String keySeq = getEscapeSequence( + virtualKeyCode, (isCtrl ? CTRL_FLAG : 0) + (isAlt ? ALT_FLAG : 0) + (isShift ? SHIFT_FLAG : 0)); + if (keySeq != null) { + data.append(keySeq); + } else { + /* uchar value in Windows when CTRL is pressed: + * 1). Ctrl + <0x41 to 0x5e> : uchar= - 'A' + 1 + * 2). Ctrl + Backspace(0x08) : uchar=0x7f + * 3). Ctrl + Enter(0x0d) : uchar=0x0a + * 4). Ctrl + Space(0x20) : uchar=0x20 + * 5). Ctrl + : uchar=0 + * 6). Ctrl + Alt + : uchar=0 + */ + if (ch > 0) { + if (isAlt) { + data.append('\033'); + } + if (isCtrl && ch != ' ' && ch != '\n' && ch != 0x7f) { + data.append((char) (ch == '?' ? 0x7f : Character.toUpperCase(ch) & 0x1f)); + } else if (isCtrl && ch == '\n') { + //simulate Alt-Enter: + data.append('\033'); + data.append('\r'); + } else { + data.append(ch); + } + } else if (isCtrl) { // Handles the ctrl key events(uchar=0) + if (virtualKeyCode >= 'A' && virtualKeyCode <= 'Z') { + ch = (char) (virtualKeyCode - 0x40); + } else if (virtualKeyCode == 191) { // ? + ch = 127; + } + if (ch > 0) { + if (isAlt) { + data.append('\033'); + } + data.append(ch); + } + } + } + } + } else if (isKeyDown && ch == '\3') { + data.append('\3'); + } + // key up event + else { + // support ALT+NumPad input method + if (virtualKeyCode == 0x12 /*VK_MENU ALT key*/ && ch > 0) { + data.append(ch); // no such combination in Windows + } + } + backlog = new int[data.length()]; + for (int i = 0; i < data.length(); i++) { + backlog[i] = data.charAt(i); + } + backlogIndex = 0; + } + + protected String getEscapeSequence(short keyCode, int keyState) { + // virtual keycodes: http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx + // TODO: numpad keys, modifiers + String escapeSequence = null; + switch (keyCode) { + case 0x08: // VK_BACK BackSpace + escapeSequence = "\u007F"; + break; + case 0x09: + return null; + case 0x23: // VK_END + escapeSequence = "\033[F"; + break; + case 0x24: // VK_HOME + escapeSequence = "\033[H"; + break; + case 0x25: // VK_LEFT + escapeSequence = "\033[D"; + break; + case 0x27: // VK_RIGHT + escapeSequence = "\033[C"; + break; + case 0x2E: // VK_DELETE + escapeSequence = "\033[3~"; + break; + case 0x21: // VK_PRIOR PageUp + case 0x22: // VK_NEXT PageDown + case 0x26: // VK_UP + case 0x28: // VK_DOWN + case 0x2D: // VK_INSERT + + case 0x70: // VK_F1 + case 0x71: // VK_F2 + case 0x72: // VK_F3 + case 0x73: // VK_F4 + case 0x74: // VK_F5 + case 0x75: // VK_F6 + case 0x76: // VK_F7 + case 0x77: // VK_F8 + case 0x78: // VK_F9 + case 0x79: // VK_F10 + case 0x7A: // VK_F11 + case 0x7B: // VK_F12 + return ""; + case 0x5D: // VK_CLOSE_BRACKET(Menu key) + case 0x5B: // VK_OPEN_BRACKET(Window key) + default: + return null; + } + if (keyState != 0) { + //with modifiers - ignore: + return ""; + } + return escapeSequence; + } + } + +}