@vsuryav/agent-sim 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +25 -0
  2. package/bin/agent-sim.js +25 -0
  3. package/package.json +72 -0
  4. package/src/app-paths.ts +29 -0
  5. package/src/app-sync.test.ts +75 -0
  6. package/src/app-sync.ts +110 -0
  7. package/src/cli.ts +129 -0
  8. package/src/collector/claude-code.test.ts +102 -0
  9. package/src/collector/claude-code.ts +133 -0
  10. package/src/collector/codex-cli.test.ts +116 -0
  11. package/src/collector/codex-cli.ts +149 -0
  12. package/src/collector/db.test.ts +59 -0
  13. package/src/collector/db.ts +125 -0
  14. package/src/collector/names.test.ts +21 -0
  15. package/src/collector/names.ts +28 -0
  16. package/src/collector/personality.test.ts +40 -0
  17. package/src/collector/personality.ts +46 -0
  18. package/src/collector/remote-sync.test.ts +31 -0
  19. package/src/collector/remote-sync.ts +171 -0
  20. package/src/collector/sync.test.ts +67 -0
  21. package/src/collector/sync.ts +148 -0
  22. package/src/collector/types.ts +1 -0
  23. package/src/engine/bootstrap/state.ts +3 -0
  24. package/src/engine/buddy/CompanionSprite.tsx +371 -0
  25. package/src/engine/buddy/companion.ts +133 -0
  26. package/src/engine/buddy/prompt.ts +36 -0
  27. package/src/engine/buddy/sprites.ts +514 -0
  28. package/src/engine/buddy/types.ts +148 -0
  29. package/src/engine/buddy/useBuddyNotification.tsx +98 -0
  30. package/src/engine/ink/Ansi.tsx +292 -0
  31. package/src/engine/ink/bidi.ts +139 -0
  32. package/src/engine/ink/clearTerminal.ts +74 -0
  33. package/src/engine/ink/colorize.ts +231 -0
  34. package/src/engine/ink/components/AlternateScreen.tsx +80 -0
  35. package/src/engine/ink/components/App.tsx +658 -0
  36. package/src/engine/ink/components/AppContext.ts +21 -0
  37. package/src/engine/ink/components/Box.tsx +214 -0
  38. package/src/engine/ink/components/Button.tsx +192 -0
  39. package/src/engine/ink/components/ClockContext.tsx +112 -0
  40. package/src/engine/ink/components/CursorDeclarationContext.ts +32 -0
  41. package/src/engine/ink/components/ErrorOverview.tsx +109 -0
  42. package/src/engine/ink/components/Link.tsx +42 -0
  43. package/src/engine/ink/components/Newline.tsx +39 -0
  44. package/src/engine/ink/components/NoSelect.tsx +68 -0
  45. package/src/engine/ink/components/RawAnsi.tsx +57 -0
  46. package/src/engine/ink/components/ScrollBox.tsx +237 -0
  47. package/src/engine/ink/components/Spacer.tsx +20 -0
  48. package/src/engine/ink/components/StdinContext.ts +49 -0
  49. package/src/engine/ink/components/TerminalFocusContext.tsx +52 -0
  50. package/src/engine/ink/components/TerminalSizeContext.tsx +7 -0
  51. package/src/engine/ink/components/Text.tsx +254 -0
  52. package/src/engine/ink/constants.ts +2 -0
  53. package/src/engine/ink/dom.ts +484 -0
  54. package/src/engine/ink/events/click-event.ts +38 -0
  55. package/src/engine/ink/events/dispatcher.ts +233 -0
  56. package/src/engine/ink/events/emitter.ts +39 -0
  57. package/src/engine/ink/events/event-handlers.ts +73 -0
  58. package/src/engine/ink/events/event.ts +11 -0
  59. package/src/engine/ink/events/focus-event.ts +21 -0
  60. package/src/engine/ink/events/input-event.ts +205 -0
  61. package/src/engine/ink/events/keyboard-event.ts +51 -0
  62. package/src/engine/ink/events/terminal-event.ts +107 -0
  63. package/src/engine/ink/events/terminal-focus-event.ts +19 -0
  64. package/src/engine/ink/focus.ts +181 -0
  65. package/src/engine/ink/frame.ts +124 -0
  66. package/src/engine/ink/get-max-width.ts +27 -0
  67. package/src/engine/ink/global.d.ts +18 -0
  68. package/src/engine/ink/hit-test.ts +130 -0
  69. package/src/engine/ink/hooks/use-animation-frame.ts +57 -0
  70. package/src/engine/ink/hooks/use-app.ts +8 -0
  71. package/src/engine/ink/hooks/use-declared-cursor.ts +73 -0
  72. package/src/engine/ink/hooks/use-input.ts +92 -0
  73. package/src/engine/ink/hooks/use-interval.ts +67 -0
  74. package/src/engine/ink/hooks/use-search-highlight.ts +53 -0
  75. package/src/engine/ink/hooks/use-selection.ts +104 -0
  76. package/src/engine/ink/hooks/use-stdin.ts +8 -0
  77. package/src/engine/ink/hooks/use-tab-status.ts +72 -0
  78. package/src/engine/ink/hooks/use-terminal-focus.ts +16 -0
  79. package/src/engine/ink/hooks/use-terminal-title.ts +31 -0
  80. package/src/engine/ink/hooks/use-terminal-viewport.ts +96 -0
  81. package/src/engine/ink/ink.tsx +1723 -0
  82. package/src/engine/ink/instances.ts +10 -0
  83. package/src/engine/ink/layout/engine.ts +6 -0
  84. package/src/engine/ink/layout/geometry.ts +97 -0
  85. package/src/engine/ink/layout/node.ts +152 -0
  86. package/src/engine/ink/layout/yoga.ts +308 -0
  87. package/src/engine/ink/line-width-cache.ts +24 -0
  88. package/src/engine/ink/log-update.ts +773 -0
  89. package/src/engine/ink/measure-element.ts +23 -0
  90. package/src/engine/ink/measure-text.ts +47 -0
  91. package/src/engine/ink/node-cache.ts +54 -0
  92. package/src/engine/ink/optimizer.ts +93 -0
  93. package/src/engine/ink/output.ts +797 -0
  94. package/src/engine/ink/parse-keypress.ts +801 -0
  95. package/src/engine/ink/reconciler.ts +512 -0
  96. package/src/engine/ink/render-border.ts +231 -0
  97. package/src/engine/ink/render-node-to-output.ts +1462 -0
  98. package/src/engine/ink/render-to-screen.ts +231 -0
  99. package/src/engine/ink/renderer.ts +178 -0
  100. package/src/engine/ink/root.ts +184 -0
  101. package/src/engine/ink/screen.ts +1486 -0
  102. package/src/engine/ink/searchHighlight.ts +93 -0
  103. package/src/engine/ink/selection.ts +917 -0
  104. package/src/engine/ink/squash-text-nodes.ts +92 -0
  105. package/src/engine/ink/stringWidth.ts +222 -0
  106. package/src/engine/ink/styles.ts +771 -0
  107. package/src/engine/ink/supports-hyperlinks.ts +57 -0
  108. package/src/engine/ink/tabstops.ts +46 -0
  109. package/src/engine/ink/terminal-focus-state.ts +47 -0
  110. package/src/engine/ink/terminal-querier.ts +212 -0
  111. package/src/engine/ink/terminal.ts +248 -0
  112. package/src/engine/ink/termio/ansi.ts +75 -0
  113. package/src/engine/ink/termio/csi.ts +319 -0
  114. package/src/engine/ink/termio/dec.ts +60 -0
  115. package/src/engine/ink/termio/esc.ts +67 -0
  116. package/src/engine/ink/termio/osc.ts +493 -0
  117. package/src/engine/ink/termio/parser.ts +394 -0
  118. package/src/engine/ink/termio/sgr.ts +308 -0
  119. package/src/engine/ink/termio/tokenize.ts +319 -0
  120. package/src/engine/ink/termio/types.ts +236 -0
  121. package/src/engine/ink/useTerminalNotification.ts +126 -0
  122. package/src/engine/ink/warn.ts +9 -0
  123. package/src/engine/ink/widest-line.ts +19 -0
  124. package/src/engine/ink/wrap-text.ts +74 -0
  125. package/src/engine/ink/wrapAnsi.ts +20 -0
  126. package/src/engine/native-ts/yoga-layout/enums.ts +134 -0
  127. package/src/engine/native-ts/yoga-layout/index.ts +2578 -0
  128. package/src/engine/stubs/bootstrap-state.ts +4 -0
  129. package/src/engine/stubs/debug.ts +6 -0
  130. package/src/engine/stubs/log.ts +4 -0
  131. package/src/engine/utils/debug.ts +5 -0
  132. package/src/engine/utils/earlyInput.ts +4 -0
  133. package/src/engine/utils/env.ts +15 -0
  134. package/src/engine/utils/envUtils.ts +4 -0
  135. package/src/engine/utils/execFileNoThrow.ts +24 -0
  136. package/src/engine/utils/fullscreen.ts +4 -0
  137. package/src/engine/utils/intl.ts +9 -0
  138. package/src/engine/utils/log.ts +3 -0
  139. package/src/engine/utils/semver.ts +13 -0
  140. package/src/engine/utils/sliceAnsi.ts +10 -0
  141. package/src/engine/utils/theme.ts +17 -0
  142. package/src/game/App.tsx +141 -0
  143. package/src/game/agents/behavior.ts +249 -0
  144. package/src/game/agents/speech.ts +57 -0
  145. package/src/game/canvas.ts +98 -0
  146. package/src/game/launch.ts +36 -0
  147. package/src/game/ship/ShipView.tsx +145 -0
  148. package/src/game/ship/ship-map.ts +172 -0
  149. package/src/game/ui/AgentBio.tsx +72 -0
  150. package/src/game/ui/HUD.tsx +63 -0
  151. package/src/game/ui/StatusBar.tsx +49 -0
  152. package/src/game/useKeyboard.ts +62 -0
  153. package/src/main.tsx +22 -0
  154. package/src/run-interactive.ts +74 -0
@@ -0,0 +1,394 @@
1
+ /**
2
+ * ANSI Parser - Semantic Action Generator
3
+ *
4
+ * A streaming parser for ANSI escape sequences that produces semantic actions.
5
+ * Uses the tokenizer for escape sequence boundary detection, then interprets
6
+ * each sequence to produce structured actions.
7
+ *
8
+ * Key design decisions:
9
+ * - Streaming: can process input incrementally
10
+ * - Semantic output: produces structured actions, not string tokens
11
+ * - Style tracking: maintains current text style state
12
+ */
13
+
14
+ import { getGraphemeSegmenter } from '../../utils/intl.js'
15
+ import { C0 } from './ansi.js'
16
+ import { CSI, CURSOR_STYLES, ERASE_DISPLAY, ERASE_LINE_REGION } from './csi.js'
17
+ import { DEC } from './dec.js'
18
+ import { parseEsc } from './esc.js'
19
+ import { parseOSC } from './osc.js'
20
+ import { applySGR } from './sgr.js'
21
+ import { createTokenizer, type Token, type Tokenizer } from './tokenize.js'
22
+ import type { Action, Grapheme, TextStyle } from './types.js'
23
+ import { defaultStyle } from './types.js'
24
+
25
+ // =============================================================================
26
+ // Grapheme Utilities
27
+ // =============================================================================
28
+
29
+ function isEmoji(codePoint: number): boolean {
30
+ return (
31
+ (codePoint >= 0x2600 && codePoint <= 0x26ff) ||
32
+ (codePoint >= 0x2700 && codePoint <= 0x27bf) ||
33
+ (codePoint >= 0x1f300 && codePoint <= 0x1f9ff) ||
34
+ (codePoint >= 0x1fa00 && codePoint <= 0x1faff) ||
35
+ (codePoint >= 0x1f1e0 && codePoint <= 0x1f1ff)
36
+ )
37
+ }
38
+
39
+ function isEastAsianWide(codePoint: number): boolean {
40
+ return (
41
+ (codePoint >= 0x1100 && codePoint <= 0x115f) ||
42
+ (codePoint >= 0x2e80 && codePoint <= 0x9fff) ||
43
+ (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
44
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
45
+ (codePoint >= 0xfe10 && codePoint <= 0xfe1f) ||
46
+ (codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
47
+ (codePoint >= 0xff00 && codePoint <= 0xff60) ||
48
+ (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
49
+ (codePoint >= 0x20000 && codePoint <= 0x2fffd) ||
50
+ (codePoint >= 0x30000 && codePoint <= 0x3fffd)
51
+ )
52
+ }
53
+
54
+ function hasMultipleCodepoints(str: string): boolean {
55
+ let count = 0
56
+ for (const _ of str) {
57
+ count++
58
+ if (count > 1) return true
59
+ }
60
+ return false
61
+ }
62
+
63
+ function graphemeWidth(grapheme: string): 1 | 2 {
64
+ if (hasMultipleCodepoints(grapheme)) return 2
65
+ const codePoint = grapheme.codePointAt(0)
66
+ if (codePoint === undefined) return 1
67
+ if (isEmoji(codePoint) || isEastAsianWide(codePoint)) return 2
68
+ return 1
69
+ }
70
+
71
+ function* segmentGraphemes(str: string): Generator<Grapheme> {
72
+ for (const { segment } of getGraphemeSegmenter().segment(str)) {
73
+ yield { value: segment, width: graphemeWidth(segment) }
74
+ }
75
+ }
76
+
77
+ // =============================================================================
78
+ // Sequence Parsing
79
+ // =============================================================================
80
+
81
+ function parseCSIParams(paramStr: string): number[] {
82
+ if (paramStr === '') return []
83
+ return paramStr.split(/[;:]/).map(s => (s === '' ? 0 : parseInt(s, 10)))
84
+ }
85
+
86
+ /** Parse a raw CSI sequence (e.g., "\x1b[31m") into an action */
87
+ function parseCSI(rawSequence: string): Action | null {
88
+ const inner = rawSequence.slice(2)
89
+ if (inner.length === 0) return null
90
+
91
+ const finalByte = inner.charCodeAt(inner.length - 1)
92
+ const beforeFinal = inner.slice(0, -1)
93
+
94
+ let privateMode = ''
95
+ let paramStr = beforeFinal
96
+ let intermediate = ''
97
+
98
+ if (beforeFinal.length > 0 && '?>='.includes(beforeFinal[0]!)) {
99
+ privateMode = beforeFinal[0]!
100
+ paramStr = beforeFinal.slice(1)
101
+ }
102
+
103
+ const intermediateMatch = paramStr.match(/([^0-9;:]+)$/)
104
+ if (intermediateMatch) {
105
+ intermediate = intermediateMatch[1]!
106
+ paramStr = paramStr.slice(0, -intermediate.length)
107
+ }
108
+
109
+ const params = parseCSIParams(paramStr)
110
+ const p0 = params[0] ?? 1
111
+ const p1 = params[1] ?? 1
112
+
113
+ // SGR (Select Graphic Rendition)
114
+ if (finalByte === CSI.SGR && privateMode === '') {
115
+ return { type: 'sgr', params: paramStr }
116
+ }
117
+
118
+ // Cursor movement
119
+ if (finalByte === CSI.CUU) {
120
+ return {
121
+ type: 'cursor',
122
+ action: { type: 'move', direction: 'up', count: p0 },
123
+ }
124
+ }
125
+ if (finalByte === CSI.CUD) {
126
+ return {
127
+ type: 'cursor',
128
+ action: { type: 'move', direction: 'down', count: p0 },
129
+ }
130
+ }
131
+ if (finalByte === CSI.CUF) {
132
+ return {
133
+ type: 'cursor',
134
+ action: { type: 'move', direction: 'forward', count: p0 },
135
+ }
136
+ }
137
+ if (finalByte === CSI.CUB) {
138
+ return {
139
+ type: 'cursor',
140
+ action: { type: 'move', direction: 'back', count: p0 },
141
+ }
142
+ }
143
+ if (finalByte === CSI.CNL) {
144
+ return { type: 'cursor', action: { type: 'nextLine', count: p0 } }
145
+ }
146
+ if (finalByte === CSI.CPL) {
147
+ return { type: 'cursor', action: { type: 'prevLine', count: p0 } }
148
+ }
149
+ if (finalByte === CSI.CHA) {
150
+ return { type: 'cursor', action: { type: 'column', col: p0 } }
151
+ }
152
+ if (finalByte === CSI.CUP || finalByte === CSI.HVP) {
153
+ return { type: 'cursor', action: { type: 'position', row: p0, col: p1 } }
154
+ }
155
+ if (finalByte === CSI.VPA) {
156
+ return { type: 'cursor', action: { type: 'row', row: p0 } }
157
+ }
158
+
159
+ // Erase
160
+ if (finalByte === CSI.ED) {
161
+ const region = ERASE_DISPLAY[params[0] ?? 0] ?? 'toEnd'
162
+ return { type: 'erase', action: { type: 'display', region } }
163
+ }
164
+ if (finalByte === CSI.EL) {
165
+ const region = ERASE_LINE_REGION[params[0] ?? 0] ?? 'toEnd'
166
+ return { type: 'erase', action: { type: 'line', region } }
167
+ }
168
+ if (finalByte === CSI.ECH) {
169
+ return { type: 'erase', action: { type: 'chars', count: p0 } }
170
+ }
171
+
172
+ // Scroll
173
+ if (finalByte === CSI.SU) {
174
+ return { type: 'scroll', action: { type: 'up', count: p0 } }
175
+ }
176
+ if (finalByte === CSI.SD) {
177
+ return { type: 'scroll', action: { type: 'down', count: p0 } }
178
+ }
179
+ if (finalByte === CSI.DECSTBM) {
180
+ return {
181
+ type: 'scroll',
182
+ action: { type: 'setRegion', top: p0, bottom: p1 },
183
+ }
184
+ }
185
+
186
+ // Cursor save/restore
187
+ if (finalByte === CSI.SCOSC) {
188
+ return { type: 'cursor', action: { type: 'save' } }
189
+ }
190
+ if (finalByte === CSI.SCORC) {
191
+ return { type: 'cursor', action: { type: 'restore' } }
192
+ }
193
+
194
+ // Cursor style
195
+ if (finalByte === CSI.DECSCUSR && intermediate === ' ') {
196
+ const styleInfo = CURSOR_STYLES[p0] ?? CURSOR_STYLES[0]!
197
+ return { type: 'cursor', action: { type: 'style', ...styleInfo } }
198
+ }
199
+
200
+ // Private modes
201
+ if (privateMode === '?' && (finalByte === CSI.SM || finalByte === CSI.RM)) {
202
+ const enabled = finalByte === CSI.SM
203
+
204
+ if (p0 === DEC.CURSOR_VISIBLE) {
205
+ return {
206
+ type: 'cursor',
207
+ action: enabled ? { type: 'show' } : { type: 'hide' },
208
+ }
209
+ }
210
+ if (p0 === DEC.ALT_SCREEN_CLEAR || p0 === DEC.ALT_SCREEN) {
211
+ return { type: 'mode', action: { type: 'alternateScreen', enabled } }
212
+ }
213
+ if (p0 === DEC.BRACKETED_PASTE) {
214
+ return { type: 'mode', action: { type: 'bracketedPaste', enabled } }
215
+ }
216
+ if (p0 === DEC.MOUSE_NORMAL) {
217
+ return {
218
+ type: 'mode',
219
+ action: { type: 'mouseTracking', mode: enabled ? 'normal' : 'off' },
220
+ }
221
+ }
222
+ if (p0 === DEC.MOUSE_BUTTON) {
223
+ return {
224
+ type: 'mode',
225
+ action: { type: 'mouseTracking', mode: enabled ? 'button' : 'off' },
226
+ }
227
+ }
228
+ if (p0 === DEC.MOUSE_ANY) {
229
+ return {
230
+ type: 'mode',
231
+ action: { type: 'mouseTracking', mode: enabled ? 'any' : 'off' },
232
+ }
233
+ }
234
+ if (p0 === DEC.FOCUS_EVENTS) {
235
+ return { type: 'mode', action: { type: 'focusEvents', enabled } }
236
+ }
237
+ }
238
+
239
+ return { type: 'unknown', sequence: rawSequence }
240
+ }
241
+
242
+ /**
243
+ * Identify the type of escape sequence from its raw form.
244
+ */
245
+ function identifySequence(
246
+ seq: string,
247
+ ): 'csi' | 'osc' | 'esc' | 'ss3' | 'unknown' {
248
+ if (seq.length < 2) return 'unknown'
249
+ if (seq.charCodeAt(0) !== C0.ESC) return 'unknown'
250
+
251
+ const second = seq.charCodeAt(1)
252
+ if (second === 0x5b) return 'csi' // [
253
+ if (second === 0x5d) return 'osc' // ]
254
+ if (second === 0x4f) return 'ss3' // O
255
+ return 'esc'
256
+ }
257
+
258
+ // =============================================================================
259
+ // Main Parser
260
+ // =============================================================================
261
+
262
+ /**
263
+ * Parser class - maintains state for streaming/incremental parsing
264
+ *
265
+ * Usage:
266
+ * ```typescript
267
+ * const parser = new Parser()
268
+ * const actions1 = parser.feed('partial\x1b[')
269
+ * const actions2 = parser.feed('31mred') // state maintained internally
270
+ * ```
271
+ */
272
+ export class Parser {
273
+ private tokenizer: Tokenizer = createTokenizer()
274
+
275
+ style: TextStyle = defaultStyle()
276
+ inLink = false
277
+ linkUrl: string | undefined
278
+
279
+ reset(): void {
280
+ this.tokenizer.reset()
281
+ this.style = defaultStyle()
282
+ this.inLink = false
283
+ this.linkUrl = undefined
284
+ }
285
+
286
+ /** Feed input and get resulting actions */
287
+ feed(input: string): Action[] {
288
+ const tokens = this.tokenizer.feed(input)
289
+ const actions: Action[] = []
290
+
291
+ for (const token of tokens) {
292
+ const tokenActions = this.processToken(token)
293
+ actions.push(...tokenActions)
294
+ }
295
+
296
+ return actions
297
+ }
298
+
299
+ private processToken(token: Token): Action[] {
300
+ switch (token.type) {
301
+ case 'text':
302
+ return this.processText(token.value)
303
+
304
+ case 'sequence':
305
+ return this.processSequence(token.value)
306
+ }
307
+ }
308
+
309
+ private processText(text: string): Action[] {
310
+ // Handle BEL characters embedded in text
311
+ const actions: Action[] = []
312
+ let current = ''
313
+
314
+ for (const char of text) {
315
+ if (char.charCodeAt(0) === C0.BEL) {
316
+ if (current) {
317
+ const graphemes = [...segmentGraphemes(current)]
318
+ if (graphemes.length > 0) {
319
+ actions.push({ type: 'text', graphemes, style: { ...this.style } })
320
+ }
321
+ current = ''
322
+ }
323
+ actions.push({ type: 'bell' })
324
+ } else {
325
+ current += char
326
+ }
327
+ }
328
+
329
+ if (current) {
330
+ const graphemes = [...segmentGraphemes(current)]
331
+ if (graphemes.length > 0) {
332
+ actions.push({ type: 'text', graphemes, style: { ...this.style } })
333
+ }
334
+ }
335
+
336
+ return actions
337
+ }
338
+
339
+ private processSequence(seq: string): Action[] {
340
+ const seqType = identifySequence(seq)
341
+
342
+ switch (seqType) {
343
+ case 'csi': {
344
+ const action = parseCSI(seq)
345
+ if (!action) return []
346
+ if (action.type === 'sgr') {
347
+ this.style = applySGR(action.params, this.style)
348
+ return []
349
+ }
350
+ return [action]
351
+ }
352
+
353
+ case 'osc': {
354
+ // Extract OSC content (between ESC ] and terminator)
355
+ let content = seq.slice(2)
356
+ // Remove terminator (BEL or ESC \)
357
+ if (content.endsWith('\x07')) {
358
+ content = content.slice(0, -1)
359
+ } else if (content.endsWith('\x1b\\')) {
360
+ content = content.slice(0, -2)
361
+ }
362
+
363
+ const action = parseOSC(content)
364
+ if (action) {
365
+ if (action.type === 'link') {
366
+ if (action.action.type === 'start') {
367
+ this.inLink = true
368
+ this.linkUrl = action.action.url
369
+ } else {
370
+ this.inLink = false
371
+ this.linkUrl = undefined
372
+ }
373
+ }
374
+ return [action]
375
+ }
376
+ return []
377
+ }
378
+
379
+ case 'esc': {
380
+ const escContent = seq.slice(1)
381
+ const action = parseEsc(escContent)
382
+ return action ? [action] : []
383
+ }
384
+
385
+ case 'ss3':
386
+ // SS3 sequences are typically cursor keys in application mode
387
+ // For output parsing, treat as unknown
388
+ return [{ type: 'unknown', sequence: seq }]
389
+
390
+ default:
391
+ return [{ type: 'unknown', sequence: seq }]
392
+ }
393
+ }
394
+ }
@@ -0,0 +1,308 @@
1
+ /**
2
+ * SGR (Select Graphic Rendition) Parser
3
+ *
4
+ * Parses SGR parameters and applies them to a TextStyle.
5
+ * Handles both semicolon (;) and colon (:) separated parameters.
6
+ */
7
+
8
+ import type { NamedColor, TextStyle, UnderlineStyle } from './types.js'
9
+ import { defaultStyle } from './types.js'
10
+
11
+ const NAMED_COLORS: NamedColor[] = [
12
+ 'black',
13
+ 'red',
14
+ 'green',
15
+ 'yellow',
16
+ 'blue',
17
+ 'magenta',
18
+ 'cyan',
19
+ 'white',
20
+ 'brightBlack',
21
+ 'brightRed',
22
+ 'brightGreen',
23
+ 'brightYellow',
24
+ 'brightBlue',
25
+ 'brightMagenta',
26
+ 'brightCyan',
27
+ 'brightWhite',
28
+ ]
29
+
30
+ const UNDERLINE_STYLES: UnderlineStyle[] = [
31
+ 'none',
32
+ 'single',
33
+ 'double',
34
+ 'curly',
35
+ 'dotted',
36
+ 'dashed',
37
+ ]
38
+
39
+ type Param = { value: number | null; subparams: number[]; colon: boolean }
40
+
41
+ function parseParams(str: string): Param[] {
42
+ if (str === '') return [{ value: 0, subparams: [], colon: false }]
43
+
44
+ const result: Param[] = []
45
+ let current: Param = { value: null, subparams: [], colon: false }
46
+ let num = ''
47
+ let inSub = false
48
+
49
+ for (let i = 0; i <= str.length; i++) {
50
+ const c = str[i]
51
+ if (c === ';' || c === undefined) {
52
+ const n = num === '' ? null : parseInt(num, 10)
53
+ if (inSub) {
54
+ if (n !== null) current.subparams.push(n)
55
+ } else {
56
+ current.value = n
57
+ }
58
+ result.push(current)
59
+ current = { value: null, subparams: [], colon: false }
60
+ num = ''
61
+ inSub = false
62
+ } else if (c === ':') {
63
+ const n = num === '' ? null : parseInt(num, 10)
64
+ if (!inSub) {
65
+ current.value = n
66
+ current.colon = true
67
+ inSub = true
68
+ } else {
69
+ if (n !== null) current.subparams.push(n)
70
+ }
71
+ num = ''
72
+ } else if (c >= '0' && c <= '9') {
73
+ num += c
74
+ }
75
+ }
76
+ return result
77
+ }
78
+
79
+ function parseExtendedColor(
80
+ params: Param[],
81
+ idx: number,
82
+ ): { r: number; g: number; b: number } | { index: number } | null {
83
+ const p = params[idx]
84
+ if (!p) return null
85
+
86
+ if (p.colon && p.subparams.length >= 1) {
87
+ if (p.subparams[0] === 5 && p.subparams.length >= 2) {
88
+ return { index: p.subparams[1]! }
89
+ }
90
+ if (p.subparams[0] === 2 && p.subparams.length >= 4) {
91
+ const off = p.subparams.length >= 5 ? 1 : 0
92
+ return {
93
+ r: p.subparams[1 + off]!,
94
+ g: p.subparams[2 + off]!,
95
+ b: p.subparams[3 + off]!,
96
+ }
97
+ }
98
+ }
99
+
100
+ const next = params[idx + 1]
101
+ if (!next) return null
102
+ if (
103
+ next.value === 5 &&
104
+ params[idx + 2]?.value !== null &&
105
+ params[idx + 2]?.value !== undefined
106
+ ) {
107
+ return { index: params[idx + 2]!.value! }
108
+ }
109
+ if (next.value === 2) {
110
+ const r = params[idx + 2]?.value
111
+ const g = params[idx + 3]?.value
112
+ const b = params[idx + 4]?.value
113
+ if (
114
+ r !== null &&
115
+ r !== undefined &&
116
+ g !== null &&
117
+ g !== undefined &&
118
+ b !== null &&
119
+ b !== undefined
120
+ ) {
121
+ return { r, g, b }
122
+ }
123
+ }
124
+ return null
125
+ }
126
+
127
+ export function applySGR(paramStr: string, style: TextStyle): TextStyle {
128
+ const params = parseParams(paramStr)
129
+ let s = { ...style }
130
+ let i = 0
131
+
132
+ while (i < params.length) {
133
+ const p = params[i]!
134
+ const code = p.value ?? 0
135
+
136
+ if (code === 0) {
137
+ s = defaultStyle()
138
+ i++
139
+ continue
140
+ }
141
+ if (code === 1) {
142
+ s.bold = true
143
+ i++
144
+ continue
145
+ }
146
+ if (code === 2) {
147
+ s.dim = true
148
+ i++
149
+ continue
150
+ }
151
+ if (code === 3) {
152
+ s.italic = true
153
+ i++
154
+ continue
155
+ }
156
+ if (code === 4) {
157
+ s.underline = p.colon
158
+ ? (UNDERLINE_STYLES[p.subparams[0]!] ?? 'single')
159
+ : 'single'
160
+ i++
161
+ continue
162
+ }
163
+ if (code === 5 || code === 6) {
164
+ s.blink = true
165
+ i++
166
+ continue
167
+ }
168
+ if (code === 7) {
169
+ s.inverse = true
170
+ i++
171
+ continue
172
+ }
173
+ if (code === 8) {
174
+ s.hidden = true
175
+ i++
176
+ continue
177
+ }
178
+ if (code === 9) {
179
+ s.strikethrough = true
180
+ i++
181
+ continue
182
+ }
183
+ if (code === 21) {
184
+ s.underline = 'double'
185
+ i++
186
+ continue
187
+ }
188
+ if (code === 22) {
189
+ s.bold = false
190
+ s.dim = false
191
+ i++
192
+ continue
193
+ }
194
+ if (code === 23) {
195
+ s.italic = false
196
+ i++
197
+ continue
198
+ }
199
+ if (code === 24) {
200
+ s.underline = 'none'
201
+ i++
202
+ continue
203
+ }
204
+ if (code === 25) {
205
+ s.blink = false
206
+ i++
207
+ continue
208
+ }
209
+ if (code === 27) {
210
+ s.inverse = false
211
+ i++
212
+ continue
213
+ }
214
+ if (code === 28) {
215
+ s.hidden = false
216
+ i++
217
+ continue
218
+ }
219
+ if (code === 29) {
220
+ s.strikethrough = false
221
+ i++
222
+ continue
223
+ }
224
+ if (code === 53) {
225
+ s.overline = true
226
+ i++
227
+ continue
228
+ }
229
+ if (code === 55) {
230
+ s.overline = false
231
+ i++
232
+ continue
233
+ }
234
+
235
+ if (code >= 30 && code <= 37) {
236
+ s.fg = { type: 'named', name: NAMED_COLORS[code - 30]! }
237
+ i++
238
+ continue
239
+ }
240
+ if (code === 39) {
241
+ s.fg = { type: 'default' }
242
+ i++
243
+ continue
244
+ }
245
+ if (code >= 40 && code <= 47) {
246
+ s.bg = { type: 'named', name: NAMED_COLORS[code - 40]! }
247
+ i++
248
+ continue
249
+ }
250
+ if (code === 49) {
251
+ s.bg = { type: 'default' }
252
+ i++
253
+ continue
254
+ }
255
+ if (code >= 90 && code <= 97) {
256
+ s.fg = { type: 'named', name: NAMED_COLORS[code - 90 + 8]! }
257
+ i++
258
+ continue
259
+ }
260
+ if (code >= 100 && code <= 107) {
261
+ s.bg = { type: 'named', name: NAMED_COLORS[code - 100 + 8]! }
262
+ i++
263
+ continue
264
+ }
265
+
266
+ if (code === 38) {
267
+ const c = parseExtendedColor(params, i)
268
+ if (c) {
269
+ s.fg =
270
+ 'index' in c
271
+ ? { type: 'indexed', index: c.index }
272
+ : { type: 'rgb', ...c }
273
+ i += p.colon ? 1 : 'index' in c ? 3 : 5
274
+ continue
275
+ }
276
+ }
277
+ if (code === 48) {
278
+ const c = parseExtendedColor(params, i)
279
+ if (c) {
280
+ s.bg =
281
+ 'index' in c
282
+ ? { type: 'indexed', index: c.index }
283
+ : { type: 'rgb', ...c }
284
+ i += p.colon ? 1 : 'index' in c ? 3 : 5
285
+ continue
286
+ }
287
+ }
288
+ if (code === 58) {
289
+ const c = parseExtendedColor(params, i)
290
+ if (c) {
291
+ s.underlineColor =
292
+ 'index' in c
293
+ ? { type: 'indexed', index: c.index }
294
+ : { type: 'rgb', ...c }
295
+ i += p.colon ? 1 : 'index' in c ? 3 : 5
296
+ continue
297
+ }
298
+ }
299
+ if (code === 59) {
300
+ s.underlineColor = { type: 'default' }
301
+ i++
302
+ continue
303
+ }
304
+
305
+ i++
306
+ }
307
+ return s
308
+ }