forked from
oppi.li/claude-code
source dump of claude code
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