@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,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the Silvery render pipeline.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Cell } from "../buffer"
|
|
6
|
+
import type { Measurer } from "../unicode"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Context threaded through the render pipeline.
|
|
10
|
+
*
|
|
11
|
+
* Carries per-render resources that were previously accessed via module-level
|
|
12
|
+
* globals (e.g., `_scopedMeasurer` + `runWithMeasurer()`). Threading context
|
|
13
|
+
* explicitly eliminates save/restore patterns and makes the pipeline pure.
|
|
14
|
+
*
|
|
15
|
+
* Phase 1: measurer only.
|
|
16
|
+
* Phase 2: NodeRenderState for per-node params.
|
|
17
|
+
* Phase 3: instrumentation/diagnostics fields (optional — fall back to
|
|
18
|
+
* module-level globals when absent for backward compat).
|
|
19
|
+
*/
|
|
20
|
+
export interface PipelineContext {
|
|
21
|
+
readonly measurer: Measurer
|
|
22
|
+
// Phase 3: instrumentation (all optional for backward compat)
|
|
23
|
+
readonly instrumentEnabled?: boolean
|
|
24
|
+
readonly stats?: ContentPhaseStats
|
|
25
|
+
readonly nodeTrace?: NodeTraceEntry[]
|
|
26
|
+
readonly nodeTraceEnabled?: boolean
|
|
27
|
+
readonly bgConflictMode?: BgConflictMode
|
|
28
|
+
readonly warnedBgConflicts?: Set<string>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Background conflict detection mode.
|
|
33
|
+
* Set via SILVERY_BG_CONFLICT env var: 'ignore' | 'warn' | 'throw'
|
|
34
|
+
*/
|
|
35
|
+
export type BgConflictMode = "ignore" | "warn" | "throw"
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Per-node trace entry for SILVERY_STRICT diagnosis.
|
|
39
|
+
*/
|
|
40
|
+
export interface NodeTraceEntry {
|
|
41
|
+
id: string
|
|
42
|
+
type: string
|
|
43
|
+
depth: number
|
|
44
|
+
rect: string
|
|
45
|
+
prevLayout: string
|
|
46
|
+
hasPrev: boolean
|
|
47
|
+
ancestorCleared: boolean
|
|
48
|
+
flags: string
|
|
49
|
+
decision: string
|
|
50
|
+
layoutChanged: boolean
|
|
51
|
+
contentAreaAffected?: boolean
|
|
52
|
+
parentRegionCleared?: boolean
|
|
53
|
+
parentRegionChanged?: boolean
|
|
54
|
+
childHasPrev?: boolean
|
|
55
|
+
childAncestorCleared?: boolean
|
|
56
|
+
skipBgFill?: boolean
|
|
57
|
+
bgColor?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Mutable stats counters for content phase instrumentation.
|
|
62
|
+
* Reset after each contentPhase call.
|
|
63
|
+
*/
|
|
64
|
+
export interface ContentPhaseStats {
|
|
65
|
+
nodesVisited: number
|
|
66
|
+
nodesRendered: number
|
|
67
|
+
nodesSkipped: number
|
|
68
|
+
textNodes: number
|
|
69
|
+
boxNodes: number
|
|
70
|
+
clearOps: number
|
|
71
|
+
// Per-flag breakdown: why nodes weren't skipped
|
|
72
|
+
noPrevBuffer: number
|
|
73
|
+
flagContentDirty: number
|
|
74
|
+
flagPaintDirty: number
|
|
75
|
+
flagLayoutChanged: number
|
|
76
|
+
flagSubtreeDirty: number
|
|
77
|
+
flagChildrenDirty: number
|
|
78
|
+
flagChildPositionChanged: number
|
|
79
|
+
flagAncestorLayoutChanged: number
|
|
80
|
+
// Scroll container diagnostics
|
|
81
|
+
scrollContainerCount: number
|
|
82
|
+
scrollViewportCleared: number
|
|
83
|
+
scrollClearReason: string
|
|
84
|
+
// Normal container diagnostics
|
|
85
|
+
normalChildrenRepaint: number
|
|
86
|
+
normalRepaintReason: string
|
|
87
|
+
// Cascade diagnostics
|
|
88
|
+
cascadeMinDepth: number
|
|
89
|
+
cascadeNodes: string
|
|
90
|
+
// Top-level prevBuffer diagnostics
|
|
91
|
+
_prevBufferNull: number
|
|
92
|
+
_prevBufferDimMismatch: number
|
|
93
|
+
_hasPrevBuffer: number
|
|
94
|
+
_layoutW: number
|
|
95
|
+
_layoutH: number
|
|
96
|
+
_prevW: number
|
|
97
|
+
_prevH: number
|
|
98
|
+
_callCount: number
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Clip bounds for viewport clipping.
|
|
103
|
+
*/
|
|
104
|
+
export type ClipBounds = { top: number; bottom: number; left?: number; right?: number }
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Per-node render state that changes at each tree level.
|
|
108
|
+
*
|
|
109
|
+
* Groups the parameters that vary per-node during tree traversal:
|
|
110
|
+
* - scrollOffset: accumulated scroll offset from scroll containers
|
|
111
|
+
* - clipBounds: viewport clipping rectangle (from overflow containers)
|
|
112
|
+
* - hasPrevBuffer: whether the buffer was cloned from a previous frame
|
|
113
|
+
* - ancestorCleared: whether an ancestor already cleared this node's region
|
|
114
|
+
*
|
|
115
|
+
* Contrast with frame-scoped params (buffer, ctx) which stay the same
|
|
116
|
+
* for the entire render pass.
|
|
117
|
+
*/
|
|
118
|
+
export interface NodeRenderState {
|
|
119
|
+
scrollOffset: number
|
|
120
|
+
clipBounds?: ClipBounds
|
|
121
|
+
hasPrevBuffer: boolean
|
|
122
|
+
ancestorCleared: boolean
|
|
123
|
+
/** True when the buffer was cloned from prevBuffer (stale pixels exist).
|
|
124
|
+
* False when the buffer is a fresh TerminalBuffer (no stale pixels).
|
|
125
|
+
* Unlike hasPrevBuffer (which can be false per-node on a cloned buffer),
|
|
126
|
+
* this is constant for the entire render pass. Used to prevent clearExcessArea
|
|
127
|
+
* from writing inherited bg into a fresh buffer — no stale pixels to clear. */
|
|
128
|
+
bufferIsCloned: boolean
|
|
129
|
+
/** True when any ancestor had layoutChangedThisFrame = true.
|
|
130
|
+
* Propagated top-down to prevent descendants from being skipped when their
|
|
131
|
+
* own dirty flags are clean but their pixels are at wrong positions in the
|
|
132
|
+
* cloned buffer (because an ancestor moved/resized). Without this, the
|
|
133
|
+
* hasPrevBuffer cascade handles most cases, but this adds a direct safety
|
|
134
|
+
* net in the skip condition itself. */
|
|
135
|
+
ancestorLayoutChanged?: boolean
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Cell change for diffing.
|
|
140
|
+
*/
|
|
141
|
+
export interface CellChange {
|
|
142
|
+
x: number
|
|
143
|
+
y: number
|
|
144
|
+
cell: Cell
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Border character sets.
|
|
149
|
+
*/
|
|
150
|
+
export interface BorderChars {
|
|
151
|
+
topLeft: string
|
|
152
|
+
topRight: string
|
|
153
|
+
bottomLeft: string
|
|
154
|
+
bottomRight: string
|
|
155
|
+
horizontal: string
|
|
156
|
+
vertical: string
|
|
157
|
+
/** Bottom horizontal character. When absent, falls back to `horizontal`. */
|
|
158
|
+
bottomHorizontal?: string
|
|
159
|
+
/** Right vertical character. When absent, falls back to `vertical`. */
|
|
160
|
+
rightVertical?: string
|
|
161
|
+
}
|
package/src/pipeline.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Silvery Render Pipeline
|
|
3
|
+
*
|
|
4
|
+
* Re-exports from the pipeline/ directory for backwards compatibility.
|
|
5
|
+
* See pipeline/index.ts for the main implementation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
// Types
|
|
10
|
+
type CellChange,
|
|
11
|
+
type BorderChars,
|
|
12
|
+
type ExecuteRenderOptions,
|
|
13
|
+
type PipelineConfig,
|
|
14
|
+
// Phase functions
|
|
15
|
+
measurePhase,
|
|
16
|
+
layoutPhase,
|
|
17
|
+
rectEqual,
|
|
18
|
+
scrollPhase,
|
|
19
|
+
screenRectPhase,
|
|
20
|
+
contentPhase,
|
|
21
|
+
outputPhase,
|
|
22
|
+
// Utilities
|
|
23
|
+
clearBgConflictWarnings,
|
|
24
|
+
setBgConflictMode,
|
|
25
|
+
// Orchestration
|
|
26
|
+
executeRender,
|
|
27
|
+
executeRenderAdapter,
|
|
28
|
+
type PipelineContext,
|
|
29
|
+
} from "./pipeline/index"
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSI 14t/18t — Terminal Pixel and Text Area Size Queries
|
|
3
|
+
*
|
|
4
|
+
* Queries the terminal for window dimensions in pixels and characters.
|
|
5
|
+
*
|
|
6
|
+
* Protocols:
|
|
7
|
+
*
|
|
8
|
+
* Text Area Pixels (CSI 14t):
|
|
9
|
+
* Query: CSI 14 t
|
|
10
|
+
* Response: CSI 4 ; height ; width t
|
|
11
|
+
*
|
|
12
|
+
* Text Area Size in Characters (CSI 18t):
|
|
13
|
+
* Query: CSI 18 t
|
|
14
|
+
* Response: CSI 8 ; rows ; cols t
|
|
15
|
+
*
|
|
16
|
+
* Cell size can be derived by dividing pixel dimensions by character dimensions.
|
|
17
|
+
*
|
|
18
|
+
* Supported by: xterm, Ghostty, Kitty, WezTerm, foot, iTerm2
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Regex for CSI 4 ; height ; width t (pixel size response) */
|
|
22
|
+
const PIXEL_RESPONSE_RE = /\x1b\[4;(\d+);(\d+)t/
|
|
23
|
+
|
|
24
|
+
/** Regex for CSI 8 ; rows ; cols t (text area size response) */
|
|
25
|
+
const TEXT_AREA_RESPONSE_RE = /\x1b\[8;(\d+);(\d+)t/
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Pixel Size Query
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Query the terminal text area size in pixels.
|
|
33
|
+
*
|
|
34
|
+
* @param write Function to write to stdout
|
|
35
|
+
* @param read Function to read a chunk from stdin
|
|
36
|
+
* @param timeoutMs How long to wait for response (default: 200ms)
|
|
37
|
+
* @returns Width and height in pixels, or null on timeout/unsupported
|
|
38
|
+
*/
|
|
39
|
+
export async function queryTextAreaPixels(
|
|
40
|
+
write: (data: string) => void,
|
|
41
|
+
read: (timeoutMs: number) => Promise<string | null>,
|
|
42
|
+
timeoutMs = 200,
|
|
43
|
+
): Promise<{ width: number; height: number } | null> {
|
|
44
|
+
write("\x1b[14t")
|
|
45
|
+
|
|
46
|
+
const data = await read(timeoutMs)
|
|
47
|
+
if (data == null) return null
|
|
48
|
+
|
|
49
|
+
const match = PIXEL_RESPONSE_RE.exec(data)
|
|
50
|
+
if (!match) return null
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
height: parseInt(match[1]!, 10),
|
|
54
|
+
width: parseInt(match[2]!, 10),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// Text Area Size Query (characters)
|
|
60
|
+
// ============================================================================
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Query the terminal text area size in characters (rows x columns).
|
|
64
|
+
*
|
|
65
|
+
* @param write Function to write to stdout
|
|
66
|
+
* @param read Function to read a chunk from stdin
|
|
67
|
+
* @param timeoutMs How long to wait for response (default: 200ms)
|
|
68
|
+
* @returns Rows and columns, or null on timeout/unsupported
|
|
69
|
+
*/
|
|
70
|
+
export async function queryTextAreaSize(
|
|
71
|
+
write: (data: string) => void,
|
|
72
|
+
read: (timeoutMs: number) => Promise<string | null>,
|
|
73
|
+
timeoutMs = 200,
|
|
74
|
+
): Promise<{ cols: number; rows: number } | null> {
|
|
75
|
+
write("\x1b[18t")
|
|
76
|
+
|
|
77
|
+
const data = await read(timeoutMs)
|
|
78
|
+
if (data == null) return null
|
|
79
|
+
|
|
80
|
+
const match = TEXT_AREA_RESPONSE_RE.exec(data)
|
|
81
|
+
if (!match) return null
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
rows: parseInt(match[1]!, 10),
|
|
85
|
+
cols: parseInt(match[2]!, 10),
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ============================================================================
|
|
90
|
+
// Cell Size (derived)
|
|
91
|
+
// ============================================================================
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Query the terminal cell size in pixels by querying both pixel
|
|
95
|
+
* dimensions and character dimensions, then dividing.
|
|
96
|
+
*
|
|
97
|
+
* @param write Function to write to stdout
|
|
98
|
+
* @param read Function to read a chunk from stdin
|
|
99
|
+
* @param timeoutMs Per-query timeout (default: 200ms)
|
|
100
|
+
* @returns Cell width and height in pixels, or null if either query fails
|
|
101
|
+
*/
|
|
102
|
+
export async function queryCellSize(
|
|
103
|
+
write: (data: string) => void,
|
|
104
|
+
read: (timeoutMs: number) => Promise<string | null>,
|
|
105
|
+
timeoutMs = 200,
|
|
106
|
+
): Promise<{ width: number; height: number } | null> {
|
|
107
|
+
const pixels = await queryTextAreaPixels(write, read, timeoutMs)
|
|
108
|
+
if (pixels == null) return null
|
|
109
|
+
|
|
110
|
+
const size = await queryTextAreaSize(write, read, timeoutMs)
|
|
111
|
+
if (size == null) return null
|
|
112
|
+
|
|
113
|
+
if (size.cols === 0 || size.rows === 0) return null
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
width: pixels.width / size.cols,
|
|
117
|
+
height: pixels.height / size.rows,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render Adapter Abstraction
|
|
3
|
+
*
|
|
4
|
+
* This module defines the interfaces that allow silvery to render to different
|
|
5
|
+
* targets (terminal, canvas, etc.) while keeping the core layout and
|
|
6
|
+
* reconciliation logic portable.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Text Measurement
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
export interface TextMeasureStyle {
|
|
14
|
+
bold?: boolean
|
|
15
|
+
italic?: boolean
|
|
16
|
+
fontSize?: number
|
|
17
|
+
fontFamily?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TextMeasureResult {
|
|
21
|
+
width: number
|
|
22
|
+
height: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface TextMeasurer {
|
|
26
|
+
/**
|
|
27
|
+
* Measure text dimensions.
|
|
28
|
+
* Returns width in adapter units (cells for terminal, pixels for canvas).
|
|
29
|
+
*/
|
|
30
|
+
measureText(text: string, style?: TextMeasureStyle): TextMeasureResult
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the line height for the given style.
|
|
34
|
+
*/
|
|
35
|
+
getLineHeight(style?: TextMeasureStyle): number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Render Buffer
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
export interface RenderStyle {
|
|
43
|
+
fg?: string
|
|
44
|
+
bg?: string
|
|
45
|
+
attrs?: {
|
|
46
|
+
bold?: boolean
|
|
47
|
+
dim?: boolean
|
|
48
|
+
italic?: boolean
|
|
49
|
+
underline?: boolean
|
|
50
|
+
underlineStyle?: "single" | "double" | "curly" | "dotted" | "dashed"
|
|
51
|
+
underlineColor?: string
|
|
52
|
+
strikethrough?: boolean
|
|
53
|
+
inverse?: boolean
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface RenderBuffer {
|
|
58
|
+
readonly width: number
|
|
59
|
+
readonly height: number
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Fill a rectangle with a style.
|
|
63
|
+
*/
|
|
64
|
+
fillRect(x: number, y: number, width: number, height: number, style: RenderStyle): void
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Draw text at a position.
|
|
68
|
+
*/
|
|
69
|
+
drawText(x: number, y: number, text: string, style: RenderStyle): void
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Draw a single character at a position.
|
|
73
|
+
*/
|
|
74
|
+
drawChar(x: number, y: number, char: string, style: RenderStyle): void
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if coordinates are within bounds.
|
|
78
|
+
*/
|
|
79
|
+
inBounds(x: number, y: number): boolean
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// Border Characters
|
|
84
|
+
// ============================================================================
|
|
85
|
+
|
|
86
|
+
export interface BorderChars {
|
|
87
|
+
topLeft: string
|
|
88
|
+
topRight: string
|
|
89
|
+
bottomLeft: string
|
|
90
|
+
bottomRight: string
|
|
91
|
+
horizontal: string
|
|
92
|
+
vertical: string
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ============================================================================
|
|
96
|
+
// Render Adapter
|
|
97
|
+
// ============================================================================
|
|
98
|
+
|
|
99
|
+
export interface RenderAdapter {
|
|
100
|
+
/** Adapter name for debugging */
|
|
101
|
+
name: string
|
|
102
|
+
|
|
103
|
+
/** Text measurement for this adapter */
|
|
104
|
+
measurer: TextMeasurer
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Create a buffer for rendering.
|
|
108
|
+
*/
|
|
109
|
+
createBuffer(width: number, height: number): RenderBuffer
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Flush the buffer to the output (terminal, canvas, etc.).
|
|
113
|
+
* For terminal: returns ANSI diff string.
|
|
114
|
+
* For canvas: draws directly, returns void.
|
|
115
|
+
*/
|
|
116
|
+
flush(buffer: RenderBuffer, prevBuffer: RenderBuffer | null): string | void
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get border characters for the given style.
|
|
120
|
+
*/
|
|
121
|
+
getBorderChars(style: string): BorderChars
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ============================================================================
|
|
125
|
+
// Global Adapter Management
|
|
126
|
+
// ============================================================================
|
|
127
|
+
|
|
128
|
+
// NOTE: currentAdapter is intentionally a module-level singleton, not threaded
|
|
129
|
+
// through PipelineContext. Unlike the width measurer (which varies per terminal
|
|
130
|
+
// and needs per-render scoping), the render adapter defines the render *target*
|
|
131
|
+
// (terminal vs canvas) and is set exactly once at startup. It never changes for
|
|
132
|
+
// the lifetime of the process. Threading it through every pipeline function
|
|
133
|
+
// would add significant plumbing for zero benefit -- there's no concurrency
|
|
134
|
+
// or per-instance variation to protect against.
|
|
135
|
+
let currentAdapter: RenderAdapter | null = null
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Set the current render adapter.
|
|
139
|
+
*/
|
|
140
|
+
export function setRenderAdapter(adapter: RenderAdapter): void {
|
|
141
|
+
currentAdapter = adapter
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get the current render adapter.
|
|
146
|
+
* Throws if no adapter is set.
|
|
147
|
+
*/
|
|
148
|
+
export function getRenderAdapter(): RenderAdapter {
|
|
149
|
+
if (!currentAdapter) {
|
|
150
|
+
throw new Error("No render adapter set. Call setRenderAdapter() first.")
|
|
151
|
+
}
|
|
152
|
+
return currentAdapter
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Check if a render adapter has been set.
|
|
157
|
+
*/
|
|
158
|
+
export function hasRenderAdapter(): boolean {
|
|
159
|
+
return currentAdapter !== null
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get the text measurer from the current adapter.
|
|
164
|
+
*/
|
|
165
|
+
export function getTextMeasurer(): TextMeasurer {
|
|
166
|
+
return getRenderAdapter().measurer
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Ensure a render adapter is initialized.
|
|
171
|
+
* If no adapter is set, lazily imports and sets the terminal adapter.
|
|
172
|
+
*/
|
|
173
|
+
export async function ensureRenderAdapterInitialized(): Promise<void> {
|
|
174
|
+
if (hasRenderAdapter()) return
|
|
175
|
+
|
|
176
|
+
// Lazy import to avoid circular dependencies
|
|
177
|
+
const { terminalAdapter } = await import("./adapters/terminal-adapter.js")
|
|
178
|
+
setRenderAdapter(terminalAdapter)
|
|
179
|
+
}
|