From 31ccd03ded688d77f01dda1bdb11561ae24f7c00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Pedersen?= Date: Thu, 5 Feb 2026 18:05:45 +0100 Subject: [PATCH] Improved the methods to query for OSC 4 palette colors, fixes #96 --- .../java/org/aesh/terminal/Connection.java | 44 ++++++ .../java/org/aesh/terminal/utils/ANSI.java | 88 +++++++++++- .../org/aesh/terminal/utils/ANSIOscTest.java | 134 ++++++++++++++++++ 3 files changed, 260 insertions(+), 6 deletions(-) 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 the type of the parsed response + * @return the parsed response, or null if timeout or not supported + */ + default T queryOsc(int oscCode, int index, String param, long timeoutMs, + java.util.function.Function responseParser) { + String query = ANSI.buildOscQuery(oscCode, index, param); + return queryTerminal(query, timeoutMs, responseParser); + } + + /** + * Query the terminal for a palette color using OSC 4. + *

+ * 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) { *

  • RRRR, GGGG, BBBB are 4-digit or 2-digit hex values
  • *
  • ST is either BEL (0x07) or ESC \ (0x1B 0x5C)
  • * + *

    + * 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 + } }