@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
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Time/tick source for silvery-loop.
3
+ *
4
+ * Creates an AsyncIterable that yields at regular intervals.
5
+ * Used for animations, spinners, progress bars, etc.
6
+ */
7
+
8
+ /**
9
+ * Create a tick source that yields at regular intervals.
10
+ *
11
+ * The tick source respects AbortSignal for cleanup and stops
12
+ * when the signal is aborted.
13
+ *
14
+ * @param intervalMs Interval between ticks in milliseconds
15
+ * @param signal Optional AbortSignal to stop the tick source
16
+ * @returns AsyncIterable that yields tick numbers (0, 1, 2, ...)
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * const controller = new AbortController()
21
+ * const ticks = createTick(100, controller.signal)
22
+ *
23
+ * for await (const tick of ticks) {
24
+ * console.log(`Tick ${tick}`)
25
+ * if (tick >= 10) controller.abort()
26
+ * }
27
+ * ```
28
+ */
29
+ export function createTick(intervalMs: number, signal?: AbortSignal): AsyncIterable<number> {
30
+ return {
31
+ [Symbol.asyncIterator]: () => createTickIterator(intervalMs, signal),
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Create the actual tick iterator.
37
+ */
38
+ function createTickIterator(intervalMs: number, signal?: AbortSignal): AsyncIterator<number> {
39
+ let count = 0
40
+ let timer: ReturnType<typeof setTimeout> | undefined
41
+ let pendingResolve: ((result: IteratorResult<number>) => void) | undefined
42
+ let done = false
43
+
44
+ // Handle abort signal
45
+ const onAbort = () => {
46
+ done = true
47
+ if (timer) {
48
+ clearTimeout(timer)
49
+ timer = undefined
50
+ }
51
+ if (pendingResolve) {
52
+ pendingResolve({ done: true, value: undefined })
53
+ pendingResolve = undefined
54
+ }
55
+ }
56
+
57
+ if (signal) {
58
+ if (signal.aborted) {
59
+ done = true
60
+ } else {
61
+ signal.addEventListener("abort", onAbort, { once: true })
62
+ }
63
+ }
64
+
65
+ return {
66
+ async next(): Promise<IteratorResult<number>> {
67
+ if (done) {
68
+ return { done: true, value: undefined }
69
+ }
70
+
71
+ return new Promise<IteratorResult<number>>((resolve, _reject) => {
72
+ pendingResolve = resolve
73
+
74
+ timer = setTimeout(() => {
75
+ if (!done) {
76
+ const value = count++
77
+ pendingResolve = undefined
78
+ resolve({ done: false, value })
79
+ }
80
+ }, intervalMs)
81
+ })
82
+ },
83
+
84
+ async return(): Promise<IteratorResult<number>> {
85
+ done = true
86
+ if (timer) {
87
+ clearTimeout(timer)
88
+ timer = undefined
89
+ }
90
+ if (signal) {
91
+ signal.removeEventListener("abort", onAbort)
92
+ }
93
+ if (pendingResolve) {
94
+ pendingResolve({ done: true, value: undefined })
95
+ pendingResolve = undefined
96
+ }
97
+ return { done: true, value: undefined }
98
+ },
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Create a tick source that yields at approximately 60fps (~16ms).
104
+ *
105
+ * @param signal Optional AbortSignal to stop the tick source
106
+ * @returns AsyncIterable that yields frame numbers
107
+ */
108
+ export function createFrameTick(signal?: AbortSignal): AsyncIterable<number> {
109
+ return createTick(16, signal)
110
+ }
111
+
112
+ /**
113
+ * Create a tick source that yields once per second.
114
+ *
115
+ * @param signal Optional AbortSignal to stop the tick source
116
+ * @returns AsyncIterable that yields second counts
117
+ */
118
+ export function createSecondTick(signal?: AbortSignal): AsyncIterable<number> {
119
+ return createTick(1000, signal)
120
+ }
121
+
122
+ /**
123
+ * Create a tick source with adaptive timing based on render performance.
124
+ *
125
+ * This is useful for maintaining a target frame rate while allowing
126
+ * for slower frames when needed.
127
+ *
128
+ * @param targetFps Target frames per second (default: 60)
129
+ * @param signal Optional AbortSignal to stop the tick source
130
+ * @returns AsyncIterable with timing information
131
+ */
132
+ export function createAdaptiveTick(
133
+ targetFps = 60,
134
+ signal?: AbortSignal,
135
+ ): AsyncIterable<{ tick: number; elapsed: number; delta: number }> {
136
+ const targetMs = 1000 / targetFps
137
+ let lastTime = Date.now()
138
+ let tick = 0
139
+
140
+ return {
141
+ [Symbol.asyncIterator]: () => {
142
+ let done = false
143
+ let timer: ReturnType<typeof setTimeout> | undefined
144
+ let pendingResolve:
145
+ | ((
146
+ result: IteratorResult<{
147
+ tick: number
148
+ elapsed: number
149
+ delta: number
150
+ }>,
151
+ ) => void)
152
+ | undefined
153
+
154
+ const onAbort = () => {
155
+ done = true
156
+ if (timer) {
157
+ clearTimeout(timer)
158
+ timer = undefined
159
+ }
160
+ if (pendingResolve) {
161
+ pendingResolve({ done: true, value: undefined })
162
+ pendingResolve = undefined
163
+ }
164
+ }
165
+
166
+ if (signal) {
167
+ if (signal.aborted) {
168
+ done = true
169
+ } else {
170
+ signal.addEventListener("abort", onAbort, { once: true })
171
+ }
172
+ }
173
+
174
+ return {
175
+ async next(): Promise<IteratorResult<{ tick: number; elapsed: number; delta: number }>> {
176
+ if (done) {
177
+ return { done: true, value: undefined }
178
+ }
179
+
180
+ return new Promise((resolve) => {
181
+ pendingResolve = resolve
182
+ const now = Date.now()
183
+ const elapsed = now - lastTime
184
+ const delay = Math.max(0, targetMs - elapsed)
185
+
186
+ timer = setTimeout(() => {
187
+ if (!done) {
188
+ const currentTime = Date.now()
189
+ const delta = currentTime - lastTime
190
+ lastTime = currentTime
191
+ pendingResolve = undefined
192
+ resolve({
193
+ done: false,
194
+ value: { tick: tick++, elapsed: currentTime, delta },
195
+ })
196
+ }
197
+ }, delay)
198
+ })
199
+ },
200
+
201
+ async return(): Promise<IteratorResult<{ tick: number; elapsed: number; delta: number }>> {
202
+ done = true
203
+ if (timer) {
204
+ clearTimeout(timer)
205
+ timer = undefined
206
+ }
207
+ if (signal) {
208
+ signal.removeEventListener("abort", onAbort)
209
+ }
210
+ if (pendingResolve) {
211
+ pendingResolve({ done: true, value: undefined })
212
+ pendingResolve = undefined
213
+ }
214
+ return { done: true, value: undefined }
215
+ },
216
+ }
217
+ },
218
+ }
219
+ }
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Core types for the silvery-loop runtime.
3
+ */
4
+
5
+ import type { TerminalBuffer } from "../buffer"
6
+ import type { TeaNode } from "@silvery/tea/types"
7
+
8
+ /**
9
+ * Dimensions for rendering.
10
+ */
11
+ export interface Dims {
12
+ cols: number
13
+ rows: number
14
+ }
15
+
16
+ /**
17
+ * Immutable render output buffer.
18
+ *
19
+ * Contains:
20
+ * - text: Plain text without ANSI codes (for assertions)
21
+ * - ansi: Styled output with ANSI escape codes
22
+ * - nodes: Internal node tree for locator queries
23
+ */
24
+ export interface Buffer {
25
+ /** Plain text without ANSI codes */
26
+ readonly text: string
27
+ /** Styled output with ANSI escape codes */
28
+ readonly ansi: string
29
+ /** Internal node tree for locator queries */
30
+ readonly nodes: TeaNode
31
+ /** Raw terminal buffer for diffing */
32
+ readonly _buffer: TerminalBuffer
33
+ }
34
+
35
+ /**
36
+ * Event types from the runtime.
37
+ */
38
+ export type Event =
39
+ | {
40
+ type: "key"
41
+ key: string
42
+ ctrl?: boolean
43
+ meta?: boolean
44
+ shift?: boolean
45
+ }
46
+ | {
47
+ type: "mouse"
48
+ button: number
49
+ x: number
50
+ y: number
51
+ action: "down" | "up" | "move" | "wheel"
52
+ delta?: number
53
+ shift: boolean
54
+ meta: boolean
55
+ ctrl: boolean
56
+ }
57
+ | { type: "paste"; content: string }
58
+ | { type: "resize"; cols: number; rows: number }
59
+ | { type: "tick"; time: number }
60
+ | { type: "effect"; id: string; result: unknown }
61
+ | { type: "error"; error: Error }
62
+
63
+ /**
64
+ * Render target interface - abstracts terminal output.
65
+ */
66
+ export interface RenderTarget {
67
+ /** Write rendered frame to output */
68
+ write(frame: string): void
69
+ /** Get current dimensions */
70
+ getDims(): Dims
71
+ /** Subscribe to resize events */
72
+ onResize?(handler: (dims: Dims) => void): () => void
73
+ }
74
+
75
+ /**
76
+ * Runtime options for createRuntime().
77
+ */
78
+ export interface RuntimeOptions {
79
+ /** Render target (terminal, test mock, etc.) */
80
+ target: RenderTarget
81
+ /** Abort signal for cleanup */
82
+ signal?: AbortSignal
83
+ /** Render mode: fullscreen (alt screen) or inline (scrollback-compatible) */
84
+ mode?: "fullscreen" | "inline"
85
+ /** Scoped output phase function (from createOutputPhase/createPipeline). When provided,
86
+ * runtime.render() uses this instead of the raw outputPhase — ensures measurer/caps are threaded. */
87
+ outputPhaseFn?: (
88
+ prev: import("../buffer.js").TerminalBuffer | null,
89
+ next: import("../buffer.js").TerminalBuffer,
90
+ mode?: "fullscreen" | "inline",
91
+ scrollbackOffset?: number,
92
+ termRows?: number,
93
+ ) => string
94
+ }
95
+
96
+ /**
97
+ * The runtime kernel interface.
98
+ */
99
+ export interface Runtime {
100
+ /** Event stream - yields until disposed */
101
+ events(): AsyncIterable<Event>
102
+
103
+ /** Schedule an effect with optional cancellation */
104
+ schedule<T>(effect: () => Promise<T>, opts?: { signal?: AbortSignal }): void
105
+
106
+ /** Render a buffer to the target */
107
+ render(buffer: Buffer): void
108
+
109
+ /** Report lines written to stdout between renders (inline mode only) */
110
+ addScrollbackLines(lines: number): void
111
+
112
+ /** Reset diff state so next render outputs a full frame */
113
+ invalidate(): void
114
+
115
+ /** Reset inline cursor tracking state (inline mode only).
116
+ * Called by useScrollback before re-emitting frozen items on resize. */
117
+ resetInlineCursor(): void
118
+
119
+ /** Get inline cursor row relative to render region start. -1 if unknown. */
120
+ getInlineCursorRow(): number
121
+
122
+ /** Promote frozen content to scrollback via the output phase.
123
+ * Content is written in a single frame with the live render — no flicker. */
124
+ promoteScrollback(content: string, lines: number): void
125
+
126
+ /** Get current dimensions */
127
+ getDims(): Dims
128
+
129
+ /** Dispose and cleanup - idempotent */
130
+ [Symbol.dispose](): void
131
+ }
132
+
133
+ // ============================================================================
134
+ // Provider Types
135
+ // ============================================================================
136
+
137
+ /**
138
+ * Event emitted by a provider, tagged with event type.
139
+ */
140
+ export type ProviderEvent<Events extends Record<string, unknown>> = {
141
+ [K in keyof Events]: { type: K; data: Events[K] }
142
+ }[keyof Events]
143
+
144
+ /**
145
+ * Provider interface - unified store + event source.
146
+ *
147
+ * Providers are the building blocks of silvery-loop applications.
148
+ * They encapsulate:
149
+ * - State (Zustand-compatible: getState/subscribe)
150
+ * - Events (AsyncIterable of typed events)
151
+ * - Cleanup (Symbol.dispose)
152
+ *
153
+ * @example
154
+ * ```typescript
155
+ * type TermProvider = Provider<
156
+ * { cols: number; rows: number },
157
+ * { key: { input: string; key: Key }; resize: Dims }
158
+ * >;
159
+ * ```
160
+ */
161
+ export interface Provider<State = unknown, Events extends Record<string, unknown> = Record<string, never>> {
162
+ /** Get current state (Zustand-compatible) */
163
+ getState(): State
164
+
165
+ /** Subscribe to state changes (Zustand-compatible) */
166
+ subscribe(listener: (state: State) => void): () => void
167
+
168
+ /** Event stream - yields typed events until disposed */
169
+ events(): AsyncIterable<ProviderEvent<Events>>
170
+
171
+ /** Cleanup resources */
172
+ [Symbol.dispose](): void
173
+ }
174
+
175
+ /**
176
+ * Extract the namespaced event type from a providers map.
177
+ *
178
+ * Given { term: TermProvider, sync: SyncProvider }, produces:
179
+ * | { type: 'term:key'; data: { input: string; key: Key } }
180
+ * | { type: 'term:resize'; data: Dims }
181
+ * | { type: 'sync:data'; data: Item[] }
182
+ * | ...
183
+ */
184
+ export type NamespacedEvent<Providers extends Record<string, Provider<unknown, Record<string, unknown>>>> = {
185
+ [P in keyof Providers]: Providers[P] extends Provider<unknown, infer E>
186
+ ? {
187
+ [K in keyof E & string]: {
188
+ type: `${P & string}:${K}`
189
+ data: E[K]
190
+ }
191
+ }[keyof E & string]
192
+ : never
193
+ }[keyof Providers]
194
+
195
+ /**
196
+ * Extract all event keys from a providers map.
197
+ *
198
+ * Given { term: TermProvider, sync: SyncProvider }, produces:
199
+ * 'term:key' | 'term:resize' | 'sync:data' | ...
200
+ */
201
+ export type ProviderEventKey<Providers extends Record<string, Provider<unknown, Record<string, unknown>>>> =
202
+ NamespacedEvent<Providers>["type"]
203
+
204
+ /**
205
+ * Get the data type for a specific event key.
206
+ */
207
+ export type EventData<
208
+ Providers extends Record<string, Provider<unknown, Record<string, unknown>>>,
209
+ K extends ProviderEventKey<Providers>,
210
+ > = Extract<NamespacedEvent<Providers>, { type: K }>["data"]