@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,198 @@
1
+ /**
2
+ * Phase 1: Measure Phase
3
+ *
4
+ * Handle fit-content nodes by measuring their intrinsic content size.
5
+ */
6
+
7
+ import type { BoxProps, TeaNode, TextProps } from "@silvery/tea/types"
8
+ import { displayWidthAnsi, wrapText } from "../unicode"
9
+ import { getBorderSize, getPadding } from "./helpers"
10
+ import type { PipelineContext } from "./types"
11
+
12
+ /**
13
+ * Handle fit-content nodes by measuring their intrinsic content size.
14
+ *
15
+ * Traverses the tree and for any node with width="fit-content" or
16
+ * height="fit-content", measures the content and sets the Yoga constraint.
17
+ */
18
+ export function measurePhase(root: TeaNode, ctx?: PipelineContext): void {
19
+ traverseTree(root, (node) => {
20
+ // Skip nodes without Yoga (raw text nodes)
21
+ if (!node.layoutNode) return
22
+
23
+ const props = node.props as BoxProps
24
+
25
+ if (props.width === "fit-content" || props.height === "fit-content") {
26
+ // When height="fit-content" but width is fixed, pass the available width
27
+ // so text nodes can wrap and compute correct intrinsic height.
28
+ let availableWidth: number | undefined
29
+ if (props.height === "fit-content" && props.width !== "fit-content" && typeof props.width === "number") {
30
+ // Subtract padding and border from the fixed width to get content area width
31
+ const padding = getPadding(props)
32
+ availableWidth = props.width - padding.left - padding.right
33
+ if (props.borderStyle) {
34
+ const border = getBorderSize(props)
35
+ availableWidth -= border.left + border.right
36
+ }
37
+ if (availableWidth < 1) availableWidth = 1
38
+ }
39
+
40
+ const intrinsicSize = measureIntrinsicSize(node, ctx, availableWidth)
41
+
42
+ if (props.width === "fit-content") {
43
+ node.layoutNode.setWidth(intrinsicSize.width)
44
+ }
45
+ if (props.height === "fit-content") {
46
+ node.layoutNode.setHeight(intrinsicSize.height)
47
+ }
48
+ }
49
+ })
50
+ }
51
+
52
+ /**
53
+ * Measure the intrinsic size of a node's content.
54
+ *
55
+ * For text nodes: measures the text width and line count.
56
+ * For box nodes: recursively measures children based on flex direction.
57
+ *
58
+ * @param availableWidth - When set, text nodes wrap at this width for height calculation.
59
+ * Used when a container has fixed width + fit-content height.
60
+ */
61
+ function measureIntrinsicSize(
62
+ node: TeaNode,
63
+ ctx?: PipelineContext,
64
+ availableWidth?: number,
65
+ ): {
66
+ width: number
67
+ height: number
68
+ } {
69
+ const props = node.props as BoxProps
70
+
71
+ // display="none" nodes have 0x0 intrinsic size
72
+ if (props.display === "none") {
73
+ return { width: 0, height: 0 }
74
+ }
75
+
76
+ if (node.type === "silvery-text") {
77
+ const textProps = props as TextProps
78
+ const text = collectTextContent(node)
79
+
80
+ // Apply internal_transform if present (used by Transform component).
81
+ // The transform is applied per-line, which can change the width.
82
+ const transform = textProps.internal_transform
83
+ let lines: string[]
84
+
85
+ if (availableWidth !== undefined && availableWidth > 0 && isWrapEnabled(textProps.wrap)) {
86
+ // Wrap text at available width to compute correct height
87
+ lines = ctx ? ctx.measurer.wrapText(text, availableWidth, true, true) : wrapText(text, availableWidth, true, true)
88
+ } else {
89
+ lines = text.split("\n")
90
+ }
91
+
92
+ if (transform) {
93
+ lines = lines.map((line, index) => transform(line, index))
94
+ }
95
+
96
+ const width = Math.max(...lines.map((line) => getTextWidth(line, ctx)))
97
+ return {
98
+ width,
99
+ height: lines.length,
100
+ }
101
+ }
102
+
103
+ // For boxes, measure based on flex direction
104
+ const isRow = props.flexDirection === "row" || props.flexDirection === "row-reverse"
105
+
106
+ let width = 0
107
+ let height = 0
108
+
109
+ let childCount = 0
110
+ for (const child of node.children) {
111
+ const childSize = measureIntrinsicSize(child, ctx, availableWidth)
112
+ childCount++
113
+
114
+ if (isRow) {
115
+ width += childSize.width
116
+ height = Math.max(height, childSize.height)
117
+ } else {
118
+ width = Math.max(width, childSize.width)
119
+ height += childSize.height
120
+ }
121
+ }
122
+
123
+ // Add gap between children
124
+ const gap = (props.gap as number) ?? 0
125
+ if (gap > 0 && childCount > 1) {
126
+ const totalGap = gap * (childCount - 1)
127
+ if (isRow) {
128
+ width += totalGap
129
+ } else {
130
+ height += totalGap
131
+ }
132
+ }
133
+
134
+ // Add padding
135
+ const padding = getPadding(props)
136
+ width += padding.left + padding.right
137
+ height += padding.top + padding.bottom
138
+
139
+ // Add border
140
+ if (props.borderStyle) {
141
+ const border = getBorderSize(props)
142
+ width += border.left + border.right
143
+ height += border.top + border.bottom
144
+ }
145
+
146
+ return { width, height }
147
+ }
148
+
149
+ /**
150
+ * Check if text wrapping is enabled for a text node.
151
+ */
152
+ function isWrapEnabled(wrap: TextProps["wrap"]): boolean {
153
+ return wrap === "wrap" || wrap === true || wrap === undefined
154
+ }
155
+
156
+ /**
157
+ * Traverse tree in depth-first order.
158
+ */
159
+ function traverseTree(node: TeaNode, callback: (node: TeaNode) => void): void {
160
+ callback(node)
161
+ for (const child of node.children) {
162
+ traverseTree(child, callback)
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Get text display width (accounting for wide characters and ANSI codes).
168
+ * Uses ANSI-aware width calculation to handle styled text.
169
+ */
170
+ function getTextWidth(text: string, ctx?: PipelineContext): number {
171
+ if (ctx) return ctx.measurer.displayWidthAnsi(text)
172
+ return displayWidthAnsi(text)
173
+ }
174
+
175
+ /**
176
+ * Collect text content from a node and its children.
177
+ * Used for measuring Text nodes that have nested Text children.
178
+ * Applies internal_transform from child nodes to match render-text.ts behavior.
179
+ */
180
+ function collectTextContent(node: TeaNode): string {
181
+ if (node.textContent !== undefined) {
182
+ return node.textContent
183
+ }
184
+ let result = ""
185
+ for (let i = 0; i < node.children.length; i++) {
186
+ const child = node.children[i]!
187
+ let childText = collectTextContent(child)
188
+ // Apply internal_transform from virtual text nodes (nested Transform components).
189
+ // Matches render-text.ts collectPlainText: transform is applied to the full
190
+ // concatenated text of the child, with index = child position in parent's children array.
191
+ const childTransform = (child.props as TextProps).internal_transform
192
+ if (childTransform && childText.length > 0) {
193
+ childText = childTransform(childText, i)
194
+ }
195
+ result += childText
196
+ }
197
+ return result
198
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Profiling counters for measure function performance analysis (dev only).
3
+ *
4
+ * Shared between @silvery/react/reconciler/nodes (where measure happens)
5
+ * and @silvery/term/pipeline/layout-phase (where stats are logged).
6
+ *
7
+ * Lives in @silvery/term to keep the @silvery/term barrel React-free.
8
+ */
9
+
10
+ export const measureStats = {
11
+ calls: 0,
12
+ cacheHits: 0,
13
+ textCollects: 0,
14
+ displayWidthCalls: 0,
15
+ reset() {
16
+ this.calls = 0
17
+ this.cacheHits = 0
18
+ this.textCollects = 0
19
+ this.displayWidthCalls = 0
20
+ },
21
+ }