@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.
Files changed (92) hide show
  1. package/package.json +54 -0
  2. package/src/adapters/canvas-adapter.ts +356 -0
  3. package/src/adapters/dom-adapter.ts +452 -0
  4. package/src/adapters/flexily-zero-adapter.ts +368 -0
  5. package/src/adapters/terminal-adapter.ts +305 -0
  6. package/src/adapters/yoga-adapter.ts +370 -0
  7. package/src/ansi/ansi.ts +251 -0
  8. package/src/ansi/constants.ts +76 -0
  9. package/src/ansi/detection.ts +441 -0
  10. package/src/ansi/hyperlink.ts +38 -0
  11. package/src/ansi/index.ts +201 -0
  12. package/src/ansi/patch-console.ts +159 -0
  13. package/src/ansi/sgr-codes.ts +34 -0
  14. package/src/ansi/storybook.ts +209 -0
  15. package/src/ansi/term.ts +724 -0
  16. package/src/ansi/types.ts +202 -0
  17. package/src/ansi/underline.ts +156 -0
  18. package/src/ansi/utils.ts +65 -0
  19. package/src/ansi-sanitize.ts +509 -0
  20. package/src/app.ts +571 -0
  21. package/src/bound-term.ts +94 -0
  22. package/src/bracketed-paste.ts +75 -0
  23. package/src/browser-renderer.ts +174 -0
  24. package/src/buffer.ts +1984 -0
  25. package/src/clipboard.ts +74 -0
  26. package/src/cursor-query.ts +85 -0
  27. package/src/device-attrs.ts +228 -0
  28. package/src/devtools.ts +123 -0
  29. package/src/dom/index.ts +194 -0
  30. package/src/errors.ts +39 -0
  31. package/src/focus-reporting.ts +48 -0
  32. package/src/hit-registry-core.ts +228 -0
  33. package/src/hit-registry.ts +176 -0
  34. package/src/index.ts +458 -0
  35. package/src/input.ts +119 -0
  36. package/src/inspector.ts +155 -0
  37. package/src/kitty-detect.ts +95 -0
  38. package/src/kitty-manager.ts +160 -0
  39. package/src/layout-engine.ts +296 -0
  40. package/src/layout.ts +26 -0
  41. package/src/measurer.ts +74 -0
  42. package/src/mode-query.ts +106 -0
  43. package/src/mouse-events.ts +419 -0
  44. package/src/mouse.ts +83 -0
  45. package/src/non-tty.ts +223 -0
  46. package/src/osc-markers.ts +32 -0
  47. package/src/osc-palette.ts +169 -0
  48. package/src/output.ts +406 -0
  49. package/src/pane-manager.ts +248 -0
  50. package/src/pipeline/CLAUDE.md +587 -0
  51. package/src/pipeline/content-phase-adapter.ts +976 -0
  52. package/src/pipeline/content-phase.ts +1765 -0
  53. package/src/pipeline/helpers.ts +42 -0
  54. package/src/pipeline/index.ts +416 -0
  55. package/src/pipeline/layout-phase.ts +686 -0
  56. package/src/pipeline/measure-phase.ts +198 -0
  57. package/src/pipeline/measure-stats.ts +21 -0
  58. package/src/pipeline/output-phase.ts +2593 -0
  59. package/src/pipeline/render-box.ts +343 -0
  60. package/src/pipeline/render-helpers.ts +243 -0
  61. package/src/pipeline/render-text.ts +1255 -0
  62. package/src/pipeline/types.ts +161 -0
  63. package/src/pipeline.ts +29 -0
  64. package/src/pixel-size.ts +119 -0
  65. package/src/render-adapter.ts +179 -0
  66. package/src/renderer.ts +1330 -0
  67. package/src/runtime/create-app.tsx +1845 -0
  68. package/src/runtime/create-buffer.ts +18 -0
  69. package/src/runtime/create-runtime.ts +325 -0
  70. package/src/runtime/diff.ts +56 -0
  71. package/src/runtime/event-handlers.ts +254 -0
  72. package/src/runtime/index.ts +119 -0
  73. package/src/runtime/keys.ts +8 -0
  74. package/src/runtime/layout.ts +164 -0
  75. package/src/runtime/run.tsx +318 -0
  76. package/src/runtime/term-provider.ts +399 -0
  77. package/src/runtime/terminal-lifecycle.ts +246 -0
  78. package/src/runtime/tick.ts +219 -0
  79. package/src/runtime/types.ts +210 -0
  80. package/src/scheduler.ts +723 -0
  81. package/src/screenshot.ts +57 -0
  82. package/src/scroll-region.ts +69 -0
  83. package/src/scroll-utils.ts +97 -0
  84. package/src/term-def.ts +267 -0
  85. package/src/terminal-caps.ts +5 -0
  86. package/src/terminal-colors.ts +216 -0
  87. package/src/termtest.ts +224 -0
  88. package/src/text-sizing.ts +109 -0
  89. package/src/toolbelt/index.ts +72 -0
  90. package/src/unicode.ts +1763 -0
  91. package/src/xterm/index.ts +491 -0
  92. package/src/xterm/xterm-provider.ts +204 -0
package/src/errors.ts ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Error types for silvery terminal rendering.
3
+ *
4
+ * Separated from scheduler.ts to allow React-free barrel imports.
5
+ * Keep this file React-free — only plain types allowed.
6
+ */
7
+
8
+ import type { ContentPhaseStats } from "./pipeline/types"
9
+
10
+ /** Structured mismatch data attached to the error (mirrors MismatchDebugContext shape) */
11
+ export interface MismatchErrorData {
12
+ /** Content-phase instrumentation snapshot (nodes visited/rendered/skipped, per-flag breakdown) */
13
+ contentPhaseStats?: ContentPhaseStats
14
+ /** Debug context for the mismatched cell (from debug-mismatch.ts) */
15
+ mismatchContext?: unknown
16
+ }
17
+
18
+ /**
19
+ * Error thrown when SILVERY_CHECK_INCREMENTAL detects a mismatch.
20
+ * This error should NOT be caught by general error handlers - it indicates
21
+ * a bug in incremental rendering that needs to be fixed.
22
+ *
23
+ * When SILVERY_STRICT fires, the error automatically includes:
24
+ * - Content-phase instrumentation (nodes visited/rendered/skipped, per-flag breakdown)
25
+ * - Cell attribution (which node owns the mismatched cell, dirty flags, scroll context)
26
+ */
27
+ export class IncrementalRenderMismatchError extends Error {
28
+ /** Content-phase instrumentation snapshot */
29
+ contentPhaseStats?: ContentPhaseStats
30
+ /** Debug context for the mismatched cell */
31
+ mismatchContext?: unknown
32
+
33
+ constructor(message: string, data?: MismatchErrorData) {
34
+ super(message)
35
+ this.name = "IncrementalRenderMismatchError"
36
+ this.contentPhaseStats = data?.contentPhaseStats
37
+ this.mismatchContext = data?.mismatchContext
38
+ }
39
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Focus Reporting (CSI ?1004h)
3
+ *
4
+ * Enables/disables terminal focus-in/focus-out event reporting.
5
+ * When enabled, the terminal sends CSI I on focus-in and CSI O on focus-out.
6
+ *
7
+ * Protocol:
8
+ * - Enable: CSI ? 1004 h
9
+ * - Disable: CSI ? 1004 l
10
+ * - Focus In: CSI I (\x1b[I)
11
+ * - Focus Out: CSI O (\x1b[O)
12
+ *
13
+ * Supported by: xterm (v282+), Ghostty, Kitty, WezTerm, iTerm2, foot, VTE
14
+ */
15
+
16
+ const CSI = "\x1b["
17
+
18
+ /**
19
+ * Enable terminal focus reporting.
20
+ * After enabling, the terminal will send CSI I / CSI O sequences
21
+ * when the terminal window gains or loses focus.
22
+ */
23
+ export function enableFocusReporting(write: (data: string) => void): void {
24
+ write(`${CSI}?1004h`)
25
+ }
26
+
27
+ /**
28
+ * Disable terminal focus reporting.
29
+ */
30
+ export function disableFocusReporting(write: (data: string) => void): void {
31
+ write(`${CSI}?1004l`)
32
+ }
33
+
34
+ /**
35
+ * Parse a focus event from terminal input.
36
+ *
37
+ * @param input Raw terminal input string
38
+ * @returns Parsed focus event, or null if not a focus sequence
39
+ */
40
+ export function parseFocusEvent(input: string): { type: "focus-in" | "focus-out" } | null {
41
+ if (input.includes(`${CSI}I`)) {
42
+ return { type: "focus-in" }
43
+ }
44
+ if (input.includes(`${CSI}O`)) {
45
+ return { type: "focus-out" }
46
+ }
47
+ return null
48
+ }
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Hit Registry Core — Pure logic for mouse hit testing.
3
+ *
4
+ * This module contains the React-free core of the hit registry:
5
+ * types, registry class, z-index constants, and ID counter.
6
+ *
7
+ * React hooks and context live in ./hit-registry (which re-exports everything
8
+ * from here plus adds useHitRegion, useHitRegionCallback, HitRegistryContext).
9
+ *
10
+ * The @silvery/term barrel imports from this file to stay React-free.
11
+ * Consumers who need React hooks should import from @silvery/term/hit-registry.
12
+ */
13
+
14
+ // ============================================================================
15
+ // Types
16
+ // ============================================================================
17
+
18
+ /**
19
+ * Target type for hit testing.
20
+ * Each type represents a different clickable element in the UI.
21
+ */
22
+ export interface HitTarget {
23
+ /** The type of element that was clicked */
24
+ type: "node" | "fold-toggle" | "link" | "column-header" | "scroll-area" | "button"
25
+ /** Column index (for column-header, or items within a column) */
26
+ colIndex?: number
27
+ /** Card index within a column */
28
+ cardIndex?: number
29
+ /** Sub-item index within a card (e.g., checklist items) */
30
+ subIndex?: number
31
+ /** Node ID for node-specific targets */
32
+ nodeId?: string
33
+ /** URL for link targets */
34
+ linkUrl?: string
35
+ /** Custom action identifier */
36
+ action?: string
37
+ }
38
+
39
+ /**
40
+ * A registered hit region with position, size, target, and z-index.
41
+ */
42
+ export interface HitRegion {
43
+ /** X position on screen (0-indexed column) */
44
+ x: number
45
+ /** Y position on screen (0-indexed row) */
46
+ y: number
47
+ /** Width in columns */
48
+ width: number
49
+ /** Height in rows */
50
+ height: number
51
+ /** The target to return when this region is clicked */
52
+ target: HitTarget
53
+ /** Z-index for layering (higher values are on top) */
54
+ zIndex: number
55
+ }
56
+
57
+ // ============================================================================
58
+ // HitRegistry Class
59
+ // ============================================================================
60
+
61
+ /**
62
+ * Registry for managing hit regions.
63
+ *
64
+ * Components register their screen regions with targets, and the registry
65
+ * resolves mouse clicks to the appropriate target based on position and z-index.
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * const registry = new HitRegistry();
70
+ *
71
+ * // Register a card region
72
+ * registry.register('card-1', {
73
+ * x: 10, y: 5, width: 30, height: 8,
74
+ * target: { type: 'node', nodeId: 'abc123' },
75
+ * zIndex: 10
76
+ * });
77
+ *
78
+ * // Hit test a click
79
+ * const target = registry.hitTest(15, 7);
80
+ * // Returns { type: 'node', nodeId: 'abc123' }
81
+ * ```
82
+ */
83
+ export class HitRegistry {
84
+ private regions = new Map<string, HitRegion>()
85
+
86
+ /**
87
+ * Register a hit region with a unique ID.
88
+ *
89
+ * @param id - Unique identifier for the region (used for unregistration)
90
+ * @param region - The region definition including position, size, target, and z-index
91
+ */
92
+ register(id: string, region: HitRegion): void {
93
+ this.regions.set(id, region)
94
+ }
95
+
96
+ /**
97
+ * Unregister a hit region by ID.
98
+ *
99
+ * @param id - The ID used when registering the region
100
+ */
101
+ unregister(id: string): void {
102
+ this.regions.delete(id)
103
+ }
104
+
105
+ /**
106
+ * Clear all registered regions.
107
+ * Useful when the UI is completely redrawn.
108
+ */
109
+ clear(): void {
110
+ this.regions.clear()
111
+ }
112
+
113
+ /**
114
+ * Get the number of registered regions.
115
+ * Useful for debugging.
116
+ */
117
+ get size(): number {
118
+ return this.regions.size
119
+ }
120
+
121
+ /**
122
+ * Test a screen position and return the highest z-index matching target.
123
+ *
124
+ * @param screenX - X position on screen (0-indexed column)
125
+ * @param screenY - Y position on screen (0-indexed row)
126
+ * @returns The target of the highest z-index region containing the point, or null if none
127
+ */
128
+ hitTest(screenX: number, screenY: number): HitTarget | null {
129
+ let bestMatch: HitRegion | null = null
130
+
131
+ for (const region of this.regions.values()) {
132
+ // Check if point is within region bounds
133
+ if (
134
+ screenX >= region.x &&
135
+ screenX < region.x + region.width &&
136
+ screenY >= region.y &&
137
+ screenY < region.y + region.height
138
+ ) {
139
+ // Keep the highest z-index match
140
+ if (!bestMatch || region.zIndex > bestMatch.zIndex) {
141
+ bestMatch = region
142
+ }
143
+ }
144
+ }
145
+
146
+ return bestMatch?.target ?? null
147
+ }
148
+
149
+ /**
150
+ * Get all regions that contain a point, sorted by z-index (highest first).
151
+ * Useful for debugging or when you need to know all overlapping elements.
152
+ *
153
+ * @param screenX - X position on screen (0-indexed column)
154
+ * @param screenY - Y position on screen (0-indexed row)
155
+ * @returns Array of matching regions, sorted by z-index descending
156
+ */
157
+ hitTestAll(screenX: number, screenY: number): HitRegion[] {
158
+ const matches: HitRegion[] = []
159
+
160
+ for (const region of this.regions.values()) {
161
+ if (
162
+ screenX >= region.x &&
163
+ screenX < region.x + region.width &&
164
+ screenY >= region.y &&
165
+ screenY < region.y + region.height
166
+ ) {
167
+ matches.push(region)
168
+ }
169
+ }
170
+
171
+ // Sort by z-index descending (highest first)
172
+ return matches.sort((a, b) => b.zIndex - a.zIndex)
173
+ }
174
+
175
+ /**
176
+ * Debug helper: get all registered regions.
177
+ */
178
+ getAllRegions(): Map<string, HitRegion> {
179
+ return new Map(this.regions)
180
+ }
181
+ }
182
+
183
+ // ============================================================================
184
+ // ID Counter
185
+ // ============================================================================
186
+
187
+ /**
188
+ * Generate a unique ID for hit region registration.
189
+ */
190
+ let hitRegionIdCounter = 0
191
+ export function generateHitRegionId(): string {
192
+ return `hit-${++hitRegionIdCounter}`
193
+ }
194
+
195
+ /**
196
+ * Reset the ID counter (useful for testing).
197
+ */
198
+ export function resetHitRegionIdCounter(): void {
199
+ hitRegionIdCounter = 0
200
+ }
201
+
202
+ // ============================================================================
203
+ // Z-Index Constants
204
+ // ============================================================================
205
+
206
+ /**
207
+ * Recommended z-index values for different UI layers.
208
+ */
209
+ export const Z_INDEX = {
210
+ /** Background elements */
211
+ BACKGROUND: 0,
212
+ /** Column headers */
213
+ COLUMN_HEADER: 5,
214
+ /** Cards in the main view */
215
+ CARD: 10,
216
+ /** Fold toggles (above cards for easier clicking) */
217
+ FOLD_TOGGLE: 15,
218
+ /** Links within cards */
219
+ LINK: 20,
220
+ /** Floating elements */
221
+ FLOATING: 50,
222
+ /** Modal dialogs */
223
+ DIALOG: 100,
224
+ /** Dropdown menus */
225
+ DROPDOWN: 150,
226
+ /** Tooltips */
227
+ TOOLTIP: 200,
228
+ } as const
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Hit Registry for Mouse Input
3
+ *
4
+ * Provides a registry for tracking clickable regions in the terminal UI.
5
+ * Components register their screen positions, and mouse clicks are resolved
6
+ * to the appropriate target based on position and z-index.
7
+ *
8
+ * The registry uses z-index ordering to handle overlapping elements:
9
+ * - Dialogs (z-index 100+) take priority over cards
10
+ * - Cards (z-index 10) take priority over background
11
+ * - Background elements (z-index 0) are lowest priority
12
+ *
13
+ * This module re-exports the pure core (types, class, constants) and adds
14
+ * React hooks/context. Import from @silvery/term/hit-registry-core for the
15
+ * React-free subset.
16
+ */
17
+
18
+ import { createContext, useContext, useEffect, useRef } from "react"
19
+ import type { Rect } from "@silvery/tea/types"
20
+
21
+ // Re-export everything from core
22
+ export { HitRegistry, generateHitRegionId, resetHitRegionIdCounter, Z_INDEX } from "./hit-registry-core"
23
+ export type { HitTarget, HitRegion } from "./hit-registry-core"
24
+
25
+ // Import for local use
26
+ import { type HitTarget, type HitRegion, HitRegistry, generateHitRegionId } from "./hit-registry-core"
27
+
28
+ // ============================================================================
29
+ // React Context
30
+ // ============================================================================
31
+
32
+ /**
33
+ * Context for accessing the HitRegistry.
34
+ * Components use this to register their hit regions.
35
+ */
36
+ export const HitRegistryContext = createContext<HitRegistry | null>(null)
37
+
38
+ // ============================================================================
39
+ // Hooks
40
+ // ============================================================================
41
+
42
+ /**
43
+ * Hook to get the HitRegistry from context.
44
+ *
45
+ * @returns The HitRegistry instance, or null if not in a HitRegistryContext
46
+ */
47
+ export function useHitRegistry(): HitRegistry | null {
48
+ return useContext(HitRegistryContext)
49
+ }
50
+
51
+ /**
52
+ * Hook to register a hit region based on component's screen position.
53
+ *
54
+ * Automatically registers on mount and when position changes,
55
+ * and unregisters on unmount.
56
+ *
57
+ * @param target - The target to return when this region is clicked
58
+ * @param rect - The screen rectangle (from useScreenRect or similar)
59
+ * @param zIndex - Z-index for layering (default: 0)
60
+ * @param enabled - Whether the region is active (default: true)
61
+ *
62
+ * @example
63
+ * ```tsx
64
+ * function Card({ nodeId }: { nodeId: string }) {
65
+ * const rect = useScreenRect();
66
+ *
67
+ * useHitRegion(
68
+ * { type: 'node', nodeId },
69
+ * rect,
70
+ * 10 // z-index for cards
71
+ * );
72
+ *
73
+ * return <Box>...</Box>;
74
+ * }
75
+ * ```
76
+ */
77
+ export function useHitRegion(target: HitTarget, rect: Rect | null, zIndex = 0, enabled = true): void {
78
+ const registry = useContext(HitRegistryContext)
79
+ const idRef = useRef<string | null>(null)
80
+
81
+ // Generate stable ID on first use
82
+ if (idRef.current === null) {
83
+ idRef.current = generateHitRegionId()
84
+ }
85
+
86
+ useEffect(() => {
87
+ if (!registry || !rect || !enabled) {
88
+ // Clean up if disabled or no registry
89
+ if (idRef.current && registry) {
90
+ registry.unregister(idRef.current)
91
+ }
92
+ return
93
+ }
94
+
95
+ const id = idRef.current!
96
+
97
+ // Register the region
98
+ registry.register(id, {
99
+ x: rect.x,
100
+ y: rect.y,
101
+ width: rect.width,
102
+ height: rect.height,
103
+ target,
104
+ zIndex,
105
+ })
106
+
107
+ // Cleanup on unmount or when dependencies change
108
+ return () => {
109
+ registry.unregister(id)
110
+ }
111
+ }, [registry, rect?.x, rect?.y, rect?.width, rect?.height, target, zIndex, enabled])
112
+ }
113
+
114
+ /**
115
+ * Hook to register a hit region using a callback for screen position.
116
+ *
117
+ * Similar to useHitRegion but works with useScreenRectCallback for
118
+ * better performance in large lists (avoids re-renders).
119
+ *
120
+ * @param target - The target to return when this region is clicked
121
+ * @param zIndex - Z-index for layering (default: 0)
122
+ * @param enabled - Whether the region is active (default: true)
123
+ * @returns A callback to pass to useScreenRectCallback
124
+ *
125
+ * @example
126
+ * ```tsx
127
+ * function Card({ nodeId }: { nodeId: string }) {
128
+ * const onLayout = useHitRegionCallback(
129
+ * { type: 'node', nodeId },
130
+ * 10 // z-index
131
+ * );
132
+ *
133
+ * useScreenRectCallback(onLayout);
134
+ *
135
+ * return <Box>...</Box>;
136
+ * }
137
+ * ```
138
+ */
139
+ export function useHitRegionCallback(target: HitTarget, zIndex = 0, enabled = true): (rect: Rect) => void {
140
+ const registry = useContext(HitRegistryContext)
141
+ const idRef = useRef<string | null>(null)
142
+
143
+ // Generate stable ID on first use
144
+ if (idRef.current === null) {
145
+ idRef.current = generateHitRegionId()
146
+ }
147
+
148
+ // Cleanup on unmount
149
+ useEffect(() => {
150
+ const id = idRef.current
151
+ return () => {
152
+ if (id && registry) {
153
+ registry.unregister(id)
154
+ }
155
+ }
156
+ }, [registry])
157
+
158
+ // Return callback that updates the region
159
+ return (rect: Rect) => {
160
+ if (!registry || !enabled) {
161
+ if (idRef.current && registry) {
162
+ registry.unregister(idRef.current)
163
+ }
164
+ return
165
+ }
166
+
167
+ registry.register(idRef.current!, {
168
+ x: rect.x,
169
+ y: rect.y,
170
+ width: rect.width,
171
+ height: rect.height,
172
+ target,
173
+ zIndex,
174
+ })
175
+ }
176
+ }