Color Detection

Æsh Readline provides automatic terminal color detection to help your application adapt its color scheme to the user’s terminal environment.

Overview

The TerminalColorDetector API detects:

  • Color Depth - How many colors the terminal supports (8, 16, 256, or true color)
  • Theme - Whether the terminal has a light or dark background
  • RGB Colors - The actual foreground and background colors (when available)

This allows your application to automatically choose appropriate colors that will be readable on any terminal background.

Quick Start

Fast Detection (Environment Only)

For immediate, non-blocking detection based on environment variables:

import org.aesh.terminal.utils.TerminalColorCapability;

TerminalColorCapability cap = TerminalColorCapability.detectFromEnvironment();

if (cap.getTheme().isDark()) {
    // Use light text colors
} else {
    // Use dark text colors
}

Full Detection (With Terminal Query)

For more accurate detection that queries the terminal:

import org.aesh.readline.terminal.TerminalColorDetector;
import org.aesh.readline.tty.terminal.TerminalConnection;

TerminalConnection connection = new TerminalConnection();
TerminalColorCapability cap = TerminalColorDetector.detect(connection);

System.out.println("Theme: " + cap.getTheme());
System.out.println("Color depth: " + cap.getColorDepth());

Cached Detection

For repeated access without re-detection overhead:

// Detects once and caches for 5 minutes
TerminalColorCapability cap = TerminalColorDetector.detectCached(connection);

TerminalColorCapability

The TerminalColorCapability class encapsulates all detected information:

TerminalColorCapability cap = TerminalColorDetector.detect(connection);

// Theme detection
TerminalTheme theme = cap.getTheme();        // DARK, LIGHT, or UNKNOWN
boolean isDark = cap.getTheme().isDark();    // true for DARK or UNKNOWN

// Color depth
ColorDepth depth = cap.getColorDepth();
boolean has256 = depth.supports256Colors();
boolean hasTrueColor = depth.supportsTrueColor();

// Actual RGB colors (may be null if not detectable)
int[] bgRGB = cap.getBackgroundRGB();  // [r, g, b] or null
int[] fgRGB = cap.getForegroundRGB();  // [r, g, b] or null

Suggested Color Codes

Get ANSI color codes that work well with the detected theme:

TerminalColorCapability cap = TerminalColorDetector.detect(connection);

// These return appropriate codes for the detected background
int normalText = cap.getSuggestedForegroundCode();  // 30 (black) or 37 (white)
int errorText = cap.getSuggestedErrorCode();        // 31 or 91 (red variants)
int successText = cap.getSuggestedSuccessCode();    // 32 or 92 (green variants)
int warningText = cap.getSuggestedWarningCode();    // 33 or 93 (yellow variants)
int infoText = cap.getSuggestedInfoCode();          // 34 or 94 (blue variants)

// Use in ANSI escape sequences
connection.write("\u001B[" + errorText + "mError: Something went wrong\u001B[0m\n");

ColorDepth

The ColorDepth enum represents terminal color capabilities:

ValueColorsDescription
NO_COLOR0No color support
COLORS_88Basic ANSI colors (30-37)
COLORS_1616Extended ANSI colors (30-37, 90-97)
COLORS_256256256-color palette (38;5;N)
TRUE_COLOR16M24-bit RGB (38;2;R;G;B)
ColorDepth depth = cap.getColorDepth();

if (depth.supportsTrueColor()) {
    // Use full RGB colors
    connection.write("\u001B[38;2;255;128;0mOrange text\u001B[0m");
} else if (depth.supports256Colors()) {
    // Use 256-color palette
    connection.write("\u001B[38;5;208mOrange text\u001B[0m");
} else {
    // Fall back to basic colors
    connection.write("\u001B[33mYellow text\u001B[0m");
}

TerminalTheme

The TerminalTheme enum represents the terminal’s background brightness:

ValueDescription
DARKDark background (use light text)
LIGHTLight background (use dark text)
UNKNOWNCould not detect (assumes dark)
TerminalTheme theme = cap.getTheme();

switch (theme) {
    case DARK:
        // Use bright/light colors for text
        break;
    case LIGHT:
        // Use dark colors for text
        break;
    case UNKNOWN:
        // Default to dark theme assumption
        break;
}

// Helper method - returns true for DARK and UNKNOWN
if (theme.isDark()) {
    // Use light colors
}

Detection Methods

The detector uses multiple strategies to determine the terminal theme:

1. OSC Color Queries (Most Accurate)

Queries the terminal directly for its background color using OSC 10/11 escape sequences:

ESC ] 11 ; ? BEL  →  ESC ] 11 ; rgb:RRRR/GGGG/BBBB BEL

This works with most modern terminal emulators including:

  • iTerm2, Kitty, WezTerm, Alacritty, Ghostty
  • GNOME Terminal, Konsole, xterm
  • Windows Terminal

2. Environment Variables

Checks standard environment variables:

VariableExampleMeaning
COLORFGBG15;0Foreground;Background color indices
COLORTERMtruecolorColor depth hint
APPLE_INTERFACE_STYLEDarkmacOS dark mode

3. Terminal-Specific Detection

For terminals that don’t support OSC queries, the detector reads configuration files:

Visual Studio Code

Reads settings.json for workbench.colorTheme:

  • Linux: ~/.config/Code/User/settings.json
  • macOS: ~/Library/Application Support/Code/User/settings.json
  • Windows: %APPDATA%\Code\User\settings.json

JetBrains IDEs (IntelliJ, PyCharm, etc.)

Reads colors.scheme.xml for the color scheme:

  • Linux: ~/.config/JetBrains/<Product>/options/colors.scheme.xml
  • macOS: ~/Library/Application Support/JetBrains/<Product>/options/colors.scheme.xml
  • Windows: %APPDATA%\JetBrains\<Product>\options\colors.scheme.xml

Alacritty

Reads alacritty.toml or alacritty.yml:

  • Linux/macOS: ~/.config/alacritty/alacritty.toml
  • Windows: %APPDATA%\alacritty\alacritty.toml

Windows Terminal

Reads settings.json for colorScheme:

  • %LOCALAPPDATA%\Packages\Microsoft.WindowsTerminal_*/LocalState/settings.json

ConEmu/Cmder

Reads ConEmu.xml for palette settings.

Windows System Dark Mode

Queries the Windows registry for AppsUseLightTheme.

Multiplexer Support (tmux/screen)

When running inside tmux or GNU Screen, color detection requires special handling:

// Check if running in a multiplexer
if (TerminalColorDetector.isRunningInTmux()) {
    System.out.println("Running inside tmux");
}

if (TerminalColorDetector.isRunningInMultiplexer()) {
    System.out.println("Running inside tmux or screen");
}

tmux Passthrough

For tmux versions before 3.3, OSC queries need to be wrapped in DCS passthrough sequences. The detector handles this automatically when:

  1. tmux 3.3+ is detected (native OSC support)
  2. allow-passthrough is enabled in tmux config
  3. TMUX_PASSTHROUGH=1 environment variable is set

To enable passthrough in tmux:

# In tmux.conf
set -g allow-passthrough on

# Or at runtime
tmux set -g allow-passthrough on

Example Application

Here’s a complete example that adapts colors based on detection:

import org.aesh.readline.terminal.TerminalColorDetector;
import org.aesh.readline.tty.terminal.TerminalConnection;
import org.aesh.terminal.utils.TerminalColorCapability;

public class AdaptiveColorApp {
    private static final String RESET = "\u001B[0m";
    
    public static void main(String[] args) throws Exception {
        TerminalConnection connection = new TerminalConnection();
        TerminalColorCapability cap = TerminalColorDetector.detect(connection);
        
        // Get theme-appropriate colors
        int error = cap.getSuggestedErrorCode();
        int success = cap.getSuggestedSuccessCode();
        int info = cap.getSuggestedInfoCode();
        
        // Display messages with appropriate colors
        connection.write("\u001B[" + info + "m[INFO]\u001B[0m Application started\n");
        connection.write("\u001B[" + success + "m[OK]\u001B[0m Configuration loaded\n");
        connection.write("\u001B[" + error + "m[ERROR]\u001B[0m Unable to connect\n");
        
        // Show detection results
        connection.write("\nDetected: " + cap.getTheme() + " theme, " + 
                        cap.getColorDepth() + "\n");
        
        if (cap.hasBackgroundColor()) {
            int[] bg = cap.getBackgroundRGB();
            connection.write("Background: RGB(" + bg[0] + "," + bg[1] + "," + bg[2] + ")\n");
        }
        
        connection.close();
    }
}

Performance Considerations

Detection Timing

MethodTimeBlocking
detectFromEnvironment()< 1msNo
detectFast()< 1msNo
detect()50-200msYes (waits for terminal response)
detectCached()< 1ms (after first call)Only on first call

Recommendations

  1. Use detectCached() for most applications - it caches results for 5 minutes
  2. Use detectFast() if you can’t wait for terminal queries
  3. Call detection once at startup, not on every output
  4. Respect user overrides - allow users to specify theme preference
// Good: Detect once at startup
public class MyApp {
    private static TerminalColorCapability colors;
    
    public static void main(String[] args) {
        TerminalConnection conn = new TerminalConnection();
        colors = TerminalColorDetector.detectCached(conn);
        // Use 'colors' throughout the application
    }
}

Supported Terminals

The following terminals have been tested with full detection support:

TerminalOSC QueryConfig FileNotes
iTerm2Yes-Full support
KittyYes-Full support
WezTermYes-Full support
AlacrittyYesYesBoth methods work
GhosttyYes-Full support
GNOME TerminalYes-Full support
KonsoleYes-Full support
Windows TerminalYesYesBoth methods work
VS Code TerminalLimitedYesConfig file preferred
JetBrains IDEsNoYesConfig file only
tmuxYes*-*Requires 3.3+ or passthrough
ConEmu/CmderNoYesConfig file only
Apple TerminalLimited-Basic support

Troubleshooting

Theme Not Detected

If theme detection returns UNKNOWN:

  1. Check environment variables: echo $TERM $COLORTERM
  2. Verify terminal support: Not all terminals support OSC queries
  3. tmux users: Enable passthrough with set -g allow-passthrough on
  4. IDE users: Ensure the IDE config files are in standard locations

Wrong Theme Detected

If the detected theme doesn’t match your terminal:

  1. Custom color schemes: Some schemes may not be in the known list
  2. Override manually: Allow users to specify their preference
  3. Report the issue: Help us improve detection for your terminal

OSC Query Hangs

If detection seems to hang:

  1. Reduce timeout: Use detect(connection, 100) for shorter timeout
  2. Use fast detection: detectFast(connection) skips OSC queries
  3. Check terminal: Some terminals don’t respond to OSC queries
// With custom timeout (milliseconds)
TerminalColorCapability cap = TerminalColorDetector.detect(connection, 100);

Using TerminalColor with Detection

The TerminalColor class integrates seamlessly with color detection, providing theme-aware factory methods and automatic color depth adaptation.

Theme-Aware Semantic Colors

Use semantic factory methods that automatically adjust for the detected terminal theme:

import org.aesh.readline.terminal.formatting.TerminalColor;
import org.aesh.terminal.utils.TerminalColorCapability;

TerminalColorCapability cap = TerminalColorDetector.detect(connection);

// These methods automatically choose appropriate colors for the theme
TerminalColor error = TerminalColor.forError(cap);      // Red, bright on dark
TerminalColor success = TerminalColor.forSuccess(cap);  // Green, bright on dark
TerminalColor warning = TerminalColor.forWarning(cap);  // Yellow, bright on dark
TerminalColor info = TerminalColor.forInfo(cap);        // Cyan/Blue, bright on dark
TerminalColor highlight = TerminalColor.forHighlight(cap);  // Emphasized text
TerminalColor muted = TerminalColor.forMuted(cap);      // Secondary/dim text

On dark themes, these return bright variants for readability. On light themes, they use normal intensity to avoid glaring colors.

Example: Adaptive Status Messages

TerminalColorCapability cap = TerminalColorDetector.detectCached(connection);

// Create semantic colors once
TerminalColor errorColor = TerminalColor.forError(cap);
TerminalColor successColor = TerminalColor.forSuccess(cap);
TerminalColor infoColor = TerminalColor.forInfo(cap);

// Use with TerminalString
TerminalString errorMsg = new TerminalString("[ERROR] Connection failed", errorColor);
TerminalString okMsg = new TerminalString("[OK] Connected", successColor);
TerminalString infoMsg = new TerminalString("[INFO] Processing...", infoColor);

connection.write(errorMsg.toString() + "\n");
connection.write(okMsg.toString() + "\n");
connection.write(infoMsg.toString() + "\n");

Color Depth Adaptation

When using RGB colors on terminals with limited color support, use forCapability() to automatically downgrade:

// Create an RGB color
TerminalColor brandColor = TerminalColor.fromHex("#FF5733");

// Adapt to terminal capabilities
TerminalColor adapted = brandColor.forCapability(cap);

// 'adapted' will be:
// - The original RGB if terminal supports true color
// - Nearest 256-color palette index if terminal supports 256 colors
// - Nearest basic 16 color if terminal only supports basic colors

This ensures your colors always work, even on limited terminals. See Terminal Colors for more details on TerminalColor RGB and formatting features.