HexaTerminal.java
package com.guinetik.hexafun.examples.tui;
import java.io.BufferedReader;
import java.io.InputStreamReader;
/**
* Cross-platform terminal utility functions for TUI applications.
*
* <p>Provides terminal dimension detection on Unix, macOS, and Windows.</p>
*/
public final class HexaTerminal {
private HexaTerminal() {}
/** Default minimum width for TUI applications */
public static final int MIN_WIDTH = 60;
/** Default width if detection fails */
public static final int DEFAULT_WIDTH = 120;
/** Default minimum height */
public static final int MIN_HEIGHT = 20;
/** Default height if detection fails */
public static final int DEFAULT_HEIGHT = 40;
/** Cached OS type */
private static final boolean IS_WINDOWS = System.getProperty("os.name", "")
.toLowerCase().contains("win");
/**
* Detect the current terminal width.
*
* @return Terminal width in characters
*/
public static int detectWidth() {
return detectWidth(MIN_WIDTH, DEFAULT_WIDTH);
}
/**
* Detect terminal width with custom bounds.
*
* @param minWidth Minimum width to return
* @param defaultWidth Default if detection fails
* @return Terminal width in characters
*/
public static int detectWidth(int minWidth, int defaultWidth) {
int[] size = detectSize();
if (size != null && size[1] > 0) {
return Math.max(minWidth, size[1]);
}
return defaultWidth;
}
/**
* Detect the current terminal height.
*
* @return Terminal height in rows
*/
public static int detectHeight() {
return detectHeight(MIN_HEIGHT, DEFAULT_HEIGHT);
}
/**
* Detect terminal height with custom bounds.
*
* @param minHeight Minimum height to return
* @param defaultHeight Default if detection fails
* @return Terminal height in rows
*/
public static int detectHeight(int minHeight, int defaultHeight) {
int[] size = detectSize();
if (size != null && size[0] > 0) {
return Math.max(minHeight, size[0]);
}
return defaultHeight;
}
/**
* Detect terminal size as [rows, columns].
*
* @return int[2] with {rows, columns} or null if detection fails
*/
public static int[] detectSize() {
return IS_WINDOWS ? detectSizeWindows() : detectSizeUnix();
}
/**
* Unix/macOS/WSL size detection using stty.
*/
private static int[] detectSizeUnix() {
// Try stty size (works on Linux, macOS, WSL)
try {
ProcessBuilder pb = new ProcessBuilder("stty", "size");
pb.inheritIO();
pb.redirectInput(ProcessBuilder.Redirect.INHERIT);
pb.redirectOutput(ProcessBuilder.Redirect.PIPE);
pb.redirectError(ProcessBuilder.Redirect.PIPE);
Process p = pb.start();
String line = new BufferedReader(
new InputStreamReader(p.getInputStream())
).readLine();
int exitCode = p.waitFor();
if (exitCode == 0 && line != null && !line.isBlank()) {
String[] parts = line.trim().split("\\s+");
if (parts.length >= 2) {
int rows = Integer.parseInt(parts[0]);
int cols = Integer.parseInt(parts[1]);
if (rows > 0 && cols > 0) {
return new int[]{rows, cols};
}
}
}
} catch (Exception ignored) {}
// Fallback: tput
try {
int cols = runCommandInt("tput", "cols");
int rows = runCommandInt("tput", "lines");
if (cols > 0 && rows > 0) {
return new int[]{rows, cols};
}
} catch (Exception ignored) {}
// Fallback: environment variables
return detectSizeFromEnv();
}
/**
* Windows size detection using PowerShell or MODE.
*/
private static int[] detectSizeWindows() {
// Try PowerShell (most reliable on modern Windows)
try {
ProcessBuilder pb = new ProcessBuilder(
"powershell", "-NoProfile", "-Command",
"[Console]::WindowWidth; [Console]::WindowHeight"
);
pb.redirectErrorStream(true);
Process p = pb.start();
BufferedReader reader = new BufferedReader(
new InputStreamReader(p.getInputStream())
);
String widthLine = reader.readLine();
String heightLine = reader.readLine();
p.waitFor();
if (widthLine != null && heightLine != null) {
int cols = Integer.parseInt(widthLine.trim());
int rows = Integer.parseInt(heightLine.trim());
if (rows > 0 && cols > 0) {
return new int[]{rows, cols};
}
}
} catch (Exception ignored) {}
// Fallback: MODE CON (cmd.exe)
try {
ProcessBuilder pb = new ProcessBuilder("cmd", "/c", "mode", "con");
pb.redirectErrorStream(true);
Process p = pb.start();
BufferedReader reader = new BufferedReader(
new InputStreamReader(p.getInputStream())
);
int cols = -1, rows = -1;
String line;
while ((line = reader.readLine()) != null) {
line = line.toLowerCase();
if (line.contains("columns") || line.contains("cols")) {
cols = extractNumber(line);
} else if (line.contains("lines")) {
rows = extractNumber(line);
}
}
p.waitFor();
if (cols > 0 && rows > 0) {
return new int[]{rows, cols};
}
} catch (Exception ignored) {}
// Fallback: environment variables
return detectSizeFromEnv();
}
/**
* Try to get size from environment variables.
*/
private static int[] detectSizeFromEnv() {
String colsEnv = System.getenv("COLUMNS");
String rowsEnv = System.getenv("LINES");
if (colsEnv != null && rowsEnv != null) {
try {
int cols = Integer.parseInt(colsEnv.trim());
int rows = Integer.parseInt(rowsEnv.trim());
if (rows > 0 && cols > 0) {
return new int[]{rows, cols};
}
} catch (NumberFormatException ignored) {}
}
return null;
}
/**
* Run a command and return the output as an integer.
*/
private static int runCommandInt(String... cmd) throws Exception {
ProcessBuilder pb = new ProcessBuilder(cmd);
pb.redirectInput(ProcessBuilder.Redirect.INHERIT);
pb.redirectErrorStream(true);
Process p = pb.start();
String line = new BufferedReader(
new InputStreamReader(p.getInputStream())
).readLine();
p.waitFor();
return line != null ? Integer.parseInt(line.trim()) : -1;
}
/**
* Extract a number from a string like "Columns: 120" or "Lines: 30".
*/
private static int extractNumber(String line) {
StringBuilder sb = new StringBuilder();
for (char c : line.toCharArray()) {
if (Character.isDigit(c)) {
sb.append(c);
} else if (sb.length() > 0) {
break; // Stop at first non-digit after finding digits
}
}
try {
return sb.length() > 0 ? Integer.parseInt(sb.toString()) : -1;
} catch (NumberFormatException e) {
return -1;
}
}
/**
* Get terminal size as "ROWSxCOLS" string.
*
* @return Size string like "24x80" or "unknown"
*/
public static String getSizeString() {
int[] size = detectSize();
if (size != null) {
return size[0] + "x" + size[1];
}
return "unknown";
}
/**
* Check if running on Windows.
*/
public static boolean isWindows() {
return IS_WINDOWS;
}
}