From 11d2c33bdb03a78756c1c867a648a0d17660195c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Pedersen?= Date: Thu, 5 Feb 2026 17:49:33 +0100 Subject: [PATCH] Added support for DA1/DA2 feature detection. --- .../java/org/aesh/terminal/Connection.java | 127 ++++++ .../org/aesh/terminal/DeviceAttributes.java | 422 ++++++++++++++++++ .../terminal/image/ImageProtocolDetector.java | 69 +++ .../java/org/aesh/terminal/utils/ANSI.java | 164 +++++++ .../aesh/terminal/ConnectionOscQueryTest.java | 2 +- .../aesh/terminal/DeviceAttributesTest.java | 419 +++++++++++++++++ 6 files changed, 1202 insertions(+), 1 deletion(-) create mode 100644 terminal-api/src/main/java/org/aesh/terminal/DeviceAttributes.java create mode 100644 terminal-api/src/test/java/org/aesh/terminal/DeviceAttributesTest.java 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 d6afd9c..3009986 100644 --- a/terminal-api/src/main/java/org/aesh/terminal/Connection.java +++ b/terminal-api/src/main/java/org/aesh/terminal/Connection.java @@ -24,6 +24,8 @@ import java.util.concurrent.CountDownLatch; import java.util.function.Consumer; +import org.aesh.terminal.image.ImageProtocol; +import org.aesh.terminal.image.ImageProtocolDetector; import org.aesh.terminal.tty.Capability; import org.aesh.terminal.tty.Point; import org.aesh.terminal.tty.Signal; @@ -319,6 +321,9 @@ default T queryTerminal(String query, long timeoutMs, *

* This checks both the device type and environment variables to determine * if OSC queries like color detection are likely to work. + *

+ * For more accurate detection, use {@link #supportsOscQueries(DeviceAttributes)} + * with DA1 query results. * * @return true if OSC queries are likely supported */ @@ -340,6 +345,41 @@ default boolean supportsOscQueries() { return supportsAnsi(); } + /** + * Check if OSC queries are supported, using DA1 device attributes for + * improved detection. + *

+ * This method uses the device attributes from a DA1 query to provide + * more accurate OSC support detection. If the terminal reports modern + * features like ANSI color or Sixel graphics, it likely supports OSC queries. + * + * @param attrs the device attributes from DA1 query (may be null) + * @return true if OSC queries are likely supported + */ + default boolean supportsOscQueries(DeviceAttributes attrs) { + // If we have DA1 data, use it for better detection + if (attrs != null && attrs.likelySupportsOscQueries()) { + return true; + } + + // Fall back to heuristic detection + return supportsOscQueries(); + } + + /** + * Query the terminal to check if OSC queries are supported. + *

+ * This method sends a DA1 query to get device attributes and uses them + * to determine OSC support more accurately than heuristic detection. + * + * @param timeoutMs timeout in milliseconds for the DA1 query + * @return true if OSC queries are likely supported + */ + default boolean querySupportsOscQueries(long timeoutMs) { + DeviceAttributes attrs = queryPrimaryDeviceAttributes(timeoutMs); + return supportsOscQueries(attrs); + } + /** * Get the color depth of this terminal connection. *

@@ -488,4 +528,91 @@ default int[] queryPaletteColor(int index, long timeoutMs) { input -> ANSI.parseOscColorResponse(input, ANSI.OSC_PALETTE, index)); } + // ==================== Device Attributes (DA1/DA2) ==================== + + /** + * Query the terminal for its primary device attributes (DA1). + *

+ * DA1 returns the device conformance level and supported features. + * This can be used to detect capabilities like: + *

+ *

+ * The terminal must be actively reading input for this to work. + * + * @param timeoutMs timeout in milliseconds to wait for response + * @return DeviceAttributes with DA1 data, or null if not supported or timeout + */ + default DeviceAttributes queryPrimaryDeviceAttributes(long timeoutMs) { + return queryTerminal(ANSI.DA1_QUERY, timeoutMs, ANSI::parseDA1Response); + } + + /** + * Query the terminal for its secondary device attributes (DA2). + *

+ * DA2 returns terminal identification information: + *

+ *

+ * Note: Not all terminals support DA2. Some may return nothing or + * the same response as DA1. + *

+ * The terminal must be actively reading input for this to work. + * + * @param timeoutMs timeout in milliseconds to wait for response + * @return DeviceAttributes with DA2 data, or null if not supported or timeout + */ + default DeviceAttributes querySecondaryDeviceAttributes(long timeoutMs) { + return queryTerminal(ANSI.DA2_QUERY, timeoutMs, ANSI::parseDA2Response); + } + + /** + * Query the terminal for both primary and secondary device attributes. + *

+ * This sends both DA1 and DA2 queries and merges the results into a + * single DeviceAttributes object containing all available information. + *

+ * The terminal must be actively reading input for this to work. + * + * @param timeoutMs timeout in milliseconds to wait for each response + * @return DeviceAttributes with merged DA1 and DA2 data, or null if neither succeeded + */ + default DeviceAttributes queryDeviceAttributes(long timeoutMs) { + DeviceAttributes da1 = queryPrimaryDeviceAttributes(timeoutMs); + DeviceAttributes da2 = querySecondaryDeviceAttributes(timeoutMs); + + if (da1 != null && da2 != null) { + return da1.merge(da2); + } else if (da1 != null) { + return da1; + } else { + return da2; + } + } + + /** + * Query the terminal for its image protocol support. + *

+ * This method sends a DA1 query to detect Sixel support authoritatively, + * and combines it with heuristic detection based on terminal type. + *

+ * For faster (but less accurate) detection without querying the terminal, + * use {@link Device#getImageProtocol()} instead. + * + * @param timeoutMs timeout in milliseconds to wait for DA1 response + * @return the detected image protocol + */ + default ImageProtocol queryImageProtocol(long timeoutMs) { + DeviceAttributes attrs = queryPrimaryDeviceAttributes(timeoutMs); + String termType = device() != null ? device().type() : null; + return ImageProtocolDetector.detect(attrs, termType); + } + } diff --git a/terminal-api/src/main/java/org/aesh/terminal/DeviceAttributes.java b/terminal-api/src/main/java/org/aesh/terminal/DeviceAttributes.java new file mode 100644 index 0000000..f48b472 --- /dev/null +++ b/terminal-api/src/main/java/org/aesh/terminal/DeviceAttributes.java @@ -0,0 +1,422 @@ +/* + * 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 java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Represents device attributes queried from a terminal via DA1/DA2 sequences. + *

+ * Device attributes provide information about terminal capabilities that + * cannot be determined from terminfo alone. This includes: + *

+ *

+ * Use {@link org.aesh.terminal.Connection#queryDeviceAttributes(long)} to + * obtain an instance of this class. + * + * @author Ståle Pedersen + */ +public class DeviceAttributes { + + /** + * Feature flags that can be reported in DA1 response. + */ + public enum Feature { + /** 132-column mode (Ps=1) */ + COLUMNS_132(1), + /** Printer port (Ps=2) */ + PRINTER(2), + /** ReGIS graphics (Ps=3) */ + REGIS_GRAPHICS(3), + /** Sixel graphics (Ps=4) */ + SIXEL(4), + /** Selective erase (Ps=6) */ + SELECTIVE_ERASE(6), + /** Soft character set / DRCS (Ps=7) */ + DRCS(7), + /** User-defined keys (Ps=8) */ + USER_DEFINED_KEYS(8), + /** National replacement character sets (Ps=9) */ + NATIONAL_CHARSETS(9), + /** Technical character set (Ps=15) */ + TECHNICAL_CHARSETS(15), + /** Locator port / mouse (Ps=16) - DEC locator */ + LOCATOR(16), + /** Terminal state interrogation (Ps=17) */ + STATE_INTERROGATION(17), + /** User windows (Ps=18) */ + USER_WINDOWS(18), + /** Horizontal scrolling (Ps=21) */ + HORIZONTAL_SCROLLING(21), + /** ANSI color (Ps=22) */ + ANSI_COLOR(22), + /** Rectangular editing (Ps=28) */ + RECTANGULAR_EDITING(28), + /** ANSI text locator / mouse (Ps=29) */ + ANSI_TEXT_LOCATOR(29); + + private final int code; + + Feature(int code) { + this.code = code; + } + + public int getCode() { + return code; + } + + /** + * Find a Feature by its DA1 parameter code. + * + * @param code the DA1 parameter code + * @return the Feature, or null if not recognized + */ + public static Feature fromCode(int code) { + for (Feature f : values()) { + if (f.code == code) { + return f; + } + } + return null; + } + } + + /** + * Terminal type identifiers from DA2 response. + */ + public enum TerminalType { + VT100(0, "VT100"), + VT220(1, "VT220"), + VT240(2, "VT240"), + VT330(18, "VT330"), + VT340(19, "VT340"), + VT320(24, "VT320"), + VT382(32, "VT382"), + VT420(41, "VT420"), + VT510(61, "VT510"), + VT520(64, "VT520"), + VT525(65, "VT525"), + XTERM(0, "xterm"), // xterm uses 0 but different version format + UNKNOWN(-1, "Unknown"); + + private final int code; + private final String name; + + TerminalType(int code, String name) { + this.code = code; + this.name = name; + } + + public int getCode() { + return code; + } + + public String getName() { + return name; + } + + /** + * Find a TerminalType by its DA2 type code. + * Note: Code 0 could be VT100 or xterm depending on version format. + * + * @param code the DA2 type code + * @return the TerminalType, or UNKNOWN if not recognized + */ + public static TerminalType fromCode(int code) { + for (TerminalType t : values()) { + if (t.code == code && t != XTERM && t != UNKNOWN) { + return t; + } + } + return UNKNOWN; + } + } + + // DA1 (Primary Device Attributes) data + private final int deviceClass; + private final Set features; + private final Set rawParameters; + + // DA2 (Secondary Device Attributes) data + private final TerminalType terminalType; + private final int firmwareVersion; + private final int romCartridge; + + /** + * Create DeviceAttributes from DA1 response data. + * + * @param deviceClass the device class (conformance level) + * @param parameters the feature parameter codes from DA1 response + */ + public DeviceAttributes(int deviceClass, Set parameters) { + this.deviceClass = deviceClass; + this.rawParameters = parameters != null ? new HashSet<>(parameters) : new HashSet<>(); + this.features = parseFeatures(this.rawParameters); + this.terminalType = TerminalType.UNKNOWN; + this.firmwareVersion = -1; + this.romCartridge = -1; + } + + /** + * Create DeviceAttributes from both DA1 and DA2 response data. + * + * @param deviceClass the device class from DA1 + * @param parameters the feature parameters from DA1 + * @param terminalType the terminal type from DA2 + * @param firmwareVersion the firmware version from DA2 + * @param romCartridge the ROM cartridge registration from DA2 + */ + public DeviceAttributes(int deviceClass, Set parameters, + TerminalType terminalType, int firmwareVersion, int romCartridge) { + this.deviceClass = deviceClass; + this.rawParameters = parameters != null ? new HashSet<>(parameters) : new HashSet<>(); + this.features = parseFeatures(this.rawParameters); + this.terminalType = terminalType != null ? terminalType : TerminalType.UNKNOWN; + this.firmwareVersion = firmwareVersion; + this.romCartridge = romCartridge; + } + + private Set parseFeatures(Set parameters) { + Set result = new HashSet<>(); + for (Integer code : parameters) { + Feature f = Feature.fromCode(code); + if (f != null) { + result.add(f); + } + } + return result; + } + + /** + * Get the device class / conformance level. + *

+ * Common values: + *

+ * + * @return the device class, or -1 if not available + */ + public int getDeviceClass() { + return deviceClass; + } + + /** + * Get the set of features reported by the terminal. + * + * @return unmodifiable set of features + */ + public Set getFeatures() { + return Collections.unmodifiableSet(features); + } + + /** + * Get the raw parameter codes from DA1 response. + *

+ * This includes both recognized and unrecognized parameters. + * + * @return unmodifiable set of parameter codes + */ + public Set getRawParameters() { + return Collections.unmodifiableSet(rawParameters); + } + + /** + * Get the terminal type from DA2 response. + * + * @return the terminal type + */ + public TerminalType getTerminalType() { + return terminalType; + } + + /** + * Get the firmware version from DA2 response. + * + * @return the firmware version, or -1 if not available + */ + public int getFirmwareVersion() { + return firmwareVersion; + } + + /** + * Get the ROM cartridge registration number from DA2. + * + * @return the ROM cartridge number, or -1 if not available + */ + public int getRomCartridge() { + return romCartridge; + } + + /** + * Check if the terminal supports a specific feature. + * + * @param feature the feature to check + * @return true if the feature is supported + */ + public boolean hasFeature(Feature feature) { + return features.contains(feature); + } + + /** + * Check if the terminal supports Sixel graphics. + * + * @return true if Sixel is supported + */ + public boolean supportsSixel() { + return hasFeature(Feature.SIXEL); + } + + /** + * Check if the terminal supports ANSI colors. + * + * @return true if ANSI color is supported + */ + public boolean supportsAnsiColor() { + return hasFeature(Feature.ANSI_COLOR); + } + + /** + * Check if the terminal supports mouse/locator. + *

+ * This checks for DEC locator (Ps=16) or ANSI text locator (Ps=29). + * + * @return true if mouse/locator is supported + */ + public boolean supportsMouse() { + return hasFeature(Feature.LOCATOR) || hasFeature(Feature.ANSI_TEXT_LOCATOR); + } + + /** + * Check if the terminal supports rectangular editing operations. + * + * @return true if rectangular editing is supported + */ + public boolean supportsRectangularEditing() { + return hasFeature(Feature.RECTANGULAR_EDITING); + } + + /** + * Check if the terminal supports 132-column mode. + * + * @return true if 132-column mode is supported + */ + public boolean supports132Columns() { + return hasFeature(Feature.COLUMNS_132); + } + + /** + * Check if DA1 data is available. + * + * @return true if DA1 was successfully queried + */ + public boolean hasDA1() { + return deviceClass >= 0; + } + + /** + * Check if DA2 data is available. + * + * @return true if DA2 was successfully queried + */ + public boolean hasDA2() { + return terminalType != TerminalType.UNKNOWN || firmwareVersion >= 0; + } + + /** + * Check if the terminal likely supports OSC (Operating System Command) queries. + *

+ * This is inferred from DA1 capabilities. Terminals that report modern features + * like ANSI color or Sixel graphics typically also support OSC queries for + * color detection (OSC 10/11), clipboard (OSC 52), etc. + *

+ * Note: This is an inference, not a guarantee. Some terminals may report + * these features but not support all OSC commands. + * + * @return true if OSC queries are likely supported based on DA1 features + */ + public boolean likelySupportsOscQueries() { + // Terminals reporting these modern features typically support OSC + // Device class >= 62 indicates VT220+ which generally supports OSC + return supportsAnsiColor() || supportsSixel() || deviceClass >= 62; + } + + /** + * Merge this DeviceAttributes with another, combining data from both. + *

+ * This is useful when DA1 and DA2 are queried separately. + * + * @param other the other DeviceAttributes to merge with + * @return a new DeviceAttributes with combined data + */ + public DeviceAttributes merge(DeviceAttributes other) { + if (other == null) { + return this; + } + + int mergedClass = this.deviceClass >= 0 ? this.deviceClass : other.deviceClass; + Set mergedParams = new HashSet<>(this.rawParameters); + mergedParams.addAll(other.rawParameters); + + TerminalType mergedType = this.terminalType != TerminalType.UNKNOWN + ? this.terminalType + : other.terminalType; + int mergedVersion = this.firmwareVersion >= 0 + ? this.firmwareVersion + : other.firmwareVersion; + int mergedRom = this.romCartridge >= 0 + ? this.romCartridge + : other.romCartridge; + + return new DeviceAttributes(mergedClass, mergedParams, mergedType, mergedVersion, mergedRom); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("DeviceAttributes{"); + if (hasDA1()) { + sb.append("class=").append(deviceClass); + if (!features.isEmpty()) { + sb.append(", features=").append(features); + } + } + if (hasDA2()) { + if (hasDA1()) { + sb.append(", "); + } + sb.append("type=").append(terminalType.getName()); + if (firmwareVersion >= 0) { + sb.append(", version=").append(firmwareVersion); + } + } + sb.append("}"); + return sb.toString(); + } +} diff --git a/terminal-api/src/main/java/org/aesh/terminal/image/ImageProtocolDetector.java b/terminal-api/src/main/java/org/aesh/terminal/image/ImageProtocolDetector.java index b8d3f3e..3e7965b 100644 --- a/terminal-api/src/main/java/org/aesh/terminal/image/ImageProtocolDetector.java +++ b/terminal-api/src/main/java/org/aesh/terminal/image/ImageProtocolDetector.java @@ -19,8 +19,18 @@ */ package org.aesh.terminal.image; +import org.aesh.terminal.DeviceAttributes; + /** * Utility class for detecting terminal image protocol support. + *

+ * Detection can be done in two ways: + *

    + *
  • Heuristic: Based on TERM type and environment variables (fast, always available)
  • + *
  • Authoritative: Based on DA1 device attributes query (accurate, requires terminal query)
  • + *
+ *

+ * For best results, use {@link #detect(DeviceAttributes, String)} which combines both methods. */ public final class ImageProtocolDetector { @@ -28,8 +38,67 @@ private ImageProtocolDetector() { // Utility class } + /** + * Detect the image protocol using both device attributes and terminal type. + *

+ * This method provides the most accurate detection by: + *

    + *
  1. Checking DA1 attributes for Sixel support (authoritative)
  2. + *
  3. Falling back to heuristic detection based on terminal type
  4. + *
+ * + * @param attrs the device attributes from DA1 query (may be null) + * @param termType the terminal type string (may be null) + * @return the detected protocol, or NONE if unknown + */ + public static ImageProtocol detect(DeviceAttributes attrs, String termType) { + // First check environment for Kitty/iTerm2 (these don't report via DA1) + ImageProtocol envProtocol = checkEnvironment(); + + // Kitty and iTerm2 protocols take priority over Sixel + if (envProtocol == ImageProtocol.KITTY || envProtocol == ImageProtocol.ITERM2) { + return envProtocol; + } + + // Check terminal type for Kitty/iTerm2 support + if (termType != null) { + String typeLower = termType.toLowerCase(); + if (typeLower.contains("kitty") || typeLower.contains("ghostty")) { + return ImageProtocol.KITTY; + } + if (typeLower.contains("iterm") || typeLower.contains("wezterm") || + typeLower.contains("mintty") || typeLower.contains("vscode") || + typeLower.contains("tabby") || typeLower.contains("hyper")) { + return ImageProtocol.ITERM2; + } + if (typeLower.contains("konsole")) { + return ImageProtocol.KITTY; + } + } + + // Check DA1 for authoritative Sixel support (parameter 4) + if (attrs != null && attrs.supportsSixel()) { + return ImageProtocol.SIXEL; + } + + // Fall back to heuristic Sixel detection from terminal type + if (termType != null) { + String typeLower = termType.toLowerCase(); + if (typeLower.contains("mlterm") || typeLower.contains("foot") || + typeLower.contains("contour") || typeLower.contains("yaft") || + typeLower.contains("ctx") || typeLower.contains("darktile")) { + return ImageProtocol.SIXEL; + } + } + + return ImageProtocol.NONE; + } + /** * Detect the image protocol based on the terminal type string. + *

+ * This is a heuristic method that does not query the terminal. + * For more accurate detection, use {@link #detect(DeviceAttributes, String)}. * * @param termType the terminal type (e.g., from TERM environment variable) * @return the detected protocol, or NONE if unknown 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 bc1dce5..bc73633 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 @@ -21,6 +21,7 @@ import java.util.Arrays; +import org.aesh.terminal.DeviceAttributes; import org.aesh.terminal.tty.Point; /** @@ -744,4 +745,167 @@ public static int[] color256ToRgb(int index) { { 255, 255, 255 } // 15: Bright White }; + // ==================== Device Attributes (DA1/DA2) ==================== + + /** DA1 (Primary Device Attributes) query sequence. */ + public static final String DA1_QUERY = "\u001B[c"; + + /** DA2 (Secondary Device Attributes) query sequence. */ + public static final String DA2_QUERY = "\u001B[>c"; + + /** + * Parse a DA1 (Primary Device Attributes) response. + *

+ * Expected format: ESC [ ? Ps ; Ps ; ... c + *

+ * Where the first Ps is the device class/conformance level and + * subsequent Ps values are feature parameters. + * + * @param input the input sequence as code points + * @return DeviceAttributes parsed from DA1, or null if parsing failed + */ + public static DeviceAttributes parseDA1Response(int[] input) { + if (input == null || input.length < 4) { + 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 DA1 response pattern: ESC [ ? ... c + int start = response.indexOf("\u001B[?"); + if (start < 0) { + return null; + } + + // Find the terminating 'c' + int end = response.indexOf('c', start); + if (end < 0) { + return null; + } + + // Extract the parameters between "?" and "c" + String params = response.substring(start + 3, end); + if (params.isEmpty()) { + return null; + } + + // Parse semicolon-separated parameters + String[] parts = params.split(";"); + if (parts.length == 0) { + return null; + } + + try { + // First parameter is device class + int deviceClass = Integer.parseInt(parts[0].trim()); + + // Remaining parameters are feature codes + java.util.Set features = new java.util.HashSet<>(); + for (int i = 1; i < parts.length; i++) { + String part = parts[i].trim(); + if (!part.isEmpty()) { + features.add(Integer.parseInt(part)); + } + } + + return new DeviceAttributes(deviceClass, features); + } catch (NumberFormatException e) { + return null; + } + } + + /** + * Parse a DA2 (Secondary Device Attributes) response. + *

+ * Expected format: ESC [ > Pp ; Pv ; Pc c + *

+ * Where: + *

    + *
  • Pp is the terminal type (0=VT100, 1=VT220, etc.)
  • + *
  • Pv is the firmware version
  • + *
  • Pc is the ROM cartridge registration number
  • + *
+ * + * @param input the input sequence as code points + * @return DeviceAttributes parsed from DA2, or null if parsing failed + */ + public static DeviceAttributes parseDA2Response(int[] input) { + if (input == null || input.length < 4) { + 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 DA2 response pattern: ESC [ > ... c + int start = response.indexOf("\u001B[>"); + if (start < 0) { + return null; + } + + // Find the terminating 'c' + int end = response.indexOf('c', start); + if (end < 0) { + return null; + } + + // Extract the parameters between ">" and "c" + String params = response.substring(start + 3, end); + if (params.isEmpty()) { + return null; + } + + // Parse semicolon-separated parameters + String[] parts = params.split(";"); + + try { + int typeCode = parts.length > 0 && !parts[0].trim().isEmpty() + ? Integer.parseInt(parts[0].trim()) + : -1; + int version = parts.length > 1 && !parts[1].trim().isEmpty() + ? Integer.parseInt(parts[1].trim()) + : -1; + int rom = parts.length > 2 && !parts[2].trim().isEmpty() + ? Integer.parseInt(parts[2].trim()) + : -1; + + DeviceAttributes.TerminalType termType = DeviceAttributes.TerminalType.fromCode(typeCode); + + return new DeviceAttributes(-1, null, termType, version, rom); + } catch (NumberFormatException e) { + return null; + } + } + + /** + * Parse both DA1 and DA2 responses from a combined input. + *

+ * This is useful when both queries are sent together and responses + * arrive in sequence. + * + * @param input the input sequence containing both responses + * @return DeviceAttributes with merged DA1 and DA2 data, or null if parsing failed + */ + public static DeviceAttributes parseDAResponse(int[] input) { + DeviceAttributes da1 = parseDA1Response(input); + DeviceAttributes da2 = parseDA2Response(input); + + if (da1 != null && da2 != null) { + return da1.merge(da2); + } else if (da1 != null) { + return da1; + } else { + return da2; + } + } + } diff --git a/terminal-api/src/test/java/org/aesh/terminal/ConnectionOscQueryTest.java b/terminal-api/src/test/java/org/aesh/terminal/ConnectionOscQueryTest.java index 757cdd9..ab82b8b 100644 --- a/terminal-api/src/test/java/org/aesh/terminal/ConnectionOscQueryTest.java +++ b/terminal-api/src/test/java/org/aesh/terminal/ConnectionOscQueryTest.java @@ -150,7 +150,7 @@ public void testQueryCursorColorReturnsValue() throws Exception { Thread responseThread = new Thread(() -> { try { queryStarted.await(1, TimeUnit.SECONDS); - Thread.sleep(20); + Thread.sleep(50); // Increased from 20ms to avoid race condition String response = "\u001B]12;rgb:0000/FFFF/0000\u0007"; connection.simulateInput(response); } catch (InterruptedException e) { diff --git a/terminal-api/src/test/java/org/aesh/terminal/DeviceAttributesTest.java b/terminal-api/src/test/java/org/aesh/terminal/DeviceAttributesTest.java new file mode 100644 index 0000000..f31bafe --- /dev/null +++ b/terminal-api/src/test/java/org/aesh/terminal/DeviceAttributesTest.java @@ -0,0 +1,419 @@ +/* + * 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.util.HashSet; +import java.util.Set; + +import org.aesh.terminal.DeviceAttributes.Feature; +import org.aesh.terminal.DeviceAttributes.TerminalType; +import org.aesh.terminal.utils.ANSI; +import org.junit.Test; + +/** + * Tests for DeviceAttributes class and DA1/DA2 parsing. + * + * @author Ståle Pedersen + */ +public class DeviceAttributesTest { + + // ==================== DA1 Query Tests ==================== + + @Test + public void testDA1QueryConstant() { + assertEquals("\u001B[c", ANSI.DA1_QUERY); + } + + @Test + public void testParseDA1Response_VT220() { + // VT220 response: ESC [ ? 62 ; 1 ; 2 ; 6 ; 7 ; 8 ; 9 c + String response = "\u001B[?62;1;2;6;7;8;9c"; + int[] input = response.codePoints().toArray(); + + DeviceAttributes da = ANSI.parseDA1Response(input); + + assertNotNull("Should parse valid DA1 response", da); + assertEquals(62, da.getDeviceClass()); + assertTrue(da.hasDA1()); + assertFalse(da.hasDA2()); + + // Check features + assertTrue(da.supports132Columns()); // 1 + assertTrue(da.hasFeature(Feature.PRINTER)); // 2 + assertTrue(da.hasFeature(Feature.SELECTIVE_ERASE)); // 6 + assertTrue(da.hasFeature(Feature.DRCS)); // 7 + assertTrue(da.hasFeature(Feature.USER_DEFINED_KEYS)); // 8 + assertTrue(da.hasFeature(Feature.NATIONAL_CHARSETS)); // 9 + } + + @Test + public void testParseDA1Response_XtermWithSixel() { + // xterm with sixel: ESC [ ? 64 ; 4 ; 22 c + String response = "\u001B[?64;4;22c"; + int[] input = response.codePoints().toArray(); + + DeviceAttributes da = ANSI.parseDA1Response(input); + + assertNotNull(da); + assertEquals(64, da.getDeviceClass()); + assertTrue(da.supportsSixel()); // 4 + assertTrue(da.supportsAnsiColor()); // 22 + } + + @Test + public void testParseDA1Response_WithMouse() { + // Terminal with mouse support: ESC [ ? 62 ; 29 c + String response = "\u001B[?62;29c"; + int[] input = response.codePoints().toArray(); + + DeviceAttributes da = ANSI.parseDA1Response(input); + + assertNotNull(da); + assertTrue(da.supportsMouse()); // 29 = ANSI text locator + } + + @Test + public void testParseDA1Response_WithDecLocator() { + // Terminal with DEC locator: ESC [ ? 62 ; 16 c + String response = "\u001B[?62;16c"; + int[] input = response.codePoints().toArray(); + + DeviceAttributes da = ANSI.parseDA1Response(input); + + assertNotNull(da); + assertTrue(da.supportsMouse()); // 16 = DEC locator + assertTrue(da.hasFeature(Feature.LOCATOR)); + } + + @Test + public void testParseDA1Response_MinimalVT100() { + // Minimal VT100 response: ESC [ ? 1 ; 0 c + String response = "\u001B[?1;0c"; + int[] input = response.codePoints().toArray(); + + DeviceAttributes da = ANSI.parseDA1Response(input); + + assertNotNull(da); + assertEquals(1, da.getDeviceClass()); + } + + @Test + public void testParseDA1Response_ClassOnly() { + // Class only, no features: ESC [ ? 62 c + String response = "\u001B[?62c"; + int[] input = response.codePoints().toArray(); + + DeviceAttributes da = ANSI.parseDA1Response(input); + + assertNotNull(da); + assertEquals(62, da.getDeviceClass()); + assertTrue(da.getFeatures().isEmpty()); + } + + @Test + public void testParseDA1Response_WithPrefixNoise() { + // Response with noise before it + String response = "noise\u001B[?64;4c"; + int[] input = response.codePoints().toArray(); + + DeviceAttributes da = ANSI.parseDA1Response(input); + + assertNotNull("Should parse response with prefix noise", da); + assertEquals(64, da.getDeviceClass()); + assertTrue(da.supportsSixel()); + } + + @Test + public void testParseDA1Response_NullInput() { + assertNull(ANSI.parseDA1Response(null)); + } + + @Test + public void testParseDA1Response_EmptyInput() { + assertNull(ANSI.parseDA1Response(new int[0])); + } + + @Test + public void testParseDA1Response_InvalidFormat() { + // Missing ? after [ + String response = "\u001B[62;4c"; + int[] input = response.codePoints().toArray(); + + assertNull(ANSI.parseDA1Response(input)); + } + + // ==================== DA2 Query Tests ==================== + + @Test + public void testDA2QueryConstant() { + assertEquals("\u001B[>c", ANSI.DA2_QUERY); + } + + @Test + public void testParseDA2Response_Xterm() { + // xterm response: ESC [ > 0 ; 136 ; 0 c (type 0, version 136) + String response = "\u001B[>0;136;0c"; + int[] input = response.codePoints().toArray(); + + DeviceAttributes da = ANSI.parseDA2Response(input); + + assertNotNull("Should parse valid DA2 response", da); + assertFalse(da.hasDA1()); + assertTrue(da.hasDA2()); + assertEquals(136, da.getFirmwareVersion()); + assertEquals(0, da.getRomCartridge()); + } + + @Test + public void testParseDA2Response_VT220() { + // VT220 response: ESC [ > 1 ; 10 ; 0 c + String response = "\u001B[>1;10;0c"; + int[] input = response.codePoints().toArray(); + + DeviceAttributes da = ANSI.parseDA2Response(input); + + assertNotNull(da); + assertEquals(TerminalType.VT220, da.getTerminalType()); + assertEquals(10, da.getFirmwareVersion()); + } + + @Test + public void testParseDA2Response_VT420() { + // VT420 response: ESC [ > 41 ; 1 ; 0 c + String response = "\u001B[>41;1;0c"; + int[] input = response.codePoints().toArray(); + + DeviceAttributes da = ANSI.parseDA2Response(input); + + assertNotNull(da); + assertEquals(TerminalType.VT420, da.getTerminalType()); + } + + @Test + public void testParseDA2Response_UnknownType() { + // Unknown terminal type + String response = "\u001B[>99;50;0c"; + int[] input = response.codePoints().toArray(); + + DeviceAttributes da = ANSI.parseDA2Response(input); + + assertNotNull(da); + assertEquals(TerminalType.UNKNOWN, da.getTerminalType()); + assertEquals(50, da.getFirmwareVersion()); + } + + @Test + public void testParseDA2Response_NullInput() { + assertNull(ANSI.parseDA2Response(null)); + } + + @Test + public void testParseDA2Response_InvalidFormat() { + // Missing > after [ + String response = "\u001B[0;136;0c"; + int[] input = response.codePoints().toArray(); + + assertNull(ANSI.parseDA2Response(input)); + } + + // ==================== Combined DA Response Tests ==================== + + @Test + public void testParseDAResponse_BothDA1AndDA2() { + // Combined response with both DA1 and DA2 + String response = "\u001B[?64;4;22c\u001B[>0;136;0c"; + int[] input = response.codePoints().toArray(); + + DeviceAttributes da = ANSI.parseDAResponse(input); + + assertNotNull(da); + assertTrue(da.hasDA1()); + assertTrue(da.hasDA2()); + assertEquals(64, da.getDeviceClass()); + assertTrue(da.supportsSixel()); + assertTrue(da.supportsAnsiColor()); + assertEquals(136, da.getFirmwareVersion()); + } + + @Test + public void testParseDAResponse_OnlyDA1() { + String response = "\u001B[?64;4c"; + int[] input = response.codePoints().toArray(); + + DeviceAttributes da = ANSI.parseDAResponse(input); + + assertNotNull(da); + assertTrue(da.hasDA1()); + assertFalse(da.hasDA2()); + } + + @Test + public void testParseDAResponse_OnlyDA2() { + String response = "\u001B[>1;10;0c"; + int[] input = response.codePoints().toArray(); + + DeviceAttributes da = ANSI.parseDAResponse(input); + + assertNotNull(da); + assertFalse(da.hasDA1()); + assertTrue(da.hasDA2()); + } + + // ==================== DeviceAttributes Class Tests ==================== + + @Test + public void testDeviceAttributesMerge() { + Set features = new HashSet<>(); + features.add(4); // Sixel + features.add(22); // ANSI color + + DeviceAttributes da1 = new DeviceAttributes(64, features); + DeviceAttributes da2 = new DeviceAttributes(-1, null, + TerminalType.VT420, 100, 0); + + DeviceAttributes merged = da1.merge(da2); + + assertEquals(64, merged.getDeviceClass()); + assertTrue(merged.supportsSixel()); + assertTrue(merged.supportsAnsiColor()); + assertEquals(TerminalType.VT420, merged.getTerminalType()); + assertEquals(100, merged.getFirmwareVersion()); + } + + @Test + public void testDeviceAttributesMerge_Null() { + Set features = new HashSet<>(); + features.add(4); + + DeviceAttributes da = new DeviceAttributes(64, features); + DeviceAttributes merged = da.merge(null); + + assertSame(da, merged); + } + + @Test + public void testFeatureFromCode() { + assertEquals(Feature.SIXEL, Feature.fromCode(4)); + assertEquals(Feature.ANSI_COLOR, Feature.fromCode(22)); + assertEquals(Feature.ANSI_TEXT_LOCATOR, Feature.fromCode(29)); + assertNull(Feature.fromCode(999)); + } + + @Test + public void testTerminalTypeFromCode() { + assertEquals(TerminalType.VT100, TerminalType.fromCode(0)); + assertEquals(TerminalType.VT220, TerminalType.fromCode(1)); + assertEquals(TerminalType.VT420, TerminalType.fromCode(41)); + assertEquals(TerminalType.UNKNOWN, TerminalType.fromCode(999)); + } + + @Test + public void testDeviceAttributesToString() { + Set features = new HashSet<>(); + features.add(4); + features.add(22); + + DeviceAttributes da = new DeviceAttributes(64, features, + TerminalType.VT420, 100, 0); + + String str = da.toString(); + assertTrue(str.contains("class=64")); + assertTrue(str.contains("SIXEL")); + assertTrue(str.contains("VT420")); + assertTrue(str.contains("version=100")); + } + + @Test + public void testRawParameters() { + Set features = new HashSet<>(); + features.add(4); + features.add(999); // Unknown feature code + + DeviceAttributes da = new DeviceAttributes(64, features); + + // Raw parameters should include unknown code + assertTrue(da.getRawParameters().contains(999)); + // But features set should not + assertFalse(da.getFeatures().contains(Feature.fromCode(999))); + // Known feature should be in both + assertTrue(da.getRawParameters().contains(4)); + assertTrue(da.hasFeature(Feature.SIXEL)); + } + + // ==================== OSC Support Inference Tests ==================== + + @Test + public void testLikelySupportsOscQueries_WithAnsiColor() { + Set features = new HashSet<>(); + features.add(22); // ANSI_COLOR + + DeviceAttributes da = new DeviceAttributes(64, features); + + assertTrue("Terminal with ANSI color should likely support OSC", + da.likelySupportsOscQueries()); + } + + @Test + public void testLikelySupportsOscQueries_WithSixel() { + Set features = new HashSet<>(); + features.add(4); // SIXEL + + DeviceAttributes da = new DeviceAttributes(1, features); + + assertTrue("Terminal with Sixel should likely support OSC", + da.likelySupportsOscQueries()); + } + + @Test + public void testLikelySupportsOscQueries_VT220Plus() { + // VT220 (class 62) and above typically support OSC + DeviceAttributes da = new DeviceAttributes(62, new HashSet<>()); + + assertTrue("VT220+ terminals should likely support OSC", + da.likelySupportsOscQueries()); + } + + @Test + public void testLikelySupportsOscQueries_VT100() { + // VT100 (class 1) without modern features - less likely to support OSC + DeviceAttributes da = new DeviceAttributes(1, new HashSet<>()); + + assertFalse("Basic VT100 without features should not indicate OSC support", + da.likelySupportsOscQueries()); + } + + @Test + public void testLikelySupportsOscQueries_ModernTerminal() { + // Modern terminal with multiple features + Set features = new HashSet<>(); + features.add(4); // Sixel + features.add(22); // ANSI color + features.add(29); // Mouse + + DeviceAttributes da = new DeviceAttributes(64, features); + + assertTrue(da.likelySupportsOscQueries()); + assertTrue(da.supportsSixel()); + assertTrue(da.supportsAnsiColor()); + assertTrue(da.supportsMouse()); + } +}