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
44 changes: 44 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 @@ -444,4 +444,48 @@ default int[] queryCursorColor(long timeoutMs) {
input -> ANSI.parseOscColorResponse(input, ANSI.OSC_CURSOR_COLOR));
}

/**
* Send an OSC query with an index parameter to the terminal.
* <p>
* This is used for OSC codes that require an index, such as OSC 4 (palette colors).
* <p>
* The terminal must be actively reading input for this to work.
*
* @param oscCode the OSC code (e.g., 4 for palette color)
* @param index the index parameter (e.g., palette color index 0-255)
* @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, int index, String param, long timeoutMs,
java.util.function.Function<int[], T> responseParser) {
String query = ANSI.buildOscQuery(oscCode, index, param);
return queryTerminal(query, timeoutMs, responseParser);
}

/**
* Query the terminal for a palette color using OSC 4.
* <p>
* Palette colors are indexed 0-255, where:
* <ul>
* <li>0-7: Standard ANSI colors</li>
* <li>8-15: Bright ANSI colors</li>
* <li>16-231: 216-color cube</li>
* <li>232-255: Grayscale ramp</li>
* </ul>
* <p>
* The terminal must be actively reading input for this to work.
*
* @param index the palette color index (0-255)
* @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[] queryPaletteColor(int index, long timeoutMs) {
return queryOsc(ANSI.OSC_PALETTE, index, "?", timeoutMs,
input -> ANSI.parseOscColorResponse(input, ANSI.OSC_PALETTE, index));
}

}
88 changes: 82 additions & 6 deletions terminal-api/src/main/java/org/aesh/terminal/utils/ANSI.java
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ public class ANSI {
/** ST (String Terminator), alternate OSC terminator. */
public static final String ST = "\u001B\\";

/** OSC code for palette color query/set. */
public static final int OSC_PALETTE = 4;
/** OSC code for foreground color query/set. */
public static final int OSC_FOREGROUND = 10;
/** OSC code for background color query/set. */
Expand Down Expand Up @@ -323,6 +325,23 @@ public static String buildOscQuery(int oscCode, String param) {
return OSC_START + oscCode + ";" + param + BEL;
}

/**
* Build an OSC query string with an additional index parameter.
* <p>
* This is used for OSC codes that require an index, such as OSC 4 (palette colors).
* <p>
* OSC format: ESC ] Ps ; Pn ; Pt BEL
* Where Ps is the OSC code, Pn is the index/parameter, and Pt is the query.
*
* @param oscCode the OSC code (e.g., 4 for palette color)
* @param index the index parameter (e.g., palette color index 0-255)
* @param param the parameter (e.g., "?" for query)
* @return the OSC query string
*/
public static String buildOscQuery(int oscCode, int index, String param) {
return OSC_START + oscCode + ";" + index + ";" + param + BEL;
}

/**
* Parse an OSC color response.
* <p>
Expand All @@ -334,12 +353,37 @@ public static String buildOscQuery(int oscCode, String param) {
* <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>
* <p>
* For OSC codes with parameters (like OSC 4 palette colors), use
* {@link #parseOscColorResponse(int[], int, int)} instead.
*
* @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) {
return parseOscColorResponse(input, oscCode, -1);
}

/**
* Parse an OSC color response with an optional parameter.
* <p>
* This method handles OSC codes that include a parameter, such as OSC 4
* (palette colors) which includes a color index.
* <p>
* Expected formats:
* <ul>
* <li>Without parameter: ESC ] {code} ; rgb:RRRR/GGGG/BBBB {ST}</li>
* <li>With parameter: ESC ] {code} ; {param} ; rgb:RRRR/GGGG/BBBB {ST}</li>
* </ul>
*
* @param input the input sequence as code points
* @param oscCode the expected OSC code in response
* @param oscParam the expected parameter (e.g., palette index for OSC 4),
* or -1 to not require a specific parameter
* @return RGB array [r, g, b] (0-255 each), or null if parsing failed
*/
public static int[] parseOscColorResponse(int[] input, int oscCode, int oscParam) {
if (input == null || input.length < 10) {
return null;
}
Expand All @@ -351,24 +395,56 @@ public static int[] parseOscColorResponse(int[] input, int oscCode) {
}
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:");
// Build the pattern to search for
String oscMarker = "\u001B]" + oscCode + ";";
int start = response.indexOf(oscMarker);
if (start < 0) {
// Try alternate format with just ']'
start = response.indexOf("]" + oscCode + ";rgb:");
oscMarker = "]" + oscCode + ";";
start = response.indexOf(oscMarker);
if (start >= 0 && start > 0 && response.charAt(start - 1) == '\u001B') {
start--;
oscMarker = "\u001B" + oscMarker;
} else if (start < 0) {
return null;
}
}

// Extract the rgb: part
int rgbStart = response.indexOf("rgb:", start);
// Move past the OSC marker
int searchStart = start + oscMarker.length();

// If a specific parameter is expected, verify it's present
if (oscParam >= 0) {
String paramMarker = oscParam + ";";
if (!response.substring(searchStart).startsWith(paramMarker)) {
return null;
}
searchStart += paramMarker.length();
}

// Find rgb: from current position
// Handle case where there might be an unexpected parameter before rgb:
int rgbStart = response.indexOf("rgb:", searchStart);
if (rgbStart < 0) {
return null;
}

// Verify rgb: comes before any terminator
int belPos = response.indexOf('\u0007', searchStart);
int stPos = response.indexOf("\u001B\\", searchStart);
int terminatorPos = -1;
if (belPos >= 0 && stPos >= 0) {
terminatorPos = Math.min(belPos, stPos);
} else if (belPos >= 0) {
terminatorPos = belPos;
} else if (stPos >= 0) {
terminatorPos = stPos;
}

if (terminatorPos >= 0 && rgbStart > terminatorPos) {
return null;
}

rgbStart += 4; // skip "rgb:"

// Find the terminator (BEL or ESC \)
Expand Down
134 changes: 134 additions & 0 deletions terminal-api/src/test/java/org/aesh/terminal/utils/ANSIOscTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -229,4 +229,138 @@ public void testParseOscColorResponse_LowercaseHex() {
assertEquals(0xef, rgb[1]); // ef01 >> 8 = 0xef = 239
assertEquals(0x23, rgb[2]); // 2345 >> 8 = 0x23 = 35
}

// ==================== OSC 4 Palette Color Tests ====================

@Test
public void testOscPaletteConstant() {
assertEquals(4, ANSI.OSC_PALETTE);
}

@Test
public void testBuildOscQueryWithIndex() {
// Test palette color query (OSC 4 with index)
String query = ANSI.buildOscQuery(4, 1, "?");
assertEquals("\u001B]4;1;?\u0007", query);

// Test with different index
query = ANSI.buildOscQuery(4, 255, "?");
assertEquals("\u001B]4;255;?\u0007", query);
}

@Test
public void testParseOscColorResponse_PaletteColorWithIndex() {
// OSC 4 response with palette index 1
// Response: ESC ] 4 ; 1 ; rgb:dc19/686d/6b19 BEL
String response = "\u001B]4;1;rgb:dc19/686d/6b19\u0007";
int[] input = response.codePoints().toArray();

int[] rgb = ANSI.parseOscColorResponse(input, 4, 1);

assertNotNull("Should parse OSC 4 response with index", rgb);
assertEquals(3, rgb.length);
assertEquals(0xdc, rgb[0]); // dc19 >> 8 = 0xdc = 220
assertEquals(0x68, rgb[1]); // 686d >> 8 = 0x68 = 104
assertEquals(0x6b, rgb[2]); // 6b19 >> 8 = 0x6b = 107
}

@Test
public void testParseOscColorResponse_PaletteColorIndex0() {
// OSC 4 response with palette index 0 (black in standard palette)
String response = "\u001B]4;0;rgb:0000/0000/0000\u0007";
int[] input = response.codePoints().toArray();

int[] rgb = ANSI.parseOscColorResponse(input, 4, 0);

assertNotNull("Should parse OSC 4 response for index 0", rgb);
assertEquals(0, rgb[0]);
assertEquals(0, rgb[1]);
assertEquals(0, rgb[2]);
}

@Test
public void testParseOscColorResponse_PaletteColorHighIndex() {
// OSC 4 response with palette index 255
String response = "\u001B]4;255;rgb:EEEE/EEEE/EEEE\u0007";
int[] input = response.codePoints().toArray();

int[] rgb = ANSI.parseOscColorResponse(input, 4, 255);

assertNotNull("Should parse OSC 4 response for index 255", rgb);
assertEquals(0xee, rgb[0]); // EEEE >> 8 = 238
assertEquals(0xee, rgb[1]);
assertEquals(0xee, rgb[2]);
}

@Test
public void testParseOscColorResponse_PaletteColorWrongIndex() {
// OSC 4 response with index 2, but we're looking for index 1
String response = "\u001B]4;2;rgb:FFFF/0000/0000\u0007";
int[] input = response.codePoints().toArray();

int[] rgb = ANSI.parseOscColorResponse(input, 4, 1);

assertNull("Should return null for wrong palette index", rgb);
}

@Test
public void testParseOscColorResponse_PaletteColorNoIndexParam() {
// Using parseOscColorResponse without index parameter
// This simulates the issue #96 - old code couldn't parse OSC 4 responses
String response = "\u001B]4;1;rgb:dc19/686d/6b19\u0007";
int[] input = response.codePoints().toArray();

// The 2-param version should still find rgb: even with index present
int[] rgb = ANSI.parseOscColorResponse(input, 4);

assertNotNull("Should find rgb: even when index is present", rgb);
assertEquals(0xdc, rgb[0]);
assertEquals(0x68, rgb[1]);
assertEquals(0x6b, rgb[2]);
}

@Test
public void testParseOscColorResponse_PaletteColorWithSTTerminator() {
// OSC 4 response with ESC \ terminator
String response = "\u001B]4;7;rgb:c0c0/c0c0/c0c0\u001B\\";
int[] input = response.codePoints().toArray();

int[] rgb = ANSI.parseOscColorResponse(input, 4, 7);

assertNotNull("Should parse OSC 4 response with ST terminator", rgb);
assertEquals(0xc0, rgb[0]); // c0c0 >> 8 = 192
assertEquals(0xc0, rgb[1]);
assertEquals(0xc0, rgb[2]);
}

@Test
public void testParseOscColorResponse_Issue96Scenario() {
// Exact scenario from GitHub issue #96
// Query: OSC 4;1;? - query palette color 1
// Response: ESC]4;1;rgb:dc19/686d/6b19
String response = "\u001B]4;1;rgb:dc19/686d/6b19\u0007";
int[] input = response.codePoints().toArray();

// This should now work with the 3-param method
int[] rgb = ANSI.parseOscColorResponse(input, ANSI.OSC_PALETTE, 1);

assertNotNull("Issue #96: Should parse OSC 4;1 palette color response", rgb);
assertEquals(220, rgb[0]); // 0xdc
assertEquals(104, rgb[1]); // 0x68
assertEquals(107, rgb[2]); // 0x6b
}

@Test
public void testParseOscColorResponse_RegularOscStillWorks() {
// Verify that regular OSC 11 still works after the changes
String response = "\u001B]11;rgb:19ee/2448/2b8a\u0007";
int[] input = response.codePoints().toArray();

int[] rgb = ANSI.parseOscColorResponse(input, ANSI.OSC_BACKGROUND);

assertNotNull("Regular OSC 11 should still work", rgb);
assertEquals(0x19, rgb[0]); // 19ee >> 8 = 25
assertEquals(0x24, rgb[1]); // 2448 >> 8 = 36
assertEquals(0x2b, rgb[2]); // 2b8a >> 8 = 43
}
}