Skip to content

Commit

Permalink
Rework fix of terminal width support on MINGW (fixes fusesource#290)
Browse files Browse the repository at this point in the history
  • Loading branch information
rosti-il committed Jul 2, 2024
1 parent 045fd56 commit 59db172
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 123 deletions.
24 changes: 3 additions & 21 deletions src/main/java/org/fusesource/jansi/AnsiConsole.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,13 @@
*/
package org.fusesource.jansi;

import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOError;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Locale;

import org.fusesource.jansi.internal.CLibrary;
import org.fusesource.jansi.internal.CLibrary.WinSize;
import org.fusesource.jansi.internal.Kernel32.CONSOLE_SCREEN_BUFFER_INFO;
import org.fusesource.jansi.internal.MingwSupport;
import org.fusesource.jansi.io.AnsiOutputStream;
import org.fusesource.jansi.io.AnsiProcessor;
Expand All @@ -37,12 +30,7 @@

import static org.fusesource.jansi.internal.CLibrary.ioctl;
import static org.fusesource.jansi.internal.CLibrary.isatty;
import static org.fusesource.jansi.internal.Kernel32.GetConsoleMode;
import static org.fusesource.jansi.internal.Kernel32.GetConsoleScreenBufferInfo;
import static org.fusesource.jansi.internal.Kernel32.GetStdHandle;
import static org.fusesource.jansi.internal.Kernel32.STD_ERROR_HANDLE;
import static org.fusesource.jansi.internal.Kernel32.STD_OUTPUT_HANDLE;
import static org.fusesource.jansi.internal.Kernel32.SetConsoleMode;
import static org.fusesource.jansi.internal.Kernel32.*;

/**
* Provides consistent access to an ANSI aware console PrintStream or an ANSI codes stripping PrintStream
Expand Down Expand Up @@ -313,13 +301,7 @@ public int getTerminalWidth() {
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;
}
width = () -> MingwSupport.getTerminalWidth().orElse(-1);
} else {
// On Windows, when no ANSI-capable terminal is used, we know the console does not natively interpret
// ANSI
Expand Down
9 changes: 4 additions & 5 deletions src/main/java/org/fusesource/jansi/AnsiMain.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.util.Optional;
import java.util.Properties;

import org.fusesource.jansi.Ansi.Attribute;
Expand Down Expand Up @@ -204,11 +205,10 @@ private static void diagnoseTty(boolean stderr) {
int[] mode = new int[1];
isatty = Kernel32.GetConsoleMode(console, mode);
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()) {
Optional<Integer> terminalWidth = MingwSupport.getTerminalWidth();
if (terminalWidth.isPresent()) {
isatty = 1;
width = mingw.getTerminalWidth(name);
width = terminalWidth.get();
} else {
isatty = 0;
width = 0;
Expand All @@ -232,7 +232,6 @@ private static void diagnoseTty(boolean stderr) {
}

private static void testAnsi(boolean stderr) {
@SuppressWarnings("resource")
PrintStream s = stderr ? System.err : System.out;
s.print("test on System." + (stderr ? "err" : "out") + ":");
for (Ansi.Color c : Ansi.Color.values()) {
Expand Down
133 changes: 36 additions & 97 deletions src/main/java/org/fusesource/jansi/internal/MingwSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,123 +15,62 @@
*/
package org.fusesource.jansi.internal;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
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.
* <p>
* Those terminals do not use the underlying Windows terminal and there's no C library 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;
private static final Pattern COLUMNS_PATTERNS = Pattern.compile("\\d+ (\\d+)");

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) {
public static Optional<Integer> getTerminalWidth() {
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");
Optional<String> terminalName = getTerminalName();
if (terminalName.isPresent()) {
Process sttyProcess = new ProcessBuilder("stty.exe", "-F", terminalName.get(), "size").start();
CharSequence result = waitAndCapture(sttyProcess);
Matcher matcher = COLUMNS_PATTERNS.matcher(result);
if (matcher.find()) {
return Optional.of(Integer.valueOf(matcher.group(1)));
}
}
} catch (Exception e) {
// ignore
}
return null;
return Optional.empty();
}

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 Optional<String> getTerminalName() throws IOException, InterruptedException {
Process ttyProcess = new ProcessBuilder("tty.exe")
.redirectInput(ProcessBuilder.Redirect.INHERIT)
.start();
CharSequence result = waitAndCapture(ttyProcess);
if (ttyProcess.exitValue() == 0) {
return Optional.of(result.toString());
}
return Optional.empty();
}

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);
private static CharSequence waitAndCapture(Process process) throws IOException, InterruptedException {
StringBuilder result = new StringBuilder();
try (BufferedReader br =
new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.US_ASCII))) {
for (String line = br.readLine(); line != null; line = br.readLine()) {
result.append(line);
}
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;
process.waitFor();
return result;
}
}

0 comments on commit 59db172

Please sign in to comment.