From ffd16888db1f9c5406648e62e0d5f57a03e3eb1d Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 2 Oct 2023 16:44:11 +0200 Subject: [PATCH] Fix terminal width support on MINGW (fixes #233) (#264) --- .../org/fusesource/jansi/AnsiConsole.java | 11 +- .../java/org/fusesource/jansi/AnsiMain.java | 15 +- .../jansi/internal/MingwSupport.java | 137 ++++++++++++++++++ 3 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/fusesource/jansi/internal/MingwSupport.java diff --git a/src/main/java/org/fusesource/jansi/AnsiConsole.java b/src/main/java/org/fusesource/jansi/AnsiConsole.java index dbe9d268..749c39cb 100644 --- a/src/main/java/org/fusesource/jansi/AnsiConsole.java +++ b/src/main/java/org/fusesource/jansi/AnsiConsole.java @@ -25,6 +25,7 @@ import java.nio.charset.Charset; import java.nio.charset.UnsupportedCharsetException; +import org.fusesource.jansi.internal.MingwSupport; import org.fusesource.jansi.internal.OSInfo; import org.fusesource.jansi.io.AnsiOutputStream; import org.fusesource.jansi.io.AnsiProcessor; @@ -285,11 +286,19 @@ private static AnsiPrintStream ansiStream(boolean stdout) { getKernel32().setConsoleMode(console, mode[0]); } }; + width = () -> getKernel32().getTerminalWidth(console); } else if ((IS_CONEMU || IS_CYGWIN || IS_MSYSTEM) && !isConsole) { // ANSI-enabled ConEmu, Cygwin or MSYS(2) on Windows... processor = null; type = AnsiType.Native; installer = uninstaller = null; + MingwSupport mingw = new MingwSupport(); + String name = mingw.getConsoleName(stdout); + if (name != null && !name.isEmpty()) { + width = () -> mingw.getTerminalWidth(name); + } else { + width = () -> -1; + } } else { // On Windows, when no ANSI-capable terminal is used, we know the console does not natively interpret // ANSI @@ -308,8 +317,8 @@ private static AnsiPrintStream ansiStream(boolean stdout) { processor = proc; type = ttype; installer = uninstaller = null; + width = () -> getKernel32().getTerminalWidth(console); } - width = () -> getKernel32().getTerminalWidth(console); } // We must be on some Unix variant... diff --git a/src/main/java/org/fusesource/jansi/AnsiMain.java b/src/main/java/org/fusesource/jansi/AnsiMain.java index a3518ad1..bb6ab3cc 100644 --- a/src/main/java/org/fusesource/jansi/AnsiMain.java +++ b/src/main/java/org/fusesource/jansi/AnsiMain.java @@ -28,6 +28,7 @@ import org.fusesource.jansi.Ansi.Attribute; import org.fusesource.jansi.internal.JansiLoader; +import org.fusesource.jansi.internal.MingwSupport; import static java.nio.charset.StandardCharsets.UTF_8; import static org.fusesource.jansi.Ansi.ansi; @@ -205,7 +206,19 @@ private static void diagnoseTty(boolean stderr) { if (AnsiConsole.IS_WINDOWS) { long console = AnsiConsoleSupport.getInstance().getKernel32().getStdHandle(!stderr); isatty = AnsiConsoleSupport.getInstance().getKernel32().isTty(console); - width = AnsiConsoleSupport.getInstance().getKernel32().getTerminalWidth(console); + if ((AnsiConsole.IS_CONEMU || AnsiConsole.IS_CYGWIN || AnsiConsole.IS_MSYSTEM) && isatty == 0) { + MingwSupport mingw = new MingwSupport(); + String name = mingw.getConsoleName(!stderr); + if (name != null && !name.isEmpty()) { + isatty = 1; + width = mingw.getTerminalWidth(name); + } else { + isatty = 0; + width = 0; + } + } else { + width = AnsiConsoleSupport.getInstance().getKernel32().getTerminalWidth(console); + } } else { int fd = stderr ? AnsiConsoleSupport.CLibrary.STDERR_FILENO : AnsiConsoleSupport.CLibrary.STDOUT_FILENO; isatty = AnsiConsoleSupport.getInstance().getCLibrary().isTty(fd); diff --git a/src/main/java/org/fusesource/jansi/internal/MingwSupport.java b/src/main/java/org/fusesource/jansi/internal/MingwSupport.java new file mode 100644 index 00000000..be0c54a2 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/MingwSupport.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2009-2023 the original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.fusesource.jansi.internal; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Support for MINGW terminals. + * Those terminals do not use the underlying windows terminal and there's no CLibrary available + * in these environments. We have to rely on calling {@code stty.exe} and {@code tty.exe} to + * obtain the terminal name and width. + */ +public class MingwSupport { + + private final String sttyCommand; + private final String ttyCommand; + private final Pattern columnsPatterns; + + public MingwSupport() { + String tty = null; + String stty = null; + String path = System.getenv("PATH"); + if (path != null) { + String[] paths = path.split(File.pathSeparator); + for (String p : paths) { + File ttyFile = new File(p, "tty.exe"); + if (tty == null && ttyFile.canExecute()) { + tty = ttyFile.getAbsolutePath(); + } + File sttyFile = new File(p, "stty.exe"); + if (stty == null && sttyFile.canExecute()) { + stty = sttyFile.getAbsolutePath(); + } + } + } + if (tty == null) { + tty = "tty.exe"; + } + if (stty == null) { + stty = "stty.exe"; + } + ttyCommand = tty; + sttyCommand = stty; + // Compute patterns + columnsPatterns = Pattern.compile("\\b" + "columns" + "\\s+(\\d+)\\b"); + } + + public String getConsoleName(boolean stdout) { + try { + Process p = new ProcessBuilder(ttyCommand) + .redirectInput(getRedirect(stdout ? FileDescriptor.out : FileDescriptor.err)) + .start(); + String result = waitAndCapture(p); + if (p.exitValue() == 0) { + return result.trim(); + } + } catch (Throwable t) { + if ("java.lang.reflect.InaccessibleObjectException" + .equals(t.getClass().getName())) { + System.err.println("MINGW support requires --add-opens java.base/java.lang=ALL-UNNAMED"); + } + // ignore + } + return null; + } + + public int getTerminalWidth(String name) { + try { + Process p = new ProcessBuilder(sttyCommand, "-F", name, "-a").start(); + String result = waitAndCapture(p); + if (p.exitValue() != 0) { + throw new IOException("Error executing '" + sttyCommand + "': " + result); + } + Matcher matcher = columnsPatterns.matcher(result); + if (matcher.find()) { + return Integer.parseInt(matcher.group(1)); + } + throw new IOException("Unable to parse columns"); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static String waitAndCapture(Process p) throws IOException, InterruptedException { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + try (InputStream in = p.getInputStream(); + InputStream err = p.getErrorStream()) { + int c; + while ((c = in.read()) != -1) { + bout.write(c); + } + while ((c = err.read()) != -1) { + bout.write(c); + } + p.waitFor(); + } + return bout.toString(); + } + + /** + * This requires --add-opens java.base/java.lang=ALL-UNNAMED + */ + private ProcessBuilder.Redirect getRedirect(FileDescriptor fd) throws ReflectiveOperationException { + // This is not really allowed, but this is the only way to redirect the output or error stream + // to the input. This is definitely not something you'd usually want to do, but in the case of + // the `tty` utility, it provides a way to get + Class rpi = Class.forName("java.lang.ProcessBuilder$RedirectPipeImpl"); + Constructor cns = rpi.getDeclaredConstructor(); + cns.setAccessible(true); + ProcessBuilder.Redirect input = (ProcessBuilder.Redirect) cns.newInstance(); + Field f = rpi.getDeclaredField("fd"); + f.setAccessible(true); + f.set(input, fd); + return input; + } +}