responseParser) {
+ String query = ANSI.buildOscQuery(oscCode, param);
+ return queryTerminal(query, timeoutMs, responseParser);
+ }
+
+ /**
+ * Query the terminal for its foreground color using OSC 10.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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));
+ }
+
}
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 9d10526..2ba76b5 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
@@ -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() {
}
@@ -295,4 +309,105 @@ else if (value > 999 && value < 9999)
return 5;
}
+ /**
+ * Build an OSC (Operating System Command) query string.
+ *
+ * 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.
+ *
+ * Expected format: ESC ] {oscCode} ; rgb:RRRR/GGGG/BBBB {ST}
+ * Where:
+ *
+ * - ESC is 0x1B (27)
+ * - oscCode is the OSC code (e.g., 10 for foreground, 11 for background)
+ * - RRRR, GGGG, BBBB are 4-digit or 2-digit hex values
+ * - ST is either BEL (0x07) or ESC \ (0x1B 0x5C)
+ *
+ *
+ * @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;
+ }
+ }
+
}
diff --git a/terminal-api/src/test/java/org/aesh/terminal/ConnectionOscQueryTest.java b/terminal-api/src/test/java/org/aesh/terminal/ConnectionOscQueryTest.java
new file mode 100644
index 0000000..757cdd9
--- /dev/null
+++ b/terminal-api/src/test/java/org/aesh/terminal/ConnectionOscQueryTest.java
@@ -0,0 +1,407 @@
+/*
+ * JBoss, Home of Professional Open Source
+ * Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @authors tag. All rights reserved.
+ * See the copyright.txt in the distribution for a
+ * full listing of individual contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.aesh.terminal;
+
+import static org.junit.Assert.*;
+
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+import org.aesh.terminal.tty.Capability;
+import org.aesh.terminal.tty.Signal;
+import org.aesh.terminal.tty.Size;
+import org.junit.Test;
+
+/**
+ * Tests for the Connection.queryOsc() method and related color query methods.
+ * These tests verify that OSC queries work correctly and responses are captured
+ * properly (addressing issue #94).
+ *
+ * @author Ståle Pedersen
+ */
+public class ConnectionOscQueryTest {
+
+ /**
+ * Test that queryOsc correctly builds and sends OSC queries.
+ */
+ @Test
+ public void testQueryOscBuildsCorrectQuery() {
+ List sentQueries = new ArrayList<>();
+ MockConnection connection = new MockConnection() {
+ @Override
+ public Consumer stdoutHandler() {
+ return codePoints -> {
+ StringBuilder sb = new StringBuilder();
+ for (int cp : codePoints) {
+ sb.appendCodePoint(cp);
+ }
+ sentQueries.add(sb.toString());
+ };
+ }
+ };
+
+ // Trigger a query (will timeout since no response)
+ connection.queryOsc(10, "?", 50, input -> null);
+
+ assertEquals(1, sentQueries.size());
+ assertEquals("\u001B]10;?\u0007", sentQueries.get(0));
+ }
+
+ /**
+ * Test that queryForegroundColor correctly parses a valid response.
+ * This tests the scenario from issue #94 - the response should be
+ * captured and returned, not echoed to terminal.
+ */
+ @Test
+ public void testQueryForegroundColorReturnsValue() throws Exception {
+ // Create a connection that simulates a terminal responding to OSC 10
+ MockConnection connection = new MockConnection();
+
+ // Simulate the terminal response in a separate thread
+ CountDownLatch queryStarted = new CountDownLatch(1);
+ Thread responseThread = new Thread(() -> {
+ try {
+ queryStarted.await(1, TimeUnit.SECONDS);
+ Thread.sleep(20); // Give time for query to be sent
+ // Simulate terminal response
+ String response = "\u001B]10;rgb:FFFF/8080/0000\u0007";
+ connection.simulateInput(response);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ });
+ responseThread.start();
+
+ // Signal that query is starting
+ queryStarted.countDown();
+
+ // Query foreground color
+ int[] rgb = connection.queryForegroundColor(500);
+
+ responseThread.join(1000);
+
+ // Verify the response was captured and returned
+ assertNotNull("queryForegroundColor should return the parsed RGB values", rgb);
+ assertEquals(3, rgb.length);
+ assertEquals(255, rgb[0]); // FFFF >> 8 = 255
+ assertEquals(128, rgb[1]); // 8080 >> 8 = 128
+ assertEquals(0, rgb[2]); // 0000 >> 8 = 0
+ }
+
+ /**
+ * Test that queryBackgroundColor correctly parses a valid response.
+ */
+ @Test
+ public void testQueryBackgroundColorReturnsValue() throws Exception {
+ MockConnection connection = new MockConnection();
+
+ CountDownLatch queryStarted = new CountDownLatch(1);
+ Thread responseThread = new Thread(() -> {
+ try {
+ queryStarted.await(1, TimeUnit.SECONDS);
+ Thread.sleep(20);
+ String response = "\u001B]11;rgb:2828/2828/2828\u0007";
+ connection.simulateInput(response);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ });
+ responseThread.start();
+
+ queryStarted.countDown();
+ int[] rgb = connection.queryBackgroundColor(500);
+ responseThread.join(1000);
+
+ assertNotNull("queryBackgroundColor should return the parsed RGB values", rgb);
+ assertEquals(40, rgb[0]); // 2828 >> 8 = 40
+ assertEquals(40, rgb[1]);
+ assertEquals(40, rgb[2]);
+ }
+
+ /**
+ * Test that queryCursorColor correctly parses a valid response.
+ */
+ @Test
+ public void testQueryCursorColorReturnsValue() throws Exception {
+ MockConnection connection = new MockConnection();
+
+ CountDownLatch queryStarted = new CountDownLatch(1);
+ Thread responseThread = new Thread(() -> {
+ try {
+ queryStarted.await(1, TimeUnit.SECONDS);
+ Thread.sleep(20);
+ String response = "\u001B]12;rgb:0000/FFFF/0000\u0007";
+ connection.simulateInput(response);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ });
+ responseThread.start();
+
+ queryStarted.countDown();
+ int[] rgb = connection.queryCursorColor(500);
+ responseThread.join(1000);
+
+ assertNotNull("queryCursorColor should return the parsed RGB values", rgb);
+ assertEquals(0, rgb[0]);
+ assertEquals(255, rgb[1]);
+ assertEquals(0, rgb[2]);
+ }
+
+ /**
+ * Test that query returns null on timeout when no response.
+ */
+ @Test
+ public void testQueryReturnsNullOnTimeout() {
+ MockConnection connection = new MockConnection();
+
+ long start = System.currentTimeMillis();
+ int[] rgb = connection.queryForegroundColor(100);
+ long elapsed = System.currentTimeMillis() - start;
+
+ assertNull("Should return null when no response received", rgb);
+ assertTrue("Should wait for timeout", elapsed >= 90);
+ }
+
+ /**
+ * Test that the response is captured by the stdin handler and not
+ * passed to any other handler (addressing issue #94).
+ */
+ @Test
+ public void testResponseNotPassedToOriginalHandler() throws Exception {
+ MockConnection connection = new MockConnection();
+ List originalHandlerReceived = new ArrayList<>();
+
+ // Set up an original handler that should NOT receive the OSC response
+ connection.setStdinHandler(input -> {
+ StringBuilder sb = new StringBuilder();
+ for (int cp : input) {
+ sb.appendCodePoint(cp);
+ }
+ originalHandlerReceived.add(sb.toString());
+ });
+
+ CountDownLatch queryStarted = new CountDownLatch(1);
+ Thread responseThread = new Thread(() -> {
+ try {
+ queryStarted.await(1, TimeUnit.SECONDS);
+ Thread.sleep(20);
+ String response = "\u001B]10;rgb:FFFF/FFFF/FFFF\u0007";
+ connection.simulateInput(response);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ });
+ responseThread.start();
+
+ queryStarted.countDown();
+ int[] rgb = connection.queryForegroundColor(500);
+ responseThread.join(1000);
+
+ assertNotNull("Query should succeed", rgb);
+ // The original handler should be restored after the query
+ // but should not have received the OSC response
+ assertTrue("Original handler should not receive OSC response during query",
+ originalHandlerReceived.isEmpty());
+ }
+
+ /**
+ * Test generic queryOsc with custom parser.
+ */
+ @Test
+ public void testGenericQueryOscWithCustomParser() throws Exception {
+ MockConnection connection = new MockConnection();
+
+ CountDownLatch queryStarted = new CountDownLatch(1);
+ Thread responseThread = new Thread(() -> {
+ try {
+ queryStarted.await(1, TimeUnit.SECONDS);
+ Thread.sleep(20);
+ // Simulate a custom OSC response
+ String response = "\u001B]52;c;SGVsbG8gV29ybGQ=\u0007";
+ connection.simulateInput(response);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ });
+ responseThread.start();
+
+ queryStarted.countDown();
+
+ // Query OSC 52 (clipboard) with custom parser
+ String result = connection.queryOsc(52, "c;?", 500, input -> {
+ StringBuilder sb = new StringBuilder();
+ for (int cp : input) {
+ sb.appendCodePoint(cp);
+ }
+ String str = sb.toString();
+ if (str.contains(";") && str.contains("\u001B]52")) {
+ // Extract base64 content
+ int start = str.indexOf(";c;") + 3;
+ int end = str.indexOf('\u0007', start);
+ if (end < 0)
+ end = str.indexOf("\u001B\\", start);
+ if (end > start) {
+ return str.substring(start, end);
+ }
+ }
+ return null;
+ });
+
+ responseThread.join(1000);
+
+ assertEquals("SGVsbG8gV29ybGQ=", result);
+ }
+
+ /**
+ * A mock connection for testing OSC queries.
+ */
+ private static class MockConnection implements Connection {
+ private Consumer stdinHandler;
+ private Consumer sizeHandler;
+ private Consumer signalHandler;
+ private Consumer closeHandler;
+ private Attributes attributes = new Attributes();
+ private final List outputBuffer = new ArrayList<>();
+
+ @Override
+ public Device device() {
+ return new BaseDevice("xterm-256color");
+ }
+
+ @Override
+ public Size size() {
+ return new Size(80, 24);
+ }
+
+ @Override
+ public Consumer getSizeHandler() {
+ return sizeHandler;
+ }
+
+ @Override
+ public void setSizeHandler(Consumer handler) {
+ this.sizeHandler = handler;
+ }
+
+ @Override
+ public Consumer getSignalHandler() {
+ return signalHandler;
+ }
+
+ @Override
+ public void setSignalHandler(Consumer handler) {
+ this.signalHandler = handler;
+ }
+
+ @Override
+ public Consumer getStdinHandler() {
+ return stdinHandler;
+ }
+
+ @Override
+ public void setStdinHandler(Consumer handler) {
+ this.stdinHandler = handler;
+ }
+
+ @Override
+ public Consumer stdoutHandler() {
+ return codePoints -> {
+ StringBuilder sb = new StringBuilder();
+ for (int cp : codePoints) {
+ sb.appendCodePoint(cp);
+ }
+ outputBuffer.add(sb.toString());
+ };
+ }
+
+ @Override
+ public void setCloseHandler(Consumer handler) {
+ this.closeHandler = handler;
+ }
+
+ @Override
+ public Consumer getCloseHandler() {
+ return closeHandler;
+ }
+
+ @Override
+ public void close() {
+ if (closeHandler != null) {
+ closeHandler.accept(null);
+ }
+ }
+
+ @Override
+ public void openBlocking() {
+ }
+
+ @Override
+ public void openNonBlocking() {
+ }
+
+ @Override
+ public boolean put(Capability capability, Object... params) {
+ return false;
+ }
+
+ @Override
+ public Attributes getAttributes() {
+ return attributes;
+ }
+
+ @Override
+ public void setAttributes(Attributes attr) {
+ this.attributes = attr;
+ }
+
+ @Override
+ public Charset inputEncoding() {
+ return Charset.defaultCharset();
+ }
+
+ @Override
+ public Charset outputEncoding() {
+ return Charset.defaultCharset();
+ }
+
+ @Override
+ public boolean supportsAnsi() {
+ return true;
+ }
+
+ /**
+ * Simulate terminal input (as if the terminal responded to a query).
+ */
+ public void simulateInput(String input) {
+ if (stdinHandler != null) {
+ stdinHandler.accept(input.codePoints().toArray());
+ }
+ }
+
+ public List getOutputBuffer() {
+ return outputBuffer;
+ }
+ }
+}
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
new file mode 100644
index 0000000..389258e
--- /dev/null
+++ b/terminal-api/src/test/java/org/aesh/terminal/utils/ANSIOscTest.java
@@ -0,0 +1,232 @@
+/*
+ * JBoss, Home of Professional Open Source
+ * Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @authors tag. All rights reserved.
+ * See the copyright.txt in the distribution for a
+ * full listing of individual contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.aesh.terminal.utils;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+/**
+ * Tests for OSC (Operating System Command) utilities in the ANSI class.
+ * These tests verify the parsing and building of OSC sequences used for
+ * terminal color queries.
+ *
+ * @author Ståle Pedersen
+ */
+public class ANSIOscTest {
+
+ @Test
+ public void testOscConstants() {
+ assertEquals("\u001B]", ANSI.OSC_START);
+ assertEquals("\u0007", ANSI.BEL);
+ assertEquals("\u001B\\", ANSI.ST);
+ assertEquals(10, ANSI.OSC_FOREGROUND);
+ assertEquals(11, ANSI.OSC_BACKGROUND);
+ assertEquals(12, ANSI.OSC_CURSOR_COLOR);
+ }
+
+ @Test
+ public void testBuildOscQuery() {
+ // Test foreground color query (OSC 10)
+ String query = ANSI.buildOscQuery(10, "?");
+ assertEquals("\u001B]10;?\u0007", query);
+
+ // Test background color query (OSC 11)
+ query = ANSI.buildOscQuery(11, "?");
+ assertEquals("\u001B]11;?\u0007", query);
+
+ // Test cursor color query (OSC 12)
+ query = ANSI.buildOscQuery(12, "?");
+ assertEquals("\u001B]12;?\u0007", query);
+
+ // Test with different parameter
+ query = ANSI.buildOscQuery(10, "rgb:ffff/ffff/ffff");
+ assertEquals("\u001B]10;rgb:ffff/ffff/ffff\u0007", query);
+ }
+
+ @Test
+ public void testParseOscColorResponse_ForegroundWith4DigitHex() {
+ // Standard OSC 10 response with 4-digit hex values (common format)
+ // Response: ESC ] 10 ; rgb:FFFF/8080/0000 BEL
+ String response = "\u001B]10;rgb:FFFF/8080/0000\u0007";
+ int[] input = response.codePoints().toArray();
+
+ int[] rgb = ANSI.parseOscColorResponse(input, 10);
+
+ assertNotNull("Should parse valid OSC 10 response", rgb);
+ assertEquals(3, rgb.length);
+ assertEquals(255, rgb[0]); // FFFF >> 8 = 255
+ assertEquals(128, rgb[1]); // 8080 >> 8 = 128
+ assertEquals(0, rgb[2]); // 0000 >> 8 = 0
+ }
+
+ @Test
+ public void testParseOscColorResponse_BackgroundWith4DigitHex() {
+ // Standard OSC 11 response with 4-digit hex values
+ String response = "\u001B]11;rgb:2828/2828/2828\u0007";
+ int[] input = response.codePoints().toArray();
+
+ int[] rgb = ANSI.parseOscColorResponse(input, 11);
+
+ assertNotNull("Should parse valid OSC 11 response", rgb);
+ assertEquals(3, rgb.length);
+ assertEquals(40, rgb[0]); // 2828 >> 8 = 40
+ assertEquals(40, rgb[1]);
+ assertEquals(40, rgb[2]);
+ }
+
+ @Test
+ public void testParseOscColorResponse_With2DigitHex() {
+ // Some terminals respond with 2-digit hex values
+ String response = "\u001B]10;rgb:FF/80/00\u0007";
+ int[] input = response.codePoints().toArray();
+
+ int[] rgb = ANSI.parseOscColorResponse(input, 10);
+
+ assertNotNull("Should parse 2-digit hex response", rgb);
+ assertEquals(255, rgb[0]);
+ assertEquals(128, rgb[1]);
+ assertEquals(0, rgb[2]);
+ }
+
+ @Test
+ public void testParseOscColorResponse_WithSTTerminator() {
+ // Some terminals use ESC \ as terminator instead of BEL
+ String response = "\u001B]10;rgb:FFFF/0000/FFFF\u001B\\";
+ int[] input = response.codePoints().toArray();
+
+ int[] rgb = ANSI.parseOscColorResponse(input, 10);
+
+ assertNotNull("Should parse response with ST terminator", rgb);
+ assertEquals(255, rgb[0]);
+ assertEquals(0, rgb[1]);
+ assertEquals(255, rgb[2]);
+ }
+
+ @Test
+ public void testParseOscColorResponse_CursorColor() {
+ // OSC 12 for cursor color
+ String response = "\u001B]12;rgb:0000/FFFF/0000\u0007";
+ int[] input = response.codePoints().toArray();
+
+ int[] rgb = ANSI.parseOscColorResponse(input, 12);
+
+ assertNotNull("Should parse cursor color response", rgb);
+ assertEquals(0, rgb[0]);
+ assertEquals(255, rgb[1]);
+ assertEquals(0, rgb[2]);
+ }
+
+ @Test
+ public void testParseOscColorResponse_WrongOscCode() {
+ // Response is for OSC 11 but we're looking for OSC 10
+ String response = "\u001B]11;rgb:FFFF/FFFF/FFFF\u0007";
+ int[] input = response.codePoints().toArray();
+
+ int[] rgb = ANSI.parseOscColorResponse(input, 10);
+
+ assertNull("Should return null for wrong OSC code", rgb);
+ }
+
+ @Test
+ public void testParseOscColorResponse_InvalidFormat() {
+ // Missing rgb: prefix
+ String response = "\u001B]10;FFFF/FFFF/FFFF\u0007";
+ int[] input = response.codePoints().toArray();
+
+ int[] rgb = ANSI.parseOscColorResponse(input, 10);
+
+ assertNull("Should return null for invalid format", rgb);
+ }
+
+ @Test
+ public void testParseOscColorResponse_NullInput() {
+ int[] rgb = ANSI.parseOscColorResponse(null, 10);
+ assertNull("Should return null for null input", rgb);
+ }
+
+ @Test
+ public void testParseOscColorResponse_EmptyInput() {
+ int[] rgb = ANSI.parseOscColorResponse(new int[0], 10);
+ assertNull("Should return null for empty input", rgb);
+ }
+
+ @Test
+ public void testParseOscColorResponse_TooShortInput() {
+ int[] rgb = ANSI.parseOscColorResponse(new int[] { 27, ']', '1', '0' }, 10);
+ assertNull("Should return null for too short input", rgb);
+ }
+
+ @Test
+ public void testParseOscColorResponse_WithPrefixNoise() {
+ // Sometimes there's noise before the actual response
+ String response = "some noise\u001B]10;rgb:8080/8080/8080\u0007";
+ int[] input = response.codePoints().toArray();
+
+ int[] rgb = ANSI.parseOscColorResponse(input, 10);
+
+ assertNotNull("Should parse response with prefix noise", rgb);
+ assertEquals(128, rgb[0]);
+ assertEquals(128, rgb[1]);
+ assertEquals(128, rgb[2]);
+ }
+
+ @Test
+ public void testParseOscColorResponse_WhiteColor() {
+ // Test parsing white color (max values)
+ String response = "\u001B]11;rgb:FFFF/FFFF/FFFF\u0007";
+ int[] input = response.codePoints().toArray();
+
+ int[] rgb = ANSI.parseOscColorResponse(input, 11);
+
+ assertNotNull(rgb);
+ assertEquals(255, rgb[0]);
+ assertEquals(255, rgb[1]);
+ assertEquals(255, rgb[2]);
+ }
+
+ @Test
+ public void testParseOscColorResponse_BlackColor() {
+ // Test parsing black color (min values)
+ String response = "\u001B]11;rgb:0000/0000/0000\u0007";
+ int[] input = response.codePoints().toArray();
+
+ int[] rgb = ANSI.parseOscColorResponse(input, 11);
+
+ assertNotNull(rgb);
+ assertEquals(0, rgb[0]);
+ assertEquals(0, rgb[1]);
+ assertEquals(0, rgb[2]);
+ }
+
+ @Test
+ public void testParseOscColorResponse_LowercaseHex() {
+ // Some terminals might respond with lowercase hex
+ String response = "\u001B]10;rgb:abcd/ef01/2345\u0007";
+ int[] input = response.codePoints().toArray();
+
+ int[] rgb = ANSI.parseOscColorResponse(input, 10);
+
+ assertNotNull("Should parse lowercase hex", rgb);
+ assertEquals(0xab, rgb[0]); // abcd >> 8 = 0xab = 171
+ assertEquals(0xef, rgb[1]); // ef01 >> 8 = 0xef = 239
+ assertEquals(0x23, rgb[2]); // 2345 >> 8 = 0x23 = 35
+ }
+}
diff --git a/terminal-tty/src/main/java/org/aesh/terminal/tty/TerminalColorDetector.java b/terminal-tty/src/main/java/org/aesh/terminal/tty/TerminalColorDetector.java
index 99d656d..3debd8a 100644
--- a/terminal-tty/src/main/java/org/aesh/terminal/tty/TerminalColorDetector.java
+++ b/terminal-tty/src/main/java/org/aesh/terminal/tty/TerminalColorDetector.java
@@ -20,9 +20,6 @@
package org.aesh.terminal.tty;
import java.io.IOException;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -1591,7 +1588,10 @@ private static TerminalTheme parseJetBrainsColorScheme(java.io.File colorsFile)
* @return RGB array [r, g, b] (0-255 each), or null if not supported
*/
public static int[] queryForegroundColor(Connection connection, long timeoutMs) {
- return queryOscColor(connection, OSC_QUERY_FOREGROUND, 10, timeoutMs);
+ if (connection == null) {
+ return null;
+ }
+ return connection.queryForegroundColor(timeoutMs);
}
/**
@@ -1602,7 +1602,10 @@ public static int[] queryForegroundColor(Connection connection, long timeoutMs)
* @return RGB array [r, g, b] (0-255 each), or null if not supported
*/
public static int[] queryBackgroundColor(Connection connection, long timeoutMs) {
- return queryOscColor(connection, OSC_QUERY_BACKGROUND, 11, timeoutMs);
+ if (connection == null) {
+ return null;
+ }
+ return connection.queryBackgroundColor(timeoutMs);
}
/**
@@ -2102,138 +2105,6 @@ private static int[] parseOscColorFromString(String response, int oscCode) {
}
}
- /**
- * Query an OSC color from the terminal.
- *
- * @param connection the terminal connection
- * @param query the OSC query sequence
- * @param oscCode the expected OSC code in response (10 or 11)
- * @param timeoutMs timeout in milliseconds
- * @return RGB array [r, g, b] (0-255 each), or null if not supported
- */
- private static int[] queryOscColor(Connection connection, String query, int oscCode, long timeoutMs) {
- if (connection == null || !connection.supportsAnsi()) {
- return null;
- }
-
- CountDownLatch latch = new CountDownLatch(1);
- final int[][] result = { null };
-
- Consumer prevHandler = connection.getStdinHandler();
- connection.setStdinHandler(input -> {
- int[] parsed = parseOscColorResponse(input, oscCode);
- if (parsed != null) {
- result[0] = parsed;
- latch.countDown();
- }
- });
-
- try {
- // Send the query
- connection.stdoutHandler().accept(query.codePoints().toArray());
-
- // Wait for response with timeout
- if (!latch.await(timeoutMs, TimeUnit.MILLISECONDS)) {
- LOGGER.log(Level.FINE, "Timeout waiting for OSC color response");
- }
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- LOGGER.log(Level.FINE, "Interrupted while waiting for OSC color response", e);
- } finally {
- // Restore previous handler
- connection.setStdinHandler(prevHandler);
- }
-
- return result[0];
- }
-
- /**
- * Parse an OSC color response.
- *
- * Expected format: ESC ] {oscCode} ; rgb:RRRR/GGGG/BBBB {ST}
- * Where:
- *
- * - ESC is 0x1B (27)
- * - oscCode is 10 (foreground) or 11 (background)
- * - RRRR, GGGG, BBBB are 4-digit hex values
- * - ST is either BEL (0x07) or ESC \ (0x1B 0x5C)
- *
- *
- * @param input the input sequence
- * @param oscCode the expected OSC code
- * @return RGB array [r, g, b] (0-255 each), or null if parsing failed
- */
- 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;
- }
-
- // 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 + 4, 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) {
- LOGGER.log(Level.FINE, "Failed to parse OSC color response: " + rgbPart, e);
- return null;
- }
- }
-
/**
* Check if the terminal likely supports OSC color queries.
*