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 committed Oct 12, 2023
1 parent fa5bea7 commit 473d6d2
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 10 deletions.
27 changes: 19 additions & 8 deletions src/main/java/org/fusesource/jansi/AnsiConsole.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
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;
import org.fusesource.jansi.io.FastBufferedOutputStream;
Expand Down Expand Up @@ -280,6 +281,15 @@ private static AnsiPrintStream ansiStream(boolean stdout) {
final long console = GetStdHandle(stdout ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE);
final int[] mode = new int[1];
final boolean isConsole = GetConsoleMode(console, mode) != 0;
final AnsiOutputStream.WidthSupplier kernel32Width = new AnsiOutputStream.WidthSupplier() {
@Override
public int getTerminalWidth() {
CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO();
GetConsoleScreenBufferInfo(console, info);
return info.windowWidth();
}
};

if (isConsole && SetConsoleMode(console, mode[0] | ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0) {
SetConsoleMode(console, mode[0]); // set it back for now, but we know it works
processor = null;
Expand All @@ -299,11 +309,19 @@ public void run() throws IOException {
}
}
};
width = kernel32Width;
} 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 @@ -322,15 +340,8 @@ public void run() throws IOException {
processor = proc;
type = ttype;
installer = uninstaller = null;
width = kernel32Width;
}
width = new AnsiOutputStream.WidthSupplier() {
@Override
public int getTerminalWidth() {
CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO();
GetConsoleScreenBufferInfo(console, info);
return info.windowWidth();
}
};
}

// We must be on some Unix variant...
Expand Down
34 changes: 32 additions & 2 deletions src/main/java/org/fusesource/jansi/AnsiMain.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@
import org.fusesource.jansi.Ansi.Attribute;
import org.fusesource.jansi.internal.CLibrary;
import org.fusesource.jansi.internal.JansiLoader;
import org.fusesource.jansi.internal.Kernel32;
import org.fusesource.jansi.internal.MingwSupport;

import static org.fusesource.jansi.Ansi.ansi;
import static org.fusesource.jansi.internal.Kernel32.GetConsoleScreenBufferInfo;

/**
* Main class for the library, providing executable jar to diagnose Jansi setup.
Expand Down Expand Up @@ -192,11 +195,38 @@ private static String getJansiVersion() {
}

private static void diagnoseTty(boolean stderr) {
int fd = stderr ? CLibrary.STDERR_FILENO : CLibrary.STDOUT_FILENO;
int isatty = CLibrary.LOADED ? CLibrary.isatty(fd) : 0;
int isatty;
int width;
if (AnsiConsole.IS_WINDOWS) {
long console = Kernel32.GetStdHandle(stderr ? Kernel32.STD_ERROR_HANDLE : Kernel32.STD_OUTPUT_HANDLE);
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()) {
isatty = 1;
width = mingw.getTerminalWidth(name);
} else {
isatty = 0;
width = 0;
}
} else {
Kernel32.CONSOLE_SCREEN_BUFFER_INFO info = new Kernel32.CONSOLE_SCREEN_BUFFER_INFO();
GetConsoleScreenBufferInfo(console, info);
width = info.windowWidth();
}
} else {
int fd = stderr ? CLibrary.STDERR_FILENO : CLibrary.STDOUT_FILENO;
isatty = CLibrary.LOADED ? CLibrary.isatty(fd) : 0;
CLibrary.WinSize ws = new CLibrary.WinSize();
CLibrary.ioctl(fd, CLibrary.TIOCGWINSZ, ws);
width = ws.ws_col;
}

System.out.println("isatty(STD" + (stderr ? "ERR" : "OUT") + "_FILENO): " + isatty + ", System."
+ (stderr ? "err" : "out") + " " + ((isatty == 0) ? "is *NOT*" : "is") + " a terminal");
System.out.println("width(STD" + (stderr ? "ERR" : "OUT") + "_FILENO): " + width);
}

private static void testAnsi(boolean stderr) {
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 473d6d2

Please sign in to comment.