Homepage: aeshell.github.io
Æsh Readline is a library for handling console input with the goal to support most GNU Readline features.
For examples, have a look here: Aesh Examples
- Features
- Scope
- Connectivity
- How to Build
- Getting Started
- Terminal Colors
- Terminal Images
- Documentation
- Examples
- Key Mappings
- Supported Runtime Properties
- Contributing
- License
- Line editing with undo/redo
- History (search, persistence, fuzzy search via Ctrl+R, ignore patterns)
- Completion
- Masking (password input)
- Paste buffer
- Emacs and Vi editing mode
- Right prompt (right-aligned, auto-hiding)
- Ghost text suggestions (inline history suggestions)
- Supports POSIX and Windows
- Easy to configure (history file & buffer size, edit mode, streams, etc)
- Redirect, Alias, Pipeline
- Print Above - Print text above the prompt without disrupting input (thread-safe)
- Status Lines - Persistent status display between output and prompt
- Split Screen - Independent scrolling regions (experimental)
- Focus Tracking - Detect terminal focus gained/lost
- Mouse Tracking - Mouse events with SGR encoding
- Synchronized Output - Tear-free rendering (Mode 2026)
- Non-blocking Input - Poll-based read with escape timeout disambiguation (Java 22+)
- Terminal Provider SPI - Pluggable terminal implementations
- Color Detection - Automatically detect terminal theme (light/dark) and color depth
- ANSIBuilder - Fluent API for building styled terminal output
- Semantic Colors - Theme-aware colors for log levels (error, warning, info, debug, trace)
- Extended Colors - Support for 256-color palette and 24-bit true color (RGB/hex)
- Color Adaptation - Automatic color downgrading for terminals with limited color support
- Image Support - Display images in supported terminals (Kitty, iTerm2, Sixel)
The scope of Æsh Readline is to provide an easy to use Readline library. It is used by the Aesh project.
Æsh Readline supports remote connections via SSH, Telnet, and WebSockets, enabling console applications to run over networks.
Æsh Readline uses Maven (https://maven.apache.org/) as its build tool.
- Java 8 or higher
- Maven 3.6+
To build the project:
mvn clean installTo run tests:
mvn testNote: the Windows JNI library aesh-console.dll is built with a non-executable stack to avoid JVM stack guard warnings during native-image builds. On Java 22+, the FFM-based implementation is used instead of the JNI DLL.
Troubleshooting: if you still see a stack guard warning mentioning aesh-console*.dll, rebuild the DLL or run execstack -c on the extracted library in your build image.
Use the BOM (Bill of Materials) to manage all module versions consistently:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.aesh</groupId>
<artifactId>aesh-readline-bom</artifactId>
<version>3.14</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>Then add the modules you need without specifying versions:
<dependencies>
<dependency>
<groupId>org.aesh</groupId>
<artifactId>readline</artifactId>
</dependency>
<dependency>
<groupId>org.aesh</groupId>
<artifactId>terminal-tty</artifactId>
</dependency>
</dependencies>Or without the BOM, add dependencies directly:
<dependency>
<groupId>org.aesh</groupId>
<artifactId>readline</artifactId>
<version>3.14</version>
</dependency>
<dependency>
<groupId>org.aesh</groupId>
<artifactId>terminal-tty</artifactId>
<version>3.14</version>
</dependency>readline pulls in terminal-api and readline-api transitively. terminal-tty provides local terminal support.
Here's a simple example to get you started:
import org.aesh.readline.Readline;
import org.aesh.readline.ReadlineBuilder;
import org.aesh.terminal.tty.TerminalConnection;
import java.io.IOException;
public class SimpleExample {
public static void main(String... args) {
TerminalConnection connection = new TerminalConnection();
read(connection, ReadlineBuilder.builder().enableHistory(false).build(), "[aesh@rules]$ ");
connection.openBlocking();
}
private static void read(TerminalConnection connection, Readline readline, String prompt) {
readline.readline(connection, prompt, input -> {
if(input != null && input.equals("exit")) {
connection.write("we're exiting\n");
connection.close();
}
else {
connection.write("=====> "+input+"\n");
read(connection, readline, prompt);
}
});
}
}Æsh Readline provides comprehensive terminal color support with automatic theme detection and a fluent API for styled output.
Automatically detect the terminal's color capabilities and theme:
import org.aesh.terminal.utils.TerminalColorCapability;
// Quick detection from environment variables
TerminalColorCapability cap = TerminalColorCapability.detectFromEnvironment();
// Check theme and color depth
if (cap.getTheme().isDark()) {
// Use light colors for dark backgrounds
}
if (cap.getColorDepth().supportsTrueColor()) {
// Terminal supports 24-bit RGB colors
}terminal-detect also provides a passive owner for one terminal/session:
import org.aesh.terminal.detect.TerminalCapabilities;
import org.aesh.terminal.detect.TerminalDetection;
TerminalDetection hints = TerminalDetection.environmentOnly();
TerminalCapabilities caps = hints.capabilities();
// Retain an existing result. No discovery starts or waits here.
TerminalDetection managed = TerminalDetection.managed(caps);
TerminalCapabilities current = managed.capabilities();Use environmentOnly(map) for explicitly supplied environment hints.
managed(null) returns unknown facts without consulting the environment or
process default. Missing RGB, palette or mode results never trigger a probe.
Capture the owner once, then reuse it for reads.
A supplied result stays attached even if the process default changes. Its producer may still fill pending values; passive reads never wait for that work. This does not establish a live TTY or grant input ownership. The passive core adds no connection-getter wiring.
Attach an owner to a connection with attachDetection(owner); passing null
clears back to the legacy answers. Attached getters prefer endpoint facts:
colorCapability() takes the attached theme and measured colors with depth
merged by max-never-downgrade, and getImageProtocol() answers from an
attached result or declines its live DA1 fallback when the attached source
is unknown. TerminalConnection captures its default once on first use.
When process stdin or stdout is a TTY, it retains the shared capabilities;
otherwise it captures independent environment hints. Neither path starts
discovery. Explicit attachment overrides either default. Process fd facts
do not establish the origin of arbitrary caller-owned streams.
Unattached theme helpers and the connectionless SixelImage encoder retain
their legacy process-environment behavior.
Shared full/async detection publishes pending hints before terminal I/O.
TerminalCapabilities.getInstance() reads that result without joining it;
detectAsync() reuses a synchronous round already in progress. detectFull()
still waits for completion. If its join is interrupted, it finishes the same
round and restores the flag instead of starting a second probe. An unchecked
producer failure is propagated by full callers until explicit invalidation.
These shared factories do not implement a hard overall enrichment deadline.
detectBounded(deadline, permissions) runs one enrichment attempt with an
explicit budget and returns an independent result: it never consults, joins,
or publishes shared discovery. Decline, timeout, and transport failure
degrade to environment hints, never to null. The call blocks up to the
deadline plus one transport read quantum, so keep it off
latency-critical threads.
Connection.endpointContext() returns an immutable metadata snapshot without
probing. Unconfigured and custom connections default to UNKNOWN origin, unknown
stream facts and no dimensions. Environment markers such as SSH_TTY do not
decide locality.
Creators can supply facts on built-in connections before handing them to consumers:
import org.aesh.terminal.EndpointContext;
import org.aesh.terminal.StreamConnection;
import org.aesh.terminal.tty.Size;
StreamConnection connection = new StreamConnection(charset, input, output);
connection.setEndpointContext(EndpointContext.builder()
.locality(EndpointContext.Locality.REMOTE)
.input(EndpointContext.StreamFacts.supplied(
EndpointContext.Truth.YES, EndpointContext.Truth.UNKNOWN))
.dimensions(new Size(120, 40), EndpointContext.Source.CALLER)
.build());
// Readers use the Connection method, with no casts or discovery.
EndpointContext context = connection.endpointContext();Input, output and error-stream terminal status are independent of native-console status and ANSI support. Known stream answers require caller or transport evidence, never environment hints or a fallback. Missing dimensions are null; explicit defaults use FALLBACK provenance. Observed or negotiated size updates outrank environment hints. Built-in connections invalidate live metadata on close and ignore late metadata writes. Wrappers should forward the getter to their endpoint, and custom implementations must manage their own metadata lifecycle.
This metadata does not attach capabilities, grant input ownership, or replace handle validation. Built-in setup and resize population, context-aware shared attachment, and Quarkus mapping remain work under #382.
Use ANSIBuilder for fluent, theme-aware terminal styling:
import org.aesh.terminal.utils.ANSIBuilder;
// Basic usage
String output = ANSIBuilder.builder()
.bold("Title: ").append("Some text")
.toString();
// Theme-aware semantic colors
TerminalColorCapability cap = TerminalColorCapability.detectFromEnvironment();
ANSIBuilder builder = ANSIBuilder.builder(cap);
String logLine = builder
.timestamp("10:30:45").append(" ")
.error("[ERROR]").append(" ")
.append("Connection failed")
.toLine();Theme-aware colors for log levels that automatically adjust to light/dark terminals:
ANSIBuilder builder = ANSIBuilder.builder(cap);
// Log level colors (most to least prominent)
builder.error("Error message"); // Red
builder.warning("Warning message"); // Yellow
builder.success("Success message"); // Green (same as INFO level)
builder.info("Info message"); // Blue
builder.debug("Debug message"); // White/Gray (subdued)
builder.trace("Trace message"); // Gray (least prominent)
// Other semantic colors
builder.timestamp("10:30:45"); // Cyan
builder.message("Highlighted"); // MagentaSupport for 256-color palette and 24-bit true color:
// 256-color palette
builder.color256(208).append("Orange text");
builder.bg256(17).append("Blue background");
// True color RGB
builder.rgb(255, 87, 51).append("Custom color");
builder.bgRgb(30, 30, 30).append("Dark background");
// Hex colors
builder.hex("#FF5733").append("Coral text");
builder.bgHex("#1E1E1E").append("Dark background");Override semantic colors with custom values:
ANSIBuilder builder = ANSIBuilder.builder()
.errorCode(196) // 256-color red
.successHex("#00FF00") // Hex green
.timestampRgb(100, 149, 237); // RGB cornflower blue
builder.error("Custom error color");Display images in terminals that support graphics protocols (Kitty, iTerm2, Sixel).
import org.aesh.terminal.utils.TerminalImageBuilder;
import java.nio.file.Path;
// Display an image
String imageData = TerminalImageBuilder.builder()
.file(Path.of("image.png"))
.width(40)
.height(20)
.build();
connection.write(imageData);// Check which protocol the terminal supports
TerminalGraphicsCapability graphics = TerminalGraphicsDetector.detect(connection);
if (graphics.supportsKitty()) {
// Use Kitty graphics protocol
} else if (graphics.supportsIterm()) {
// Use iTerm2 inline images
} else if (graphics.supportsSixel()) {
// Use Sixel graphics
}For detailed documentation, visit the Æsh Readline Documentation.
For examples, have a look at Aesh Examples
Note: C equals Control and M is Meta/Alt
- Move back one char : C-b or left arrow
- Move forward one char : C-f or right arrow
- Delete the character left of cursor : backspace
- Delete the character on cursor : C-d
- Undo : C-_ or C-x C-u
- Move to the start of the line : C-a or home
- Move to the end of the line : C-e or end
- Move forward a word, where a word is composed of letters and digits : M-f
- Move backward a word : M-b
- Previous line : up arrow
- Next line : down arrow
- Clear the screen, reprinting the current line at the top : C-l
- Delete next word to the right of cursor : M-d
- Complete : tab
- Kill the text from the current cursor position to the end of the line : C-k
- Kill from the cursor to the end of the current word, or, if between words, to the end of the next word : M-d
- Kill from the cursor to the previous whitespace : C-w
- Yank the most recently killed text back into the buffer at the cursor : C-y
- Search backward in the history for a particular string : C-r
- Search forward in the history for a particular string : C-s
- Switch to VI editing mode: M-C-j
In command mode: About every vi command is supported, here's a few:
- Move back one char : h
- Move forward one char : l
- Delete the character left of cursor : X
- Delete the character on cursor : x
- Undo : u
- Move to the start of the line : 0
- Move to the end of the line : $
- Move forward a word, where a word is composed of letters and digits : w
- Move backward a word : b
- Previous line : k
- Next line : n
- Clear the screen, reprinting the current line at the top : C-l
- Delete next word to the right of cursor : dw
- Kill the text from the current cursor position to the end of the line : D and d$
- Kill from the cursor to the end of the current word, or, if between words, to the end of the next word : db
- Kill from the cursor to the previous whitespace : dB
- Yank the most recently killed text back into the buffer at the cursor : p (after cursor), P (before cursor)
- Add text into yank buffer : y + movement action
- Enable change mode : c
- Repeat previous action : .
- +++ (read a vi manual)
In edit mode:
- Search backward in the history for a particular string : C-r
- Search forward in the history for a particular string : C-s
- Delete the character left of cursor : backspace
- aesh.terminal : specify Terminal object
- aesh.editmode : specify either VI or EMACS edit mode
- aesh.readinputrc : specify if Æsh should read settings from inputrc
- aesh.inputrc : specify the inputrc file (must exist)
- aesh.historyfile : specify the history file (must exist)
- aesh.historypersistent : specify if Æsh should persist history file on exit
- aesh.historydisabled : specify if history should be disabled
- aesh.historysize : specify the maximum size of the history file
- aesh.logging : specify if logging should be enabled
- aesh.logfile : specify the log file
- aesh.disablecompletion : specify if completion should be disabled
We welcome contributions! Please see our Contributing Guide for details on how to get started.
- Report issues: GitHub Issues
- Submit pull requests: GitHub Pull Requests
This project is licensed under the Apache License, Version 2.0. See the LICENSE file for details.