terminal.ts
ink/terminal.ts
249
Lines
8190
Bytes
10
Exports
9
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with ui-flow. It contains 249 lines, 9 detected imports, and 10 detected exports.
Important relationships
Detected exports
ProgressisProgressReportingAvailableisSynchronizedOutputSupportedsetXtversionNameisXtermJssupportsExtendedKeyshasCursorUpViewportYankBugSYNC_OUTPUT_SUPPORTEDTerminalwriteDiffToTerminal
Keywords
terminalprocessbuffertermpatchcasebreakterminalsversiontermprogram
Detected imports
semverstream../utils/env.js../utils/semver.js./clearTerminal.js./frame.js./termio/csi.js./termio/dec.js./termio/osc.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
import { coerce } from 'semver'
import type { Writable } from 'stream'
import { env } from '../utils/env.js'
import { gte } from '../utils/semver.js'
import { getClearTerminalSequence } from './clearTerminal.js'
import type { Diff } from './frame.js'
import { cursorMove, cursorTo, eraseLines } from './termio/csi.js'
import { BSU, ESU, HIDE_CURSOR, SHOW_CURSOR } from './termio/dec.js'
import { link } from './termio/osc.js'
export type Progress = {
state: 'running' | 'completed' | 'error' | 'indeterminate'
percentage?: number
}
/**
* Checks if the terminal supports OSC 9;4 progress reporting.
* Supported terminals:
* - ConEmu (Windows) - all versions
* - Ghostty 1.2.0+
* - iTerm2 3.6.6+
*
* Note: Windows Terminal interprets OSC 9;4 as notifications, not progress.
*/
export function isProgressReportingAvailable(): boolean {
// Only available if we have a TTY (not piped)
if (!process.stdout.isTTY) {
return false
}
// Explicitly exclude Windows Terminal, which interprets OSC 9;4 as
// notifications rather than progress indicators
if (process.env.WT_SESSION) {
return false
}
// ConEmu supports OSC 9;4 for progress (all versions)
if (
process.env.ConEmuANSI ||
process.env.ConEmuPID ||
process.env.ConEmuTask
) {
return true
}
const version = coerce(process.env.TERM_PROGRAM_VERSION)
if (!version) {
return false
}
// Ghostty 1.2.0+ supports OSC 9;4 for progress
// https://ghostty.org/docs/install/release-notes/1-2-0
if (process.env.TERM_PROGRAM === 'ghostty') {
return gte(version.version, '1.2.0')
}
// iTerm2 3.6.6+ supports OSC 9;4 for progress
// https://iterm2.com/downloads.html
if (process.env.TERM_PROGRAM === 'iTerm.app') {
return gte(version.version, '3.6.6')
}
return false
}
/**
* Checks if the terminal supports DEC mode 2026 (synchronized output).
* When supported, BSU/ESU sequences prevent visible flicker during redraws.
*/
export function isSynchronizedOutputSupported(): boolean {
// tmux parses and proxies every byte but doesn't implement DEC 2026.
// BSU/ESU pass through to the outer terminal but tmux has already
// broken atomicity by chunking. Skip to save 16 bytes/frame + parser work.
if (process.env.TMUX) return false
const termProgram = process.env.TERM_PROGRAM
const term = process.env.TERM
// Modern terminals with known DEC 2026 support
if (
termProgram === 'iTerm.app' ||
termProgram === 'WezTerm' ||
termProgram === 'WarpTerminal' ||
termProgram === 'ghostty' ||
termProgram === 'contour' ||
termProgram === 'vscode' ||
termProgram === 'alacritty'
) {
return true
}
// kitty sets TERM=xterm-kitty or KITTY_WINDOW_ID
if (term?.includes('kitty') || process.env.KITTY_WINDOW_ID) return true
// Ghostty may set TERM=xterm-ghostty without TERM_PROGRAM
if (term === 'xterm-ghostty') return true
// foot sets TERM=foot or TERM=foot-extra
if (term?.startsWith('foot')) return true
// Alacritty may set TERM containing 'alacritty'
if (term?.includes('alacritty')) return true
// Zed uses the alacritty_terminal crate which supports DEC 2026
if (process.env.ZED_TERM) return true
// Windows Terminal
if (process.env.WT_SESSION) return true
// VTE-based terminals (GNOME Terminal, Tilix, etc.) since VTE 0.68
const vteVersion = process.env.VTE_VERSION
if (vteVersion) {
const version = parseInt(vteVersion, 10)
if (version >= 6800) return true
}
return false
}
// -- XTVERSION-detected terminal name (populated async at startup) --
//
// TERM_PROGRAM is not forwarded over SSH by default, so env-based detection
// fails when claude runs remotely inside a VS Code integrated terminal.
// XTVERSION (CSI > 0 q → DCS > | name ST) goes through the pty — the query
// reaches the *client* terminal and the reply comes back through stdin.
// App.tsx fires the query when raw mode enables; setXtversionName() is called
// from the response handler. Readers should treat undefined as "not yet known"
// and fall back to env-var detection.
let xtversionName: string | undefined
/** Record the XTVERSION response. Called once from App.tsx when the reply
* arrives on stdin. No-op if already set (defend against re-probe). */
export function setXtversionName(name: string): void {
if (xtversionName === undefined) xtversionName = name
}
/** True if running in an xterm.js-based terminal (VS Code, Cursor, Windsurf
* integrated terminals). Combines TERM_PROGRAM env check (fast, sync, but
* not forwarded over SSH) with the XTVERSION probe result (async, survives
* SSH — query/reply goes through the pty). Early calls may miss the probe
* reply — call lazily (e.g. in an event handler) if SSH detection matters. */
export function isXtermJs(): boolean {
if (process.env.TERM_PROGRAM === 'vscode') return true
return xtversionName?.startsWith('xterm.js') ?? false
}
// Terminals known to correctly implement the Kitty keyboard protocol
// (CSI >1u) and/or xterm modifyOtherKeys (CSI >4;2m) for ctrl+shift+<letter>
// disambiguation. We previously enabled unconditionally (#23350), assuming
// terminals silently ignore unknown CSI — but some terminals honor the enable
// and emit codepoints our input parser doesn't handle (notably over SSH and
// in xterm.js-based terminals like VS Code). tmux is allowlisted because it
// accepts modifyOtherKeys and doesn't forward the kitty sequence to the outer
// terminal.
const EXTENDED_KEYS_TERMINALS = [
'iTerm.app',
'kitty',
'WezTerm',
'ghostty',
'tmux',
'windows-terminal',
]
/** True if this terminal correctly handles extended key reporting
* (Kitty keyboard protocol + xterm modifyOtherKeys). */
export function supportsExtendedKeys(): boolean {
return EXTENDED_KEYS_TERMINALS.includes(env.terminal ?? '')
}
/** True if the terminal scrolls the viewport when it receives cursor-up
* sequences that reach above the visible area. On Windows, conhost's
* SetConsoleCursorPosition follows the cursor into scrollback
* (microsoft/terminal#14774), yanking users to the top of their buffer
* mid-stream. WT_SESSION catches WSL-in-Windows-Terminal where platform
* is linux but output still routes through conhost. */
export function hasCursorUpViewportYankBug(): boolean {
return process.platform === 'win32' || !!process.env.WT_SESSION
}
// Computed once at module load — terminal capabilities don't change mid-session.
// Exported so callers can pass a sync-skip hint gated to specific modes.
export const SYNC_OUTPUT_SUPPORTED = isSynchronizedOutputSupported()
export type Terminal = {
stdout: Writable
stderr: Writable
}
export function writeDiffToTerminal(
terminal: Terminal,
diff: Diff,
skipSyncMarkers = false,
): void {
// No output if there are no patches
if (diff.length === 0) {
return
}
// BSU/ESU wrapping is opt-out to keep main-screen behavior unchanged.
// Callers pass skipSyncMarkers=true when the terminal doesn't support
// DEC 2026 (e.g. tmux) AND the cost matters (high-frequency alt-screen).
const useSync = !skipSyncMarkers
// Buffer all writes into a single string to avoid multiple write calls
let buffer = useSync ? BSU : ''
for (const patch of diff) {
switch (patch.type) {
case 'stdout':
buffer += patch.content
break
case 'clear':
if (patch.count > 0) {
buffer += eraseLines(patch.count)
}
break
case 'clearTerminal':
buffer += getClearTerminalSequence()
break
case 'cursorHide':
buffer += HIDE_CURSOR
break
case 'cursorShow':
buffer += SHOW_CURSOR
break
case 'cursorMove':
buffer += cursorMove(patch.x, patch.y)
break
case 'cursorTo':
buffer += cursorTo(patch.col)
break
case 'carriageReturn':
buffer += '\r'
break
case 'hyperlink':
buffer += link(patch.uri)
break
case 'styleStr':
buffer += patch.str
break
}
}
// Add synchronized update end and flush buffer
if (useSync) buffer += ESU
terminal.stdout.write(buffer)
}