Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions apps/desktop/docs/ESCAPE_SEQUENCE_HANDLING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Escape Sequence Handling

This document explains how we handle escape sequences in the terminal to prevent them from leaking into output, particularly for TUI (Terminal User Interface) applications.

## Problem

When terminal applications query terminal capabilities or send control sequences, the terminal responds with escape sequences. If these responses aren't properly suppressed, they can appear as visible text in the terminal output, breaking TUI applications like `vim`, `less`, `htop`, `ncurses`-based tools, etc.

## Solution Approach

We use **xterm.js parser hooks** to intercept and suppress escape sequences at the parser level, before they reach the display layer. This approach is similar to how iTerm2 and Hyper handle escape sequences.

### How iTerm2 Handles It

iTerm2 handles escape sequences at a very low level in their `VT100Screen` implementation. They parse sequences before they ever reach the display layer, ensuring that query responses and control sequences are handled internally and never rendered as text.

### How Hyper Handles It

Hyper uses xterm.js (like we do) but **does not use parser hooks** to suppress escape sequences. They rely entirely on xterm.js's default behavior. This means Hyper trusts that xterm.js handles query responses correctly internally.

However, we've found that some query responses can still leak into the terminal output, which is why we implement explicit suppression using parser hooks.

### Our Implementation

We use the same parser hook approach as Hyper. The `suppressQueryResponses` function registers handlers for various escape sequence types:
Comment on lines +17 to +25

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Contradictory statements about Hyper's approach.

Line 19 states Hyper "does not use parser hooks" to suppress escape sequences, but line 25 says "We use the same parser hook approach as Hyper." These statements contradict each other.

Suggested fix:

-We use the same parser hook approach as Hyper. The `suppressQueryResponses` function registers handlers for various escape sequence types:
+Unlike Hyper, we explicitly register parser hooks via the `suppressQueryResponses` function to handle various escape sequence types:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
### How Hyper Handles It
Hyper uses xterm.js (like we do) but **does not use parser hooks** to suppress escape sequences. They rely entirely on xterm.js's default behavior. This means Hyper trusts that xterm.js handles query responses correctly internally.
However, we've found that some query responses can still leak into the terminal output, which is why we implement explicit suppression using parser hooks.
### Our Implementation
We use the same parser hook approach as Hyper. The `suppressQueryResponses` function registers handlers for various escape sequence types:
### How Hyper Handles It
Hyper uses xterm.js (like we do) but **does not use parser hooks** to suppress escape sequences. They rely entirely on xterm.js's default behavior. This means Hyper trusts that xterm.js handles query responses correctly internally.
However, we've found that some query responses can still leak into the terminal output, which is why we implement explicit suppression using parser hooks.
### Our Implementation
Unlike Hyper, we explicitly register parser hooks via the `suppressQueryResponses` function to handle various escape sequence types:
🤖 Prompt for AI Agents
apps/desktop/docs/ESCAPE_SEQUENCE_HANDLING.md around lines 17 to 25: the two
sentences contradict each other — one claims Hyper does not use parser hooks
while the next claims we use the same parser hook approach as Hyper; update the
text so it consistently describes Hyper's actual behavior and then contrasts it
with our implementation (for example, state clearly whether Hyper relies on
xterm.js defaults or uses parser hooks, then say "By contrast, we implement
explicit suppression using parser hooks" or, if Hyper does use parser hooks,
reword to "Like Hyper, we use parser hooks" so both lines align).


- **CSI sequences**: Device Attributes (DA), Cursor Position Reports (CPR), Mode Reports, Device Status Reports (DSR), Terminal ID queries
- **OSC sequences**: Color query responses (OSC 10-19)
- **ESC sequences**: Various control sequences

## Supported Sequences

### CSI Sequences

- `ESC[...c` - Device Attributes (DA1, DA2)
- `ESC[...R` - Cursor Position Report (CPR)
- `ESC[...$y` - Mode Reports (DECRPM)
- `ESC[...n` - Device Status Reports (DSR)
- `ESC[?...n` - DEC Private Mode Status Reports
- `ESC[...q` - Terminal ID queries
- `ESC[>...q` - Terminal ID responses
- `ESC[...x` - Request Terminal Parameters
- `ESC[?...x` - DEC Request Terminal Parameters

### OSC Sequences

- `ESC]10;...BEL` - Foreground color query response
- `ESC]11;...BEL` - Background color query response
- `ESC]12;...BEL` - Cursor color query response
- `ESC]13-19;...BEL` - Additional color query responses

## Implementation Details

The suppression is implemented in:
- `apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/suppressQueryResponses.ts`

This function is called during terminal initialization in:
- `apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts`

## How Parser Hooks Work

1. When `term.write(data)` is called, data enters the parser
2. The parser identifies escape sequences (CSI, OSC, ESC, etc.)
3. For each sequence type, registered handlers are called in reverse registration order
4. If a handler returns `true`, the sequence is suppressed and no further handlers are called
5. If a handler returns `false`, the next handler is tried
6. If no handler returns `true`, the default xterm.js handler processes the sequence

## Testing

To test escape sequence suppression:

1. Run a TUI application like `vim`, `less`, or `htop`
2. Verify that escape sequences don't appear as visible text
3. Check that cursor positioning and screen updates work correctly
4. Test with programs that query terminal capabilities (e.g., `tput` commands)

## Common Issues

### Escape sequences still appearing

If escape sequences are still leaking into output:

1. Check if the sequence type is registered in `suppressQueryResponses`
2. Verify the sequence format matches the handler pattern
3. Check xterm.js documentation for sequence format details
4. Add a new handler for the problematic sequence type

### TUI applications not working correctly

If TUI applications aren't working:

1. Verify that we're not suppressing sequences that should be processed
2. Check that handlers return `true` only for query responses, not control sequences
3. Ensure we're not interfering with legitimate terminal state changes

## References

- [xterm.js Parser Hooks Documentation](http://xtermjs.org/docs/guides/hooks/)
- [xterm.js Parser API](http://xtermjs.org/docs/api/terminal/interfaces/iparser/)
- [iTerm2 Source Code](https://github.com/gnachman/iTerm2)
- [Hyper Source Code](https://github.com/vercel/hyper)
- [ECMA-48 Standard](https://www.ecma-international.org/publications/files/ECMA-ST/Ecma-048.pdf)
- [VT100.net Documentation](https://vt100.net/)
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import type { Terminal } from "@xterm/xterm";

/**
* Registers parser hooks to suppress terminal query responses from being displayed.
* Registers parser hooks to suppress terminal query responses and control sequences
* from being displayed as visible text.
*
* When programs query terminal capabilities (DA1, DA2, CPR, etc.), the terminal
* responds with escape sequences. These responses should be handled internally,
* When programs query terminal capabilities or send control sequences, the terminal
* may respond with escape sequences. These responses should be handled internally,
* not displayed as visible text. xterm.js's parser hooks let us intercept and
* suppress these sequences at the display layer.
* suppress these sequences at the display layer, similar to how iTerm2 and Hyper
* handle escape sequences.
*
* This prevents escape sequences from "leaking" into the terminal output, which
* is especially important for TUI applications that rely on precise terminal state.
*
* @param terminal - The xterm.js Terminal instance
* @returns Cleanup function to dispose all registered handlers
Expand All @@ -15,6 +20,10 @@ export function suppressQueryResponses(terminal: Terminal): () => void {
const disposables: { dispose: () => void }[] = [];
const parser = terminal.parser;

// ============================================================================
// CSI (Control Sequence Introducer) Sequences
// ============================================================================

// CSI sequences ending in 'c' - Device Attributes responses
// DA1: ESC[?1;2c (primary device attributes)
// DA2: ESC[>0;276;0c (secondary device attributes)
Expand All @@ -23,6 +32,7 @@ export function suppressQueryResponses(terminal: Terminal): () => void {

// CSI sequences ending in 'R' - Cursor Position Report
// CPR: ESC[24;1R (row;column)
// Used by programs to query cursor position
disposables.push(parser.registerCsiHandler({ final: "R" }, () => true));

// CSI sequences ending in 'y' with '$' intermediate - Mode Reports
Expand All @@ -34,11 +44,82 @@ export function suppressQueryResponses(terminal: Terminal): () => void {
}),
);

// CSI sequences ending in 'n' - Device Status Reports (DSR)
// DSR 5: ESC[0n (device status OK)
// DSR 6: ESC[n;mR (cursor position report - already handled above)
// DSR 7: ESC[?15n (printer ready)
// These are responses to queries and shouldn't be displayed
// Note: We need to be careful - some DSR sequences are control sequences, not queries
// But query responses (like ESC[0n) should be suppressed
disposables.push(parser.registerCsiHandler({ final: "n" }, () => true));

// CSI sequences with '?' prefix ending in 'n' - DEC Private Mode Status Reports
// DECDSR: ESC[?6n (cursor position report)
// ESC[?15n (printer ready)
// ESC[?25n (cursor visible)
// ESC[?26n (cursor key mode)
// All DEC private mode reports are query responses
disposables.push(
parser.registerCsiHandler({ prefix: "?", final: "n" }, () => true),
);

// CSI sequences ending in 'q' - Terminal ID and Capability Queries
// DECRQSS: ESC[?q - Request Status String (query)
// ESC[>0q - Terminal ID query response
// ESC[>1q - Terminal ID query response
// ESC[>c - Secondary device attributes (already handled above)
// All 'q' sequences are query responses
disposables.push(parser.registerCsiHandler({ final: "q" }, () => true));

// CSI sequences with '>' prefix ending in 'q' - Terminal ID responses
// ESC[>0;276;0q - Terminal ID response
// These are always query responses
disposables.push(
parser.registerCsiHandler({ prefix: ">", final: "q" }, () => true),
);

// CSI sequences with '?' prefix ending in 'q' - DEC Request Status String
// DECRQSS: ESC[?q - Request Status String (query response)
disposables.push(
parser.registerCsiHandler({ prefix: "?", final: "q" }, () => true),
);

// CSI sequences ending in 'x' - Request Terminal Parameters
// DECREQTPARM: ESC[?x (request terminal parameters)
// Response sequences shouldn't be displayed
// Note: ESC[x is also used for some control functions, but query responses
// typically come from the terminal, not the application
disposables.push(parser.registerCsiHandler({ final: "x" }, () => true));

// CSI sequences with '?' prefix ending in 'x' - DEC Request Terminal Parameters
// DECREQTPARM: ESC[?x - Request Terminal Parameters (query response)
disposables.push(
parser.registerCsiHandler({ prefix: "?", final: "x" }, () => true),
);

// CSI sequences ending in 't' - Window manipulation
// Some are queries (ESC[13t, ESC[14t, etc.) but many are control sequences
// (ESC[3;x;yt to move window, ESC[22t to save title, etc.)
// We can't easily distinguish query responses from control sequences
// Query responses are rare and xterm.js handles window manipulation internally
// So we don't suppress 't' sequences to avoid breaking legitimate functionality

// ============================================================================
// OSC (Operating System Command) Sequences
// ============================================================================

// OSC 10-19 - Color query responses
// OSC 10: foreground color (ESC]10;rgb:ffff/ffff/ffff BEL)
// OSC 11: background color
// OSC 12: cursor color
// etc.
// OSC 13: mouse foreground color
// OSC 14: mouse background color
// OSC 15: Tektronix foreground color
// OSC 16: Tektronix background color
// OSC 17: highlight background color
// OSC 18: Tektronix cursor color
// OSC 19: highlight foreground color
// When queried with "?", these return color values that shouldn't be displayed
for (let i = 10; i <= 19; i++) {
disposables.push(
parser.registerOscHandler(i, () => {
Expand All @@ -47,6 +128,50 @@ export function suppressQueryResponses(terminal: Terminal): () => void {
);
}

// OSC 4 - Color palette
// ESC]4;c;? ST - Query color palette (query)
// ESC]4;c;rgb:rrrr/gggg/bbbb ST - Set color (control sequence)
// Query responses come back as ESC]4;c;rgb:... ST
// We can't easily distinguish responses from sets, so we don't suppress OSC 4
// Applications should handle color palette queries correctly

// OSC 52 - Clipboard manipulation (selection/paste)
// ESC]52;c;BASE64 ST - Set clipboard
// ESC]52;c;? ST - Query clipboard (query)
// Query responses come back as ESC]52;c;BASE64 ST
// We can't easily distinguish responses from sets, so we don't suppress OSC 52
// Note: xterm.js handles OSC 52 internally, and clipboard queries are rare
// If clipboard data leaks, it's a security issue but not a display issue

// OSC 1048-1049 - Save/Restore cursor (iTerm2 extensions)
// These are control sequences, not queries, but some implementations
// may send responses that shouldn't be displayed
// We don't suppress these as they're control sequences, not query responses

// OSC 1337 - iTerm2 proprietary sequences
// ESC]1337;... ST - Various iTerm2-specific commands
// We don't suppress these as they may contain actual content,
// but query responses within this range should be handled
// Note: iTerm2 sequences are typically one-way (app -> terminal),
// so they shouldn't leak back into output

// ============================================================================
// ESC (Escape) Sequences
// ============================================================================

// ESC sequences ending in 'c' - Full Reset / Identify Device
// ESC c - Full reset (RIS)
// This is a control sequence, not a query response, so we don't suppress it
// But some terminals may send identification sequences that should be suppressed

// ============================================================================
// Additional TUI-friendly suppressions
// ============================================================================

// Some programs send malformed or incomplete escape sequences that can
// leak into output. While we can't catch all of them, the above handlers
// cover the most common cases that cause issues with TUI applications.

return () => {
for (const disposable of disposables) {
disposable.dispose();
Expand Down