diff --git a/terminal-api/src/main/java/org/aesh/terminal/Connection.java b/terminal-api/src/main/java/org/aesh/terminal/Connection.java index 64d37a5a..d6afd9ca 100644 --- a/terminal-api/src/main/java/org/aesh/terminal/Connection.java +++ b/terminal-api/src/main/java/org/aesh/terminal/Connection.java @@ -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. + *
+ * This is used for OSC codes that require an index, such as OSC 4 (palette colors). + *
+ * 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
+ * Palette colors are indexed 0-255, where:
+ *
+ * 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));
+ }
+
}
diff --git a/terminal-api/src/main/java/org/aesh/terminal/utils/ANSI.java b/terminal-api/src/main/java/org/aesh/terminal/utils/ANSI.java
index 2ba76b5a..f445c967 100644
--- a/terminal-api/src/main/java/org/aesh/terminal/utils/ANSI.java
+++ b/terminal-api/src/main/java/org/aesh/terminal/utils/ANSI.java
@@ -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. */
@@ -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.
+ *
+ * This is used for OSC codes that require an index, such as OSC 4 (palette colors).
+ *
+ * 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.
*
@@ -334,12 +353,37 @@ public static String buildOscQuery(int oscCode, String param) {
*
+ * 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.
+ *
+ * This method handles OSC codes that include a parameter, such as OSC 4
+ * (palette colors) which includes a color index.
+ *
+ * Expected formats:
+ *
+ *
+ *
+ *
+ *
+ * @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;
}
@@ -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 \)
diff --git a/terminal-api/src/test/java/org/aesh/terminal/utils/ANSIOscTest.java b/terminal-api/src/test/java/org/aesh/terminal/utils/ANSIOscTest.java
index 389258e6..5b818f70 100644
--- a/terminal-api/src/test/java/org/aesh/terminal/utils/ANSIOscTest.java
+++ b/terminal-api/src/test/java/org/aesh/terminal/utils/ANSIOscTest.java
@@ -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
+ }
}