|
| 1 | +--- |
| 2 | +date: '2026-02-26T15:00:00+01:00' |
| 3 | +draft: false |
| 4 | +title: 'Hyperlinks' |
| 5 | +weight: 14 |
| 6 | +--- |
| 7 | + |
| 8 | +Æsh Readline supports clickable hyperlinks in terminal output using the OSC 8 escape sequence. When a supported terminal renders OSC 8 sequences, text is displayed as a clickable link — similar to HTML anchor tags. On terminals that don't recognize OSC 8, the sequences are silently ignored and only the visible text is shown. |
| 9 | + |
| 10 | +## How It Works |
| 11 | + |
| 12 | +OSC 8 uses two escape sequences to bracket the clickable text: |
| 13 | + |
| 14 | +| Sequence | Purpose | |
| 15 | +|----------|---------| |
| 16 | +| `ESC ] 8 ; params ; URL ST` | Start hyperlink (associate URL with following text) | |
| 17 | +| `ESC ] 8 ; ; ST` | End hyperlink (stop linking) | |
| 18 | + |
| 19 | +Everything written between the start and end sequences is rendered as a clickable link. The `params` field is optional and can contain an `id=VALUE` parameter to group multiple link segments (e.g., a link that wraps across lines). |
| 20 | + |
| 21 | +The String Terminator (ST) can be either `ESC \` or `BEL` (`\u0007`). Æsh uses `ESC \` by default. |
| 22 | + |
| 23 | +## Quick Start |
| 24 | + |
| 25 | +```java |
| 26 | +import org.aesh.terminal.tty.TerminalConnection; |
| 27 | +import org.aesh.terminal.utils.ANSI; |
| 28 | + |
| 29 | +TerminalConnection conn = new TerminalConnection(); |
| 30 | + |
| 31 | +// Check if terminal supports hyperlinks |
| 32 | +if (conn.supportsHyperlinks()) { |
| 33 | + // Write a clickable link |
| 34 | + conn.writeHyperlink("https://aeshell.github.io", "Æsh Documentation"); |
| 35 | + conn.write("\n"); |
| 36 | +} |
| 37 | +``` |
| 38 | + |
| 39 | +## Terminal Support |
| 40 | + |
| 41 | +| Terminal | Supported | |
| 42 | +|----------|-----------| |
| 43 | +| iTerm2 | Yes | |
| 44 | +| Kitty | Yes | |
| 45 | +| Ghostty | Yes | |
| 46 | +| WezTerm | Yes | |
| 47 | +| foot | Yes | |
| 48 | +| Contour | Yes | |
| 49 | +| Rio | Yes | |
| 50 | +| Warp | Yes | |
| 51 | +| Wave | Yes | |
| 52 | +| Hyper | Yes | |
| 53 | +| Tabby | Yes | |
| 54 | +| Extraterm | Yes | |
| 55 | +| GNOME Terminal | Yes | |
| 56 | +| Konsole | Yes | |
| 57 | +| Mintty | Yes | |
| 58 | +| xterm | Yes | |
| 59 | +| JetBrains | Yes | |
| 60 | +| VS Code | Yes | |
| 61 | +| Alacritty | Yes | |
| 62 | +| Windows Terminal | Yes | |
| 63 | +| Apple Terminal | No | |
| 64 | +| rxvt | No | |
| 65 | +| ConEmu | No | |
| 66 | +| tmux | No | |
| 67 | +| GNU Screen | No | |
| 68 | +| Linux Console | No | |
| 69 | + |
| 70 | +On unsupported terminals the OSC 8 sequences are silently ignored, so it is always safe to send them. |
| 71 | + |
| 72 | +## Connection API |
| 73 | + |
| 74 | +The `Connection` interface provides methods for writing hyperlinks: |
| 75 | + |
| 76 | +### Checking Support |
| 77 | + |
| 78 | +```java |
| 79 | +// Heuristic check based on terminal type detection |
| 80 | +boolean supported = connection.supportsHyperlinks(); |
| 81 | +``` |
| 82 | + |
| 83 | +This uses environment-based terminal detection via `TerminalEnvironment` and the `Device.TerminalType` enum. No terminal query is sent. |
| 84 | + |
| 85 | +### Writing Hyperlinks |
| 86 | + |
| 87 | +```java |
| 88 | +// Simple hyperlink |
| 89 | +connection.writeHyperlink("https://example.com", "Click here"); |
| 90 | + |
| 91 | +// Hyperlink with grouping ID (for links that span multiple lines) |
| 92 | +connection.writeHyperlink("https://example.com", "Click here", "link1"); |
| 93 | +``` |
| 94 | + |
| 95 | +Both methods return the `Connection` for chaining: |
| 96 | + |
| 97 | +```java |
| 98 | +connection |
| 99 | + .writeHyperlink("https://example.com", "Example") |
| 100 | + .write(" | ") |
| 101 | + .writeHyperlink("https://aeshell.github.io", "Æsh Docs") |
| 102 | + .write("\n"); |
| 103 | +``` |
| 104 | + |
| 105 | +## ANSI Utility Methods |
| 106 | + |
| 107 | +The `ANSI` class provides static methods for building hyperlink escape sequences directly: |
| 108 | + |
| 109 | +### Wrapping Text |
| 110 | + |
| 111 | +```java |
| 112 | +import org.aesh.terminal.utils.ANSI; |
| 113 | + |
| 114 | +// Full hyperlink: opening sequence + text + closing sequence |
| 115 | +String link = ANSI.hyperlink("https://example.com", "click here"); |
| 116 | +connection.write(link); |
| 117 | + |
| 118 | +// With grouping ID |
| 119 | +String link = ANSI.hyperlink("https://example.com", "click here", "myid"); |
| 120 | +``` |
| 121 | + |
| 122 | +### Building Sequences Manually |
| 123 | + |
| 124 | +For more control, build the opening and closing sequences separately: |
| 125 | + |
| 126 | +```java |
| 127 | +// Opening sequence |
| 128 | +String start = ANSI.buildHyperlinkStart("https://example.com", null); |
| 129 | +// Result: ESC ] 8 ; ; https://example.com ST |
| 130 | + |
| 131 | +// Opening sequence with ID |
| 132 | +String start = ANSI.buildHyperlinkStart("https://example.com", "link1"); |
| 133 | +// Result: ESC ] 8 ; id=link1 ; https://example.com ST |
| 134 | + |
| 135 | +// Closing sequence |
| 136 | +String end = ANSI.buildHyperlinkEnd(); |
| 137 | +// Result: ESC ] 8 ; ; ST |
| 138 | + |
| 139 | +// Combine manually |
| 140 | +connection.write(start + "visible text" + end); |
| 141 | +``` |
| 142 | + |
| 143 | +### Grouping with IDs |
| 144 | + |
| 145 | +The `id` parameter allows multiple disjoint text segments to refer to the same hyperlink. This is useful when a link wraps across lines — hovering over any segment highlights all segments with the same ID: |
| 146 | + |
| 147 | +```java |
| 148 | +// Two segments of the same link on different lines |
| 149 | +String id = "doc-link"; |
| 150 | +connection.write(ANSI.buildHyperlinkStart("https://example.com/long-page", id)); |
| 151 | +connection.write("This link continues"); |
| 152 | +connection.write(ANSI.buildHyperlinkEnd()); |
| 153 | +connection.write("\n"); |
| 154 | +connection.write(ANSI.buildHyperlinkStart("https://example.com/long-page", id)); |
| 155 | +connection.write("on the next line"); |
| 156 | +connection.write(ANSI.buildHyperlinkEnd()); |
| 157 | +``` |
| 158 | + |
| 159 | +## Security |
| 160 | + |
| 161 | +The implementation sanitizes URLs and IDs to prevent OSC injection. Control characters that could terminate the escape sequence prematurely — specifically ESC (`\u001B`) and BEL (`\u0007`) — are stripped from both URL and ID values. Passing a `null` URL throws an `IllegalArgumentException`. |
| 162 | + |
| 163 | +## Stripping Hyperlinks |
| 164 | + |
| 165 | +The `Parser.stripAwayAnsiCodes()` method removes OSC 8 sequences while preserving the visible text. This is useful for measuring display width or logging plain text: |
| 166 | + |
| 167 | +```java |
| 168 | +import org.aesh.terminal.utils.Parser; |
| 169 | + |
| 170 | +String linked = ANSI.hyperlink("https://example.com", "click here"); |
| 171 | +String plain = Parser.stripAwayAnsiCodes(linked); |
| 172 | +// Result: "click here" |
| 173 | +``` |
| 174 | + |
| 175 | +Both `ESC \` and `BEL` terminators are handled correctly. |
| 176 | + |
| 177 | +## Complete Example |
| 178 | + |
| 179 | +```java |
| 180 | +import org.aesh.terminal.tty.TerminalConnection; |
| 181 | +import org.aesh.terminal.utils.ANSI; |
| 182 | + |
| 183 | +public class HyperlinkDemo { |
| 184 | + public static void main(String[] args) throws Exception { |
| 185 | + TerminalConnection conn = new TerminalConnection(); |
| 186 | + |
| 187 | + if (conn.supportsHyperlinks()) { |
| 188 | + conn.write("Terminal supports hyperlinks!\n\n"); |
| 189 | + |
| 190 | + // Simple link |
| 191 | + conn.write("Visit: "); |
| 192 | + conn.writeHyperlink("https://aeshell.github.io", "Æsh Documentation"); |
| 193 | + conn.write("\n"); |
| 194 | + |
| 195 | + // Link with styled text (combine with ANSI formatting) |
| 196 | + conn.write("Source: "); |
| 197 | + conn.write(ANSI.BOLD); |
| 198 | + conn.writeHyperlink("https://github.com/aeshell", "GitHub"); |
| 199 | + conn.write(ANSI.RESET); |
| 200 | + conn.write("\n"); |
| 201 | + |
| 202 | + // Multiple links on one line |
| 203 | + conn.write("\nLinks: "); |
| 204 | + conn.writeHyperlink("https://example.com/one", "One") |
| 205 | + .write(" | ") |
| 206 | + .writeHyperlink("https://example.com/two", "Two") |
| 207 | + .write(" | ") |
| 208 | + .writeHyperlink("https://example.com/three", "Three") |
| 209 | + .write("\n"); |
| 210 | + } else { |
| 211 | + conn.write("This terminal does not support hyperlinks.\n"); |
| 212 | + conn.write("Try running in: Kitty, iTerm2, WezTerm, or VS Code\n"); |
| 213 | + } |
| 214 | + |
| 215 | + conn.close(); |
| 216 | + } |
| 217 | +} |
| 218 | +``` |
| 219 | + |
| 220 | +## Specification |
| 221 | + |
| 222 | +The OSC 8 hyperlink protocol is documented by the Contour terminal: |
| 223 | + |
| 224 | +[Contour VT Extension: Clickable Links](https://contour-terminal.org/vt-extensions/clickable-links/) |
| 225 | + |
| 226 | +## See Also |
| 227 | + |
| 228 | +- [Connection](connection) — Full `Connection` API reference |
| 229 | +- [Terminal Environment](terminal-environment) — Terminal type detection |
| 230 | +- [Synchronized Output](synchronized-output) — Tear-free rendering |
0 commit comments