Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions terminal-api/src/main/java/org/aesh/terminal/Connection.java
Original file line number Diff line number Diff line change
Expand Up @@ -376,4 +376,72 @@ default TerminalColorCapability getColorCapability() {
return new TerminalColorCapability(depth, TerminalColorCapability.detectThemeFromEnvironment());
}

/**
* Send an OSC (Operating System Command) query to the terminal.
* <p>
* This method sends an OSC query sequence and waits for the terminal's response.
* The terminal must be actively reading input (via {@link #openBlocking()} or
* {@link #openNonBlocking()}) for this to work.
* <p>
* Common OSC codes:
* <ul>
* <li>10 - Query/set foreground color</li>
* <li>11 - Query/set background color</li>
* <li>12 - Query/set cursor color</li>
* <li>4;N - Query/set palette color N</li>
* </ul>
*
* @param oscCode the OSC code (e.g., 10 for foreground, 11 for background)
* @param param the query parameter (typically "?" for queries)
* @param timeoutMs timeout in milliseconds to wait for response
* @param responseParser function to parse the response; should return non-null
* when a complete response is received, null to continue waiting
* @param <T> the type of the parsed response
* @return the parsed response, or null if timeout or not supported
*/
default <T> T queryOsc(int oscCode, String param, long timeoutMs,
java.util.function.Function<int[], T> responseParser) {
String query = ANSI.buildOscQuery(oscCode, param);
return queryTerminal(query, timeoutMs, responseParser);
}

/**
* Query the terminal for its foreground color using OSC 10.
* <p>
* The terminal must be actively reading input for this to work.
*
* @param timeoutMs timeout in milliseconds to wait for response
* @return RGB array [r, g, b] (0-255 each), or null if not supported or timeout
*/
default int[] queryForegroundColor(long timeoutMs) {
return queryOsc(ANSI.OSC_FOREGROUND, "?", timeoutMs,
input -> ANSI.parseOscColorResponse(input, ANSI.OSC_FOREGROUND));
}

/**
* Query the terminal for its background color using OSC 11.
* <p>
* The terminal must be actively reading input for this to work.
*
* @param timeoutMs timeout in milliseconds to wait for response
* @return RGB array [r, g, b] (0-255 each), or null if not supported or timeout
*/
default int[] queryBackgroundColor(long timeoutMs) {
return queryOsc(ANSI.OSC_BACKGROUND, "?", timeoutMs,
input -> ANSI.parseOscColorResponse(input, ANSI.OSC_BACKGROUND));
}

/**
* Query the terminal for its cursor color using OSC 12.
* <p>
* The terminal must be actively reading input for this to work.
*
* @param timeoutMs timeout in milliseconds to wait for response
* @return RGB array [r, g, b] (0-255 each), or null if not supported or timeout
*/
default int[] queryCursorColor(long timeoutMs) {
return queryOsc(ANSI.OSC_CURSOR_COLOR, "?", timeoutMs,
input -> ANSI.parseOscColorResponse(input, ANSI.OSC_CURSOR_COLOR));
}

}
115 changes: 115 additions & 0 deletions terminal-api/src/main/java/org/aesh/terminal/utils/ANSI.java
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,20 @@ public class ANSI {
/** ANSI escape code to enable dark (normal) background mode. */
public static final String DARK_BG = "\u001B[?5l";

/** OSC (Operating System Command) escape sequence start. */
public static final String OSC_START = "\u001B]";
/** BEL character, used as OSC terminator. */
public static final String BEL = "\u0007";
/** ST (String Terminator), alternate OSC terminator. */
public static final String ST = "\u001B\\";

/** OSC code for foreground color query/set. */
public static final int OSC_FOREGROUND = 10;
/** OSC code for background color query/set. */
public static final int OSC_BACKGROUND = 11;
/** OSC code for cursor color query/set. */
public static final int OSC_CURSOR_COLOR = 12;

private ANSI() {
}

Expand Down Expand Up @@ -295,4 +309,105 @@ else if (value > 999 && value < 9999)
return 5;
}

/**
* Build an OSC (Operating System Command) query string.
* <p>
* OSC format: ESC ] Ps ; Pt BEL
* Where Ps is the OSC code and Pt is the parameter.
*
* @param oscCode the OSC code (e.g., 10 for foreground, 11 for background)
* @param param the parameter (e.g., "?" for query)
* @return the OSC query string
*/
public static String buildOscQuery(int oscCode, String param) {
return OSC_START + oscCode + ";" + param + BEL;
}

/**
* Parse an OSC color response.
* <p>
* Expected format: ESC ] {oscCode} ; rgb:RRRR/GGGG/BBBB {ST}
* Where:
* <ul>
* <li>ESC is 0x1B (27)</li>
* <li>oscCode is the OSC code (e.g., 10 for foreground, 11 for background)</li>
* <li>RRRR, GGGG, BBBB are 4-digit or 2-digit hex values</li>
* <li>ST is either BEL (0x07) or ESC \ (0x1B 0x5C)</li>
* </ul>
*
* @param input the input sequence as code points
* @param oscCode the expected OSC code in response
* @return RGB array [r, g, b] (0-255 each), or null if parsing failed
*/
public static int[] parseOscColorResponse(int[] input, int oscCode) {
if (input == null || input.length < 10) {
return null;
}

// Convert to string for easier parsing
StringBuilder sb = new StringBuilder();
for (int c : input) {
sb.appendCodePoint(c);
}
String response = sb.toString();

// Look for the OSC response pattern
// Format: ESC ] {code} ; rgb:RRRR/GGGG/BBBB {terminator}
int start = response.indexOf("\u001B]" + oscCode + ";rgb:");
if (start < 0) {
// Try alternate format with just ']'
start = response.indexOf("]" + oscCode + ";rgb:");
if (start >= 0 && start > 0 && response.charAt(start - 1) == '\u001B') {
start--;
} else if (start < 0) {
return null;
}
}

// Extract the rgb: part
int rgbStart = response.indexOf("rgb:", start);
if (rgbStart < 0) {
return null;
}
rgbStart += 4; // skip "rgb:"

// Find the terminator (BEL or ESC \)
int end = response.indexOf('\u0007', rgbStart);
if (end < 0) {
end = response.indexOf("\u001B\\", rgbStart);
}
if (end < 0) {
end = response.length();
}

String rgbPart = response.substring(rgbStart, end);

// Parse RRRR/GGGG/BBBB
String[] parts = rgbPart.split("/");
if (parts.length != 3) {
return null;
}

try {
int[] rgb = new int[3];
for (int i = 0; i < 3; i++) {
String hex = parts[i].trim();
int value;
if (hex.length() == 4) {
// 4-digit hex (e.g., FFFF), take high byte
value = Integer.parseInt(hex, 16) >> 8;
} else if (hex.length() == 2) {
// 2-digit hex
value = Integer.parseInt(hex, 16);
} else {
return null;
}
rgb[i] = Math.min(255, Math.max(0, value));
}
return rgb;
} catch (NumberFormatException e) {
return null;
}
}

}
Loading