source dump of claude code
at main 236 lines 7.1 kB view raw
1/** 2 * ANSI Parser - Semantic Types 3 * 4 * These types represent the semantic meaning of ANSI escape sequences, 5 * not their string representation. Inspired by ghostty's action-based design. 6 */ 7 8// ============================================================================= 9// Colors 10// ============================================================================= 11 12/** Named colors from the 16-color palette */ 13export type NamedColor = 14 | 'black' 15 | 'red' 16 | 'green' 17 | 'yellow' 18 | 'blue' 19 | 'magenta' 20 | 'cyan' 21 | 'white' 22 | 'brightBlack' 23 | 'brightRed' 24 | 'brightGreen' 25 | 'brightYellow' 26 | 'brightBlue' 27 | 'brightMagenta' 28 | 'brightCyan' 29 | 'brightWhite' 30 31/** Color specification - can be named, indexed (256), or RGB */ 32export type Color = 33 | { type: 'named'; name: NamedColor } 34 | { type: 'indexed'; index: number } // 0-255 35 | { type: 'rgb'; r: number; g: number; b: number } 36 | { type: 'default' } 37 38// ============================================================================= 39// Text Styles 40// ============================================================================= 41 42/** Underline style variants */ 43export type UnderlineStyle = 44 | 'none' 45 | 'single' 46 | 'double' 47 | 'curly' 48 | 'dotted' 49 | 'dashed' 50 51/** Text style attributes - represents current styling state */ 52export type TextStyle = { 53 bold: boolean 54 dim: boolean 55 italic: boolean 56 underline: UnderlineStyle 57 blink: boolean 58 inverse: boolean 59 hidden: boolean 60 strikethrough: boolean 61 overline: boolean 62 fg: Color 63 bg: Color 64 underlineColor: Color 65} 66 67/** Create a default (reset) text style */ 68export function defaultStyle(): TextStyle { 69 return { 70 bold: false, 71 dim: false, 72 italic: false, 73 underline: 'none', 74 blink: false, 75 inverse: false, 76 hidden: false, 77 strikethrough: false, 78 overline: false, 79 fg: { type: 'default' }, 80 bg: { type: 'default' }, 81 underlineColor: { type: 'default' }, 82 } 83} 84 85/** Check if two styles are equal */ 86export function stylesEqual(a: TextStyle, b: TextStyle): boolean { 87 return ( 88 a.bold === b.bold && 89 a.dim === b.dim && 90 a.italic === b.italic && 91 a.underline === b.underline && 92 a.blink === b.blink && 93 a.inverse === b.inverse && 94 a.hidden === b.hidden && 95 a.strikethrough === b.strikethrough && 96 a.overline === b.overline && 97 colorsEqual(a.fg, b.fg) && 98 colorsEqual(a.bg, b.bg) && 99 colorsEqual(a.underlineColor, b.underlineColor) 100 ) 101} 102 103/** Check if two colors are equal */ 104export function colorsEqual(a: Color, b: Color): boolean { 105 if (a.type !== b.type) return false 106 switch (a.type) { 107 case 'named': 108 return a.name === (b as typeof a).name 109 case 'indexed': 110 return a.index === (b as typeof a).index 111 case 'rgb': 112 return ( 113 a.r === (b as typeof a).r && 114 a.g === (b as typeof a).g && 115 a.b === (b as typeof a).b 116 ) 117 case 'default': 118 return true 119 } 120} 121 122// ============================================================================= 123// Cursor Actions 124// ============================================================================= 125 126export type CursorDirection = 'up' | 'down' | 'forward' | 'back' 127 128export type CursorAction = 129 | { type: 'move'; direction: CursorDirection; count: number } 130 | { type: 'position'; row: number; col: number } 131 | { type: 'column'; col: number } 132 | { type: 'row'; row: number } 133 | { type: 'save' } 134 | { type: 'restore' } 135 | { type: 'show' } 136 | { type: 'hide' } 137 | { 138 type: 'style' 139 style: 'block' | 'underline' | 'bar' 140 blinking: boolean 141 } 142 | { type: 'nextLine'; count: number } 143 | { type: 'prevLine'; count: number } 144 145// ============================================================================= 146// Erase Actions 147// ============================================================================= 148 149export type EraseAction = 150 | { type: 'display'; region: 'toEnd' | 'toStart' | 'all' | 'scrollback' } 151 | { type: 'line'; region: 'toEnd' | 'toStart' | 'all' } 152 | { type: 'chars'; count: number } 153 154// ============================================================================= 155// Scroll Actions 156// ============================================================================= 157 158export type ScrollAction = 159 | { type: 'up'; count: number } 160 | { type: 'down'; count: number } 161 | { type: 'setRegion'; top: number; bottom: number } 162 163// ============================================================================= 164// Mode Actions 165// ============================================================================= 166 167export type ModeAction = 168 | { type: 'alternateScreen'; enabled: boolean } 169 | { type: 'bracketedPaste'; enabled: boolean } 170 | { type: 'mouseTracking'; mode: 'off' | 'normal' | 'button' | 'any' } 171 | { type: 'focusEvents'; enabled: boolean } 172 173// ============================================================================= 174// Link Actions (OSC 8) 175// ============================================================================= 176 177export type LinkAction = 178 | { type: 'start'; url: string; params?: Record<string, string> } 179 | { type: 'end' } 180 181// ============================================================================= 182// Title Actions (OSC 0/1/2) 183// ============================================================================= 184 185export type TitleAction = 186 | { type: 'windowTitle'; title: string } 187 | { type: 'iconName'; name: string } 188 | { type: 'both'; title: string } 189 190// ============================================================================= 191// Tab Status Action (OSC 21337) 192// ============================================================================= 193 194/** 195 * Per-tab chrome metadata. Tristate for each field: 196 * - property absent → not mentioned in sequence, no change 197 * - null → explicitly cleared (bare key or key= with empty value) 198 * - value → set to this 199 */ 200export type TabStatusAction = { 201 indicator?: Color | null 202 status?: string | null 203 statusColor?: Color | null 204} 205 206// ============================================================================= 207// Parsed Segments - The output of the parser 208// ============================================================================= 209 210/** A segment of styled text */ 211export type TextSegment = { 212 type: 'text' 213 text: string 214 style: TextStyle 215} 216 217/** A grapheme (visual character unit) with width info */ 218export type Grapheme = { 219 value: string 220 width: 1 | 2 // Display width in columns 221} 222 223/** All possible parsed actions */ 224export type Action = 225 | { type: 'text'; graphemes: Grapheme[]; style: TextStyle } 226 | { type: 'cursor'; action: CursorAction } 227 | { type: 'erase'; action: EraseAction } 228 | { type: 'scroll'; action: ScrollAction } 229 | { type: 'mode'; action: ModeAction } 230 | { type: 'link'; action: LinkAction } 231 | { type: 'title'; action: TitleAction } 232 | { type: 'tabStatus'; action: TabStatusAction } 233 | { type: 'sgr'; params: string } // Select Graphic Rendition (style change) 234 | { type: 'bell' } 235 | { type: 'reset' } // Full terminal reset (ESC c) 236 | { type: 'unknown'; sequence: string } // Unrecognized sequence