@silvery/term 0.3.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.
- package/package.json +54 -0
- package/src/adapters/canvas-adapter.ts +356 -0
- package/src/adapters/dom-adapter.ts +452 -0
- package/src/adapters/flexily-zero-adapter.ts +368 -0
- package/src/adapters/terminal-adapter.ts +305 -0
- package/src/adapters/yoga-adapter.ts +370 -0
- package/src/ansi/ansi.ts +251 -0
- package/src/ansi/constants.ts +76 -0
- package/src/ansi/detection.ts +441 -0
- package/src/ansi/hyperlink.ts +38 -0
- package/src/ansi/index.ts +201 -0
- package/src/ansi/patch-console.ts +159 -0
- package/src/ansi/sgr-codes.ts +34 -0
- package/src/ansi/storybook.ts +209 -0
- package/src/ansi/term.ts +724 -0
- package/src/ansi/types.ts +202 -0
- package/src/ansi/underline.ts +156 -0
- package/src/ansi/utils.ts +65 -0
- package/src/ansi-sanitize.ts +509 -0
- package/src/app.ts +571 -0
- package/src/bound-term.ts +94 -0
- package/src/bracketed-paste.ts +75 -0
- package/src/browser-renderer.ts +174 -0
- package/src/buffer.ts +1984 -0
- package/src/clipboard.ts +74 -0
- package/src/cursor-query.ts +85 -0
- package/src/device-attrs.ts +228 -0
- package/src/devtools.ts +123 -0
- package/src/dom/index.ts +194 -0
- package/src/errors.ts +39 -0
- package/src/focus-reporting.ts +48 -0
- package/src/hit-registry-core.ts +228 -0
- package/src/hit-registry.ts +176 -0
- package/src/index.ts +458 -0
- package/src/input.ts +119 -0
- package/src/inspector.ts +155 -0
- package/src/kitty-detect.ts +95 -0
- package/src/kitty-manager.ts +160 -0
- package/src/layout-engine.ts +296 -0
- package/src/layout.ts +26 -0
- package/src/measurer.ts +74 -0
- package/src/mode-query.ts +106 -0
- package/src/mouse-events.ts +419 -0
- package/src/mouse.ts +83 -0
- package/src/non-tty.ts +223 -0
- package/src/osc-markers.ts +32 -0
- package/src/osc-palette.ts +169 -0
- package/src/output.ts +406 -0
- package/src/pane-manager.ts +248 -0
- package/src/pipeline/CLAUDE.md +587 -0
- package/src/pipeline/content-phase-adapter.ts +976 -0
- package/src/pipeline/content-phase.ts +1765 -0
- package/src/pipeline/helpers.ts +42 -0
- package/src/pipeline/index.ts +416 -0
- package/src/pipeline/layout-phase.ts +686 -0
- package/src/pipeline/measure-phase.ts +198 -0
- package/src/pipeline/measure-stats.ts +21 -0
- package/src/pipeline/output-phase.ts +2593 -0
- package/src/pipeline/render-box.ts +343 -0
- package/src/pipeline/render-helpers.ts +243 -0
- package/src/pipeline/render-text.ts +1255 -0
- package/src/pipeline/types.ts +161 -0
- package/src/pipeline.ts +29 -0
- package/src/pixel-size.ts +119 -0
- package/src/render-adapter.ts +179 -0
- package/src/renderer.ts +1330 -0
- package/src/runtime/create-app.tsx +1845 -0
- package/src/runtime/create-buffer.ts +18 -0
- package/src/runtime/create-runtime.ts +325 -0
- package/src/runtime/diff.ts +56 -0
- package/src/runtime/event-handlers.ts +254 -0
- package/src/runtime/index.ts +119 -0
- package/src/runtime/keys.ts +8 -0
- package/src/runtime/layout.ts +164 -0
- package/src/runtime/run.tsx +318 -0
- package/src/runtime/term-provider.ts +399 -0
- package/src/runtime/terminal-lifecycle.ts +246 -0
- package/src/runtime/tick.ts +219 -0
- package/src/runtime/types.ts +210 -0
- package/src/scheduler.ts +723 -0
- package/src/screenshot.ts +57 -0
- package/src/scroll-region.ts +69 -0
- package/src/scroll-utils.ts +97 -0
- package/src/term-def.ts +267 -0
- package/src/terminal-caps.ts +5 -0
- package/src/terminal-colors.ts +216 -0
- package/src/termtest.ts +224 -0
- package/src/text-sizing.ts +109 -0
- package/src/toolbelt/index.ts +72 -0
- package/src/unicode.ts +1763 -0
- package/src/xterm/index.ts +491 -0
- package/src/xterm/xterm-provider.ts +204 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises"
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Types
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
export interface Screenshotter {
|
|
8
|
+
/** Render HTML to PNG. First call starts Playwright (~3-5s), subsequent calls ~200ms */
|
|
9
|
+
capture(html: string, outputPath?: string): Promise<Buffer>
|
|
10
|
+
/** Close browser */
|
|
11
|
+
close(): Promise<void>
|
|
12
|
+
[Symbol.asyncDispose](): Promise<void>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Factory
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
export function createScreenshotter(): Screenshotter {
|
|
20
|
+
let browser: import("playwright").Browser | null = null
|
|
21
|
+
let page: import("playwright").Page | null = null
|
|
22
|
+
|
|
23
|
+
async function ensureBrowser() {
|
|
24
|
+
if (browser && page) return page
|
|
25
|
+
|
|
26
|
+
const { chromium } = await import("playwright")
|
|
27
|
+
browser = await chromium.launch()
|
|
28
|
+
const context = await browser.newContext()
|
|
29
|
+
page = await context.newPage()
|
|
30
|
+
return page
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function capture(html: string, outputPath?: string): Promise<Buffer> {
|
|
34
|
+
const p = await ensureBrowser()
|
|
35
|
+
await p.setContent(html, { waitUntil: "load" })
|
|
36
|
+
await p.waitForTimeout(50)
|
|
37
|
+
const buffer = (await p.screenshot({ fullPage: true })) as Buffer
|
|
38
|
+
if (outputPath) {
|
|
39
|
+
await writeFile(outputPath, buffer)
|
|
40
|
+
}
|
|
41
|
+
return buffer
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function close() {
|
|
45
|
+
if (browser) {
|
|
46
|
+
await browser.close()
|
|
47
|
+
browser = null
|
|
48
|
+
page = null
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
capture,
|
|
54
|
+
close,
|
|
55
|
+
[Symbol.asyncDispose]: close,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal scroll region (DECSTBM) utilities.
|
|
3
|
+
*
|
|
4
|
+
* Scroll regions tell the terminal to natively scroll content within
|
|
5
|
+
* a defined row range, which is faster than re-rendering all cells.
|
|
6
|
+
*
|
|
7
|
+
* DECSTBM (DEC Set Top and Bottom Margins) is supported by most modern
|
|
8
|
+
* terminal emulators: xterm, iTerm2, Kitty, Ghostty, WezTerm, etc.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const ESC = "\x1b"
|
|
12
|
+
|
|
13
|
+
/** Set terminal scroll region (1-indexed top and bottom rows). */
|
|
14
|
+
export function setScrollRegion(stdout: NodeJS.WriteStream, top: number, bottom: number): void {
|
|
15
|
+
stdout.write(`${ESC}[${top};${bottom}r`)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Reset scroll region to full terminal. */
|
|
19
|
+
export function resetScrollRegion(stdout: NodeJS.WriteStream): void {
|
|
20
|
+
stdout.write(`${ESC}[r`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Scroll content up by N lines within the current scroll region. */
|
|
24
|
+
export function scrollUp(stdout: NodeJS.WriteStream, lines: number = 1): void {
|
|
25
|
+
stdout.write(`${ESC}[${lines}S`)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Scroll content down by N lines within the current scroll region. */
|
|
29
|
+
export function scrollDown(stdout: NodeJS.WriteStream, lines: number = 1): void {
|
|
30
|
+
stdout.write(`${ESC}[${lines}T`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Move cursor to a specific position (1-indexed row and column). */
|
|
34
|
+
export function moveCursor(stdout: NodeJS.WriteStream, row: number, col: number): void {
|
|
35
|
+
stdout.write(`${ESC}[${row};${col}H`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ScrollRegionConfig {
|
|
39
|
+
/** Top row of the scrollable area (0-indexed). */
|
|
40
|
+
top: number
|
|
41
|
+
/** Bottom row of the scrollable area (0-indexed). */
|
|
42
|
+
bottom: number
|
|
43
|
+
/** Whether scroll region optimization is enabled. */
|
|
44
|
+
enabled: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Detect if the terminal likely supports DECSTBM scroll regions.
|
|
49
|
+
*
|
|
50
|
+
* Most modern terminals do (xterm, iTerm2, Kitty, Ghostty, WezTerm, etc.)
|
|
51
|
+
* but some (e.g., Linux console) may not handle them correctly.
|
|
52
|
+
*/
|
|
53
|
+
export function supportsScrollRegions(): boolean {
|
|
54
|
+
const term = process.env.TERM ?? ""
|
|
55
|
+
const termProgram = process.env.TERM_PROGRAM ?? ""
|
|
56
|
+
|
|
57
|
+
// Known-good terminal programs
|
|
58
|
+
if (termProgram === "iTerm.app" || termProgram === "WezTerm" || termProgram === "ghostty" || termProgram === "vscode")
|
|
59
|
+
return true
|
|
60
|
+
|
|
61
|
+
// Known-good TERM values
|
|
62
|
+
if (term.includes("xterm") || term.includes("screen") || term.includes("tmux") || term.includes("kitty")) return true
|
|
63
|
+
|
|
64
|
+
// Linux console doesn't support DECSTBM
|
|
65
|
+
if (term === "linux") return false
|
|
66
|
+
|
|
67
|
+
// Default: assume support for any term that's not empty
|
|
68
|
+
return term !== ""
|
|
69
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scroll Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared functions for edge-based scrolling behavior across VirtualList,
|
|
5
|
+
* HorizontalVirtualList, and other scroll-aware components.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Calculate edge-based scroll offset.
|
|
10
|
+
*
|
|
11
|
+
* Only scrolls when cursor approaches the edge of the visible area.
|
|
12
|
+
* This provides smoother scrolling by starting to scroll before hitting
|
|
13
|
+
* the absolute edge, maintaining context around the selected item.
|
|
14
|
+
*
|
|
15
|
+
* ## Algorithm
|
|
16
|
+
*
|
|
17
|
+
* The viewport is divided into zones:
|
|
18
|
+
* ```
|
|
19
|
+
* |<padding>|<------ safe zone ------>|<padding>|
|
|
20
|
+
* | scroll | no scroll needed | scroll |
|
|
21
|
+
* | if < | | if > |
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* When the selected item enters a padding zone, the viewport scrolls
|
|
25
|
+
* to keep the item visible with margin.
|
|
26
|
+
*
|
|
27
|
+
* ## Asymmetry Note
|
|
28
|
+
*
|
|
29
|
+
* The +1 in the "scroll down/right" case is intentional:
|
|
30
|
+
* - Offset points to the TOP/LEFT of the viewport
|
|
31
|
+
* - We want the selected item to be `padding` items from the BOTTOM/RIGHT
|
|
32
|
+
* - Formula: `selectedIndex - visibleCount + padding + 1`
|
|
33
|
+
*
|
|
34
|
+
* Example: visibleCount=10, padding=2, selectedIndex=15
|
|
35
|
+
* offset = 15 - 10 + 2 + 1 = 8
|
|
36
|
+
* viewport shows items 8-17, selected item 15 is at position 7 (2 from bottom)
|
|
37
|
+
*
|
|
38
|
+
* @param selectedIndex - Currently selected item index
|
|
39
|
+
* @param currentOffset - Current scroll offset (topmost/leftmost visible item)
|
|
40
|
+
* @param visibleCount - Number of items visible in viewport
|
|
41
|
+
* @param totalCount - Total number of items
|
|
42
|
+
* @param padding - Items to keep visible before/after cursor (default: 1)
|
|
43
|
+
* @returns New scroll offset
|
|
44
|
+
*/
|
|
45
|
+
export function calcEdgeBasedScrollOffset(
|
|
46
|
+
selectedIndex: number,
|
|
47
|
+
currentOffset: number,
|
|
48
|
+
visibleCount: number,
|
|
49
|
+
totalCount: number,
|
|
50
|
+
padding = 1,
|
|
51
|
+
): number {
|
|
52
|
+
// If everything fits, no scrolling needed
|
|
53
|
+
if (totalCount <= visibleCount) return 0
|
|
54
|
+
|
|
55
|
+
// Reduce padding when viewport is too small to have a non-empty safe zone.
|
|
56
|
+
// With padding=1 and visibleCount=2, paddedStart > paddedEnd (inverted zone),
|
|
57
|
+
// causing every re-render to trigger a scroll in one direction.
|
|
58
|
+
const effectivePadding = padding * 2 >= visibleCount ? 0 : padding
|
|
59
|
+
|
|
60
|
+
// Calculate visible range
|
|
61
|
+
const visibleStart = currentOffset
|
|
62
|
+
const visibleEnd = currentOffset + visibleCount - 1
|
|
63
|
+
|
|
64
|
+
// Define the "safe zone" where cursor doesn't trigger scroll
|
|
65
|
+
const paddedStart = visibleStart + effectivePadding
|
|
66
|
+
const paddedEnd = visibleEnd - effectivePadding
|
|
67
|
+
|
|
68
|
+
let newOffset = currentOffset
|
|
69
|
+
|
|
70
|
+
if (selectedIndex < paddedStart) {
|
|
71
|
+
// Scrolling UP/LEFT: place item `effectivePadding` rows from top
|
|
72
|
+
newOffset = Math.max(0, selectedIndex - effectivePadding)
|
|
73
|
+
} else if (
|
|
74
|
+
effectivePadding === 0 &&
|
|
75
|
+
selectedIndex === paddedStart &&
|
|
76
|
+
currentOffset > 0 &&
|
|
77
|
+
// Only scroll back if the viewport is large enough to show both the
|
|
78
|
+
// context item and the selected item. When visibleCount <= padding,
|
|
79
|
+
// scrolling back pushes the selected item out of view, which triggers
|
|
80
|
+
// a forward scroll on the next render → infinite oscillation.
|
|
81
|
+
visibleCount > padding
|
|
82
|
+
) {
|
|
83
|
+
// Small viewport (effectivePadding forced to 0): cursor at the very first visible
|
|
84
|
+
// position should still scroll back to provide context. Without this, scrolling
|
|
85
|
+
// right works (cursor past last visible triggers scroll) but scrolling left doesn't
|
|
86
|
+
// (cursor at first visible doesn't trigger), creating asymmetric behavior.
|
|
87
|
+
// Use original padding for the offset formula to show context before the cursor.
|
|
88
|
+
newOffset = Math.max(0, selectedIndex - padding)
|
|
89
|
+
} else if (selectedIndex > paddedEnd) {
|
|
90
|
+
// Scrolling DOWN/RIGHT: place item `effectivePadding` rows from bottom
|
|
91
|
+
// The +1 converts from 0-indexed offset to correct position
|
|
92
|
+
newOffset = Math.min(totalCount - visibleCount, selectedIndex - visibleCount + effectivePadding + 1)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Clamp to valid range
|
|
96
|
+
return Math.max(0, Math.min(newOffset, totalCount - visibleCount))
|
|
97
|
+
}
|
package/src/term-def.ts
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TermDef Resolution
|
|
3
|
+
*
|
|
4
|
+
* Converts TermDef (minimal render config) into resolved values for rendering.
|
|
5
|
+
* Handles auto-detection of events from stdin, dimension defaults, etc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ColorLevel, Term } from "./ansi/index"
|
|
9
|
+
import type { Event, TermDef } from "@silvery/tea/types"
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Resolved TermDef
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolved values from a TermDef, ready for use by the render system.
|
|
17
|
+
*/
|
|
18
|
+
export interface ResolvedTermDef {
|
|
19
|
+
/** Output stream (may be mock for static rendering) */
|
|
20
|
+
stdout: NodeJS.WriteStream | null
|
|
21
|
+
|
|
22
|
+
/** Width in columns */
|
|
23
|
+
width: number
|
|
24
|
+
|
|
25
|
+
/** Height in rows */
|
|
26
|
+
height: number
|
|
27
|
+
|
|
28
|
+
/** Color level (null = no colors) */
|
|
29
|
+
colors: ColorLevel | null
|
|
30
|
+
|
|
31
|
+
/** Event source (null = static mode) */
|
|
32
|
+
events: AsyncIterable<Event> | null
|
|
33
|
+
|
|
34
|
+
/** Whether this is static mode (no events = render until stable) */
|
|
35
|
+
isStatic: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Resolution Logic
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Default dimensions when not detectable.
|
|
44
|
+
*/
|
|
45
|
+
const DEFAULT_WIDTH = 80
|
|
46
|
+
const DEFAULT_HEIGHT = 24
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if a value is a Term instance (duck typing).
|
|
50
|
+
*/
|
|
51
|
+
export function isTerm(value: unknown): value is Term {
|
|
52
|
+
// Term can be a callable Proxy (typeof === 'function') or object
|
|
53
|
+
if (!value || (typeof value !== "object" && typeof value !== "function")) {
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
const obj = value as Record<string, unknown>
|
|
57
|
+
return (
|
|
58
|
+
typeof obj.hasCursor === "function" &&
|
|
59
|
+
typeof obj.hasInput === "function" &&
|
|
60
|
+
typeof obj.hasColor === "function" &&
|
|
61
|
+
typeof obj.write === "function"
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if a value is a TermDef (not a Term).
|
|
67
|
+
*/
|
|
68
|
+
export function isTermDef(value: unknown): value is TermDef {
|
|
69
|
+
if (!value || typeof value !== "object") return false
|
|
70
|
+
// TermDef doesn't have hasCursor method
|
|
71
|
+
const obj = value as Record<string, unknown>
|
|
72
|
+
return typeof obj.hasCursor !== "function"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolve a TermDef into concrete values.
|
|
77
|
+
*
|
|
78
|
+
* @param def - TermDef to resolve
|
|
79
|
+
* @returns Resolved values ready for rendering
|
|
80
|
+
*/
|
|
81
|
+
export function resolveTermDef(def: TermDef): ResolvedTermDef {
|
|
82
|
+
// Resolve dimensions
|
|
83
|
+
const width = def.width ?? def.stdout?.columns ?? DEFAULT_WIDTH
|
|
84
|
+
const height = def.height ?? def.stdout?.rows ?? DEFAULT_HEIGHT
|
|
85
|
+
|
|
86
|
+
// Resolve colors
|
|
87
|
+
let colors: ColorLevel | null = null
|
|
88
|
+
if (def.colors === true) {
|
|
89
|
+
// Auto-detect from stdout
|
|
90
|
+
colors = detectColorLevel(def.stdout)
|
|
91
|
+
} else if (def.colors === false || def.colors === null) {
|
|
92
|
+
colors = null
|
|
93
|
+
} else if (def.colors) {
|
|
94
|
+
colors = def.colors
|
|
95
|
+
} else {
|
|
96
|
+
// Default: auto-detect
|
|
97
|
+
colors = detectColorLevel(def.stdout)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Resolve events
|
|
101
|
+
let events: AsyncIterable<Event> | null = null
|
|
102
|
+
if (def.events) {
|
|
103
|
+
// Explicit events provided
|
|
104
|
+
events = def.events
|
|
105
|
+
} else if (def.stdin) {
|
|
106
|
+
// Auto-create events from stdin
|
|
107
|
+
events = createInputEvents(def.stdin)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
stdout: def.stdout ?? null,
|
|
112
|
+
width,
|
|
113
|
+
height,
|
|
114
|
+
colors,
|
|
115
|
+
events,
|
|
116
|
+
isStatic: events === null,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Resolve a Term instance into ResolvedTermDef.
|
|
122
|
+
*
|
|
123
|
+
* @param term - Term instance
|
|
124
|
+
* @returns Resolved values
|
|
125
|
+
*/
|
|
126
|
+
export function resolveFromTerm(term: Term): ResolvedTermDef {
|
|
127
|
+
return {
|
|
128
|
+
stdout: term.stdout,
|
|
129
|
+
width: term.cols ?? DEFAULT_WIDTH,
|
|
130
|
+
height: term.rows ?? DEFAULT_HEIGHT,
|
|
131
|
+
colors: term.hasColor(),
|
|
132
|
+
// Term instances always have interactive capabilities
|
|
133
|
+
events: createInputEvents(term.stdin),
|
|
134
|
+
isStatic: false,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ============================================================================
|
|
139
|
+
// Color Detection
|
|
140
|
+
// ============================================================================
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Detect color level from stdout stream.
|
|
144
|
+
*/
|
|
145
|
+
function detectColorLevel(stdout?: NodeJS.WriteStream): ColorLevel | null {
|
|
146
|
+
// Check environment variables
|
|
147
|
+
if (process.env.NO_COLOR !== undefined) {
|
|
148
|
+
return null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (process.env.FORCE_COLOR !== undefined) {
|
|
152
|
+
const level = Number.parseInt(process.env.FORCE_COLOR, 10)
|
|
153
|
+
if (level === 0) return null
|
|
154
|
+
if (level === 1) return "basic"
|
|
155
|
+
if (level === 2) return "256"
|
|
156
|
+
if (level >= 3) return "truecolor"
|
|
157
|
+
return "basic"
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check COLORTERM for truecolor
|
|
161
|
+
if (process.env.COLORTERM === "truecolor" || process.env.COLORTERM === "24bit") {
|
|
162
|
+
return "truecolor"
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Check if TTY
|
|
166
|
+
if (!stdout?.isTTY) {
|
|
167
|
+
return null
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Check TERM for 256 color support
|
|
171
|
+
const term = process.env.TERM ?? ""
|
|
172
|
+
if (term.includes("256color") || term.includes("256")) {
|
|
173
|
+
return "256"
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Default to basic if TTY
|
|
177
|
+
return "basic"
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ============================================================================
|
|
181
|
+
// Input Events
|
|
182
|
+
// ============================================================================
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Create an async iterable of input events from a stdin stream.
|
|
186
|
+
*
|
|
187
|
+
* This enables interactive mode by providing a source of keyboard events.
|
|
188
|
+
*/
|
|
189
|
+
export function createInputEvents(stdin: NodeJS.ReadStream): AsyncIterable<Event> {
|
|
190
|
+
return {
|
|
191
|
+
[Symbol.asyncIterator](): AsyncIterator<Event> {
|
|
192
|
+
const buffer: Event[] = []
|
|
193
|
+
let resolveNext: ((value: IteratorResult<Event>) => void) | null = null
|
|
194
|
+
let done = false
|
|
195
|
+
|
|
196
|
+
// Set up stdin reading
|
|
197
|
+
const handleData = (chunk: Buffer | string) => {
|
|
198
|
+
const data = typeof chunk === "string" ? chunk : chunk.toString("utf8")
|
|
199
|
+
|
|
200
|
+
// Convert raw input to key events
|
|
201
|
+
// This is simplified - real implementation would parse ANSI sequences
|
|
202
|
+
for (const char of data) {
|
|
203
|
+
const event: Event = {
|
|
204
|
+
type: "key",
|
|
205
|
+
key: char,
|
|
206
|
+
ctrl: char.charCodeAt(0) < 32 && char !== "\r" && char !== "\n" && char !== "\t",
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (resolveNext) {
|
|
210
|
+
resolveNext({ value: event, done: false })
|
|
211
|
+
resolveNext = null
|
|
212
|
+
} else {
|
|
213
|
+
buffer.push(event)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const handleEnd = () => {
|
|
219
|
+
done = true
|
|
220
|
+
if (resolveNext) {
|
|
221
|
+
resolveNext({ value: undefined as unknown as Event, done: true })
|
|
222
|
+
resolveNext = null
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Only set up if stdin supports raw mode
|
|
227
|
+
if (stdin.isTTY && typeof stdin.setRawMode === "function") {
|
|
228
|
+
stdin.setEncoding("utf8")
|
|
229
|
+
stdin.on("data", handleData)
|
|
230
|
+
stdin.on("end", handleEnd)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
next(): Promise<IteratorResult<Event>> {
|
|
235
|
+
// Return buffered event if available
|
|
236
|
+
const buffered = buffer.shift()
|
|
237
|
+
if (buffered) {
|
|
238
|
+
return Promise.resolve({ value: buffered, done: false })
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// If done, return done
|
|
242
|
+
if (done) {
|
|
243
|
+
return Promise.resolve({
|
|
244
|
+
value: undefined as unknown as Event,
|
|
245
|
+
done: true,
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Wait for next event
|
|
250
|
+
return new Promise((resolve) => {
|
|
251
|
+
resolveNext = resolve
|
|
252
|
+
})
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
return(): Promise<IteratorResult<Event>> {
|
|
256
|
+
done = true
|
|
257
|
+
stdin.off("data", handleData)
|
|
258
|
+
stdin.off("end", handleEnd)
|
|
259
|
+
return Promise.resolve({
|
|
260
|
+
value: undefined as unknown as Event,
|
|
261
|
+
done: true,
|
|
262
|
+
})
|
|
263
|
+
},
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
}
|
|
267
|
+
}
|