Skip to content

Commit

Permalink
Fix terminal width support on MINGW (fixes #233) (#264)
Browse files Browse the repository at this point in the history
  • Loading branch information
gnodet authored Oct 2, 2023
1 parent 764a422 commit ffd1688
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 2 deletions.
11 changes: 10 additions & 1 deletion src/main/java/org/fusesource/jansi/AnsiConsole.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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...
Expand Down
15 changes: 14 additions & 1 deletion src/main/java/org/fusesource/jansi/AnsiMain.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
137 changes: 137 additions & 0 deletions src/main/java/org/fusesource/jansi/internal/MingwSupport.java
Original file line number Diff line number Diff line change
@@ -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;
}
}

0 comments on commit ffd1688

Please sign in to comment.