Skip to content

Commit 473d6d2

Browse files
committedOct 12, 2023
Fix terminal width support on MINGW (fixes #233) (#264)
1 parent fa5bea7 commit 473d6d2

File tree

3 files changed

+188
-10
lines changed

3 files changed

+188
-10
lines changed
 

‎src/main/java/org/fusesource/jansi/AnsiConsole.java

+19-8
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.fusesource.jansi.internal.CLibrary;
3030
import org.fusesource.jansi.internal.CLibrary.WinSize;
3131
import org.fusesource.jansi.internal.Kernel32.CONSOLE_SCREEN_BUFFER_INFO;
32+
import org.fusesource.jansi.internal.MingwSupport;
3233
import org.fusesource.jansi.io.AnsiOutputStream;
3334
import org.fusesource.jansi.io.AnsiProcessor;
3435
import org.fusesource.jansi.io.FastBufferedOutputStream;
@@ -280,6 +281,15 @@ private static AnsiPrintStream ansiStream(boolean stdout) {
280281
final long console = GetStdHandle(stdout ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE);
281282
final int[] mode = new int[1];
282283
final boolean isConsole = GetConsoleMode(console, mode) != 0;
284+
final AnsiOutputStream.WidthSupplier kernel32Width = new AnsiOutputStream.WidthSupplier() {
285+
@Override
286+
public int getTerminalWidth() {
287+
CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO();
288+
GetConsoleScreenBufferInfo(console, info);
289+
return info.windowWidth();
290+
}
291+
};
292+
283293
if (isConsole && SetConsoleMode(console, mode[0] | ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0) {
284294
SetConsoleMode(console, mode[0]); // set it back for now, but we know it works
285295
processor = null;
@@ -299,11 +309,19 @@ public void run() throws IOException {
299309
}
300310
}
301311
};
312+
width = kernel32Width;
302313
} else if ((IS_CONEMU || IS_CYGWIN || IS_MSYSTEM) && !isConsole) {
303314
// ANSI-enabled ConEmu, Cygwin or MSYS(2) on Windows...
304315
processor = null;
305316
type = AnsiType.Native;
306317
installer = uninstaller = null;
318+
MingwSupport mingw = new MingwSupport();
319+
String name = mingw.getConsoleName(stdout);
320+
if (name != null && !name.isEmpty()) {
321+
width = () -> mingw.getTerminalWidth(name);
322+
} else {
323+
width = () -> -1;
324+
}
307325
} else {
308326
// On Windows, when no ANSI-capable terminal is used, we know the console does not natively interpret
309327
// ANSI
@@ -322,15 +340,8 @@ public void run() throws IOException {
322340
processor = proc;
323341
type = ttype;
324342
installer = uninstaller = null;
343+
width = kernel32Width;
325344
}
326-
width = new AnsiOutputStream.WidthSupplier() {
327-
@Override
328-
public int getTerminalWidth() {
329-
CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO();
330-
GetConsoleScreenBufferInfo(console, info);
331-
return info.windowWidth();
332-
}
333-
};
334345
}
335346

336347
// We must be on some Unix variant...

‎src/main/java/org/fusesource/jansi/AnsiMain.java

+32-2
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@
2828
import org.fusesource.jansi.Ansi.Attribute;
2929
import org.fusesource.jansi.internal.CLibrary;
3030
import org.fusesource.jansi.internal.JansiLoader;
31+
import org.fusesource.jansi.internal.Kernel32;
32+
import org.fusesource.jansi.internal.MingwSupport;
3133

3234
import static org.fusesource.jansi.Ansi.ansi;
35+
import static org.fusesource.jansi.internal.Kernel32.GetConsoleScreenBufferInfo;
3336

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

194197
private static void diagnoseTty(boolean stderr) {
195-
int fd = stderr ? CLibrary.STDERR_FILENO : CLibrary.STDOUT_FILENO;
196-
int isatty = CLibrary.LOADED ? CLibrary.isatty(fd) : 0;
198+
int isatty;
199+
int width;
200+
if (AnsiConsole.IS_WINDOWS) {
201+
long console = Kernel32.GetStdHandle(stderr ? Kernel32.STD_ERROR_HANDLE : Kernel32.STD_OUTPUT_HANDLE);
202+
int[] mode = new int[1];
203+
isatty = Kernel32.GetConsoleMode(console, mode);
204+
if ((AnsiConsole.IS_CONEMU || AnsiConsole.IS_CYGWIN || AnsiConsole.IS_MSYSTEM) && isatty == 0) {
205+
MingwSupport mingw = new MingwSupport();
206+
String name = mingw.getConsoleName(!stderr);
207+
if (name != null && !name.isEmpty()) {
208+
isatty = 1;
209+
width = mingw.getTerminalWidth(name);
210+
} else {
211+
isatty = 0;
212+
width = 0;
213+
}
214+
} else {
215+
Kernel32.CONSOLE_SCREEN_BUFFER_INFO info = new Kernel32.CONSOLE_SCREEN_BUFFER_INFO();
216+
GetConsoleScreenBufferInfo(console, info);
217+
width = info.windowWidth();
218+
}
219+
} else {
220+
int fd = stderr ? CLibrary.STDERR_FILENO : CLibrary.STDOUT_FILENO;
221+
isatty = CLibrary.LOADED ? CLibrary.isatty(fd) : 0;
222+
CLibrary.WinSize ws = new CLibrary.WinSize();
223+
CLibrary.ioctl(fd, CLibrary.TIOCGWINSZ, ws);
224+
width = ws.ws_col;
225+
}
197226

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

202232
private static void testAnsi(boolean stderr) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
* Copyright (C) 2009-2023 the original author(s).
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.fusesource.jansi.internal;
17+
18+
import java.io.ByteArrayOutputStream;
19+
import java.io.File;
20+
import java.io.FileDescriptor;
21+
import java.io.IOException;
22+
import java.io.InputStream;
23+
import java.lang.reflect.Constructor;
24+
import java.lang.reflect.Field;
25+
import java.util.regex.Matcher;
26+
import java.util.regex.Pattern;
27+
28+
/**
29+
* Support for MINGW terminals.
30+
* Those terminals do not use the underlying windows terminal and there's no CLibrary available
31+
* in these environments. We have to rely on calling {@code stty.exe} and {@code tty.exe} to
32+
* obtain the terminal name and width.
33+
*/
34+
public class MingwSupport {
35+
36+
private final String sttyCommand;
37+
private final String ttyCommand;
38+
private final Pattern columnsPatterns;
39+
40+
public MingwSupport() {
41+
String tty = null;
42+
String stty = null;
43+
String path = System.getenv("PATH");
44+
if (path != null) {
45+
String[] paths = path.split(File.pathSeparator);
46+
for (String p : paths) {
47+
File ttyFile = new File(p, "tty.exe");
48+
if (tty == null && ttyFile.canExecute()) {
49+
tty = ttyFile.getAbsolutePath();
50+
}
51+
File sttyFile = new File(p, "stty.exe");
52+
if (stty == null && sttyFile.canExecute()) {
53+
stty = sttyFile.getAbsolutePath();
54+
}
55+
}
56+
}
57+
if (tty == null) {
58+
tty = "tty.exe";
59+
}
60+
if (stty == null) {
61+
stty = "stty.exe";
62+
}
63+
ttyCommand = tty;
64+
sttyCommand = stty;
65+
// Compute patterns
66+
columnsPatterns = Pattern.compile("\\b" + "columns" + "\\s+(\\d+)\\b");
67+
}
68+
69+
public String getConsoleName(boolean stdout) {
70+
try {
71+
Process p = new ProcessBuilder(ttyCommand)
72+
.redirectInput(getRedirect(stdout ? FileDescriptor.out : FileDescriptor.err))
73+
.start();
74+
String result = waitAndCapture(p);
75+
if (p.exitValue() == 0) {
76+
return result.trim();
77+
}
78+
} catch (Throwable t) {
79+
if ("java.lang.reflect.InaccessibleObjectException"
80+
.equals(t.getClass().getName())) {
81+
System.err.println("MINGW support requires --add-opens java.base/java.lang=ALL-UNNAMED");
82+
}
83+
// ignore
84+
}
85+
return null;
86+
}
87+
88+
public int getTerminalWidth(String name) {
89+
try {
90+
Process p = new ProcessBuilder(sttyCommand, "-F", name, "-a").start();
91+
String result = waitAndCapture(p);
92+
if (p.exitValue() != 0) {
93+
throw new IOException("Error executing '" + sttyCommand + "': " + result);
94+
}
95+
Matcher matcher = columnsPatterns.matcher(result);
96+
if (matcher.find()) {
97+
return Integer.parseInt(matcher.group(1));
98+
}
99+
throw new IOException("Unable to parse columns");
100+
} catch (Exception e) {
101+
throw new RuntimeException(e);
102+
}
103+
}
104+
105+
private static String waitAndCapture(Process p) throws IOException, InterruptedException {
106+
ByteArrayOutputStream bout = new ByteArrayOutputStream();
107+
try (InputStream in = p.getInputStream();
108+
InputStream err = p.getErrorStream()) {
109+
int c;
110+
while ((c = in.read()) != -1) {
111+
bout.write(c);
112+
}
113+
while ((c = err.read()) != -1) {
114+
bout.write(c);
115+
}
116+
p.waitFor();
117+
}
118+
return bout.toString();
119+
}
120+
121+
/**
122+
* This requires --add-opens java.base/java.lang=ALL-UNNAMED
123+
*/
124+
private ProcessBuilder.Redirect getRedirect(FileDescriptor fd) throws ReflectiveOperationException {
125+
// This is not really allowed, but this is the only way to redirect the output or error stream
126+
// to the input. This is definitely not something you'd usually want to do, but in the case of
127+
// the `tty` utility, it provides a way to get
128+
Class<?> rpi = Class.forName("java.lang.ProcessBuilder$RedirectPipeImpl");
129+
Constructor<?> cns = rpi.getDeclaredConstructor();
130+
cns.setAccessible(true);
131+
ProcessBuilder.Redirect input = (ProcessBuilder.Redirect) cns.newInstance();
132+
Field f = rpi.getDeclaredField("fd");
133+
f.setAccessible(true);
134+
f.set(input, fd);
135+
return input;
136+
}
137+
}

0 commit comments

Comments
 (0)
Please sign in to comment.