@effect-tui/react 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +138 -0
- package/dist/jsx-dev-runtime.d.ts +3 -0
- package/dist/jsx-dev-runtime.d.ts.map +1 -0
- package/dist/jsx-dev-runtime.js +3 -0
- package/dist/jsx-dev-runtime.js.map +1 -0
- package/dist/jsx-runtime.d.ts +47 -0
- package/dist/jsx-runtime.d.ts.map +1 -0
- package/dist/jsx-runtime.js +6 -0
- package/dist/jsx-runtime.js.map +1 -0
- package/dist/src/codeblock.d.ts +9 -0
- package/dist/src/codeblock.d.ts.map +1 -0
- package/dist/src/codeblock.js +24 -0
- package/dist/src/codeblock.js.map +1 -0
- package/dist/src/constants.d.ts +3 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +3 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/debug/DiagnosticsPanel.d.ts +7 -0
- package/dist/src/debug/DiagnosticsPanel.d.ts.map +1 -0
- package/dist/src/debug/DiagnosticsPanel.js +13 -0
- package/dist/src/debug/DiagnosticsPanel.js.map +1 -0
- package/dist/src/highlight.d.ts +20 -0
- package/dist/src/highlight.d.ts.map +1 -0
- package/dist/src/highlight.js +51 -0
- package/dist/src/highlight.js.map +1 -0
- package/dist/src/hooks/index.d.ts +4 -0
- package/dist/src/hooks/index.d.ts.map +1 -0
- package/dist/src/hooks/index.js +3 -0
- package/dist/src/hooks/index.js.map +1 -0
- package/dist/src/hooks/use-keyboard.d.ts +18 -0
- package/dist/src/hooks/use-keyboard.d.ts.map +1 -0
- package/dist/src/hooks/use-keyboard.js +26 -0
- package/dist/src/hooks/use-keyboard.js.map +1 -0
- package/dist/src/hooks/use-paste.d.ts +5 -0
- package/dist/src/hooks/use-paste.d.ts.map +1 -0
- package/dist/src/hooks/use-paste.js +14 -0
- package/dist/src/hooks/use-paste.js.map +1 -0
- package/dist/src/hooks/useFrameStats.d.ts +7 -0
- package/dist/src/hooks/useFrameStats.d.ts.map +1 -0
- package/dist/src/hooks/useFrameStats.js +28 -0
- package/dist/src/hooks/useFrameStats.js.map +1 -0
- package/dist/src/hosts/base.d.ts +22 -0
- package/dist/src/hosts/base.d.ts.map +1 -0
- package/dist/src/hosts/base.js +53 -0
- package/dist/src/hosts/base.js.map +1 -0
- package/dist/src/hosts/box.d.ts +26 -0
- package/dist/src/hosts/box.d.ts.map +1 -0
- package/dist/src/hosts/box.js +84 -0
- package/dist/src/hosts/box.js.map +1 -0
- package/dist/src/hosts/canvas.d.ts +48 -0
- package/dist/src/hosts/canvas.d.ts.map +1 -0
- package/dist/src/hosts/canvas.js +109 -0
- package/dist/src/hosts/canvas.js.map +1 -0
- package/dist/src/hosts/codeblock.d.ts +32 -0
- package/dist/src/hosts/codeblock.d.ts.map +1 -0
- package/dist/src/hosts/codeblock.js +118 -0
- package/dist/src/hosts/codeblock.js.map +1 -0
- package/dist/src/hosts/hstack.d.ts +18 -0
- package/dist/src/hosts/hstack.d.ts.map +1 -0
- package/dist/src/hosts/hstack.js +45 -0
- package/dist/src/hosts/hstack.js.map +1 -0
- package/dist/src/hosts/index.d.ts +16 -0
- package/dist/src/hosts/index.d.ts.map +1 -0
- package/dist/src/hosts/index.js +40 -0
- package/dist/src/hosts/index.js.map +1 -0
- package/dist/src/hosts/spacer.d.ts +19 -0
- package/dist/src/hosts/spacer.d.ts.map +1 -0
- package/dist/src/hosts/spacer.js +28 -0
- package/dist/src/hosts/spacer.js.map +1 -0
- package/dist/src/hosts/text.d.ts +43 -0
- package/dist/src/hosts/text.d.ts.map +1 -0
- package/dist/src/hosts/text.js +148 -0
- package/dist/src/hosts/text.js.map +1 -0
- package/dist/src/hosts/vstack.d.ts +18 -0
- package/dist/src/hosts/vstack.d.ts.map +1 -0
- package/dist/src/hosts/vstack.js +45 -0
- package/dist/src/hosts/vstack.js.map +1 -0
- package/dist/src/hosts/zstack.d.ts +20 -0
- package/dist/src/hosts/zstack.d.ts.map +1 -0
- package/dist/src/hosts/zstack.js +65 -0
- package/dist/src/hosts/zstack.js.map +1 -0
- package/dist/src/index.d.ts +20 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +20 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/inline/index.d.ts +32 -0
- package/dist/src/inline/index.d.ts.map +1 -0
- package/dist/src/inline/index.js +111 -0
- package/dist/src/inline/index.js.map +1 -0
- package/dist/src/jsx.d.ts +2 -0
- package/dist/src/jsx.d.ts.map +1 -0
- package/dist/src/jsx.js +4 -0
- package/dist/src/jsx.js.map +1 -0
- package/dist/src/motion/color-motion-value.d.ts +32 -0
- package/dist/src/motion/color-motion-value.d.ts.map +1 -0
- package/dist/src/motion/color-motion-value.js +80 -0
- package/dist/src/motion/color-motion-value.js.map +1 -0
- package/dist/src/motion/color.d.ts +30 -0
- package/dist/src/motion/color.d.ts.map +1 -0
- package/dist/src/motion/color.js +172 -0
- package/dist/src/motion/color.js.map +1 -0
- package/dist/src/motion/color.test.d.ts +2 -0
- package/dist/src/motion/color.test.d.ts.map +1 -0
- package/dist/src/motion/color.test.js +97 -0
- package/dist/src/motion/color.test.js.map +1 -0
- package/dist/src/motion/event-emitter.d.ts +18 -0
- package/dist/src/motion/event-emitter.d.ts.map +1 -0
- package/dist/src/motion/event-emitter.js +30 -0
- package/dist/src/motion/event-emitter.js.map +1 -0
- package/dist/src/motion/frame.d.ts +9 -0
- package/dist/src/motion/frame.d.ts.map +1 -0
- package/dist/src/motion/frame.js +51 -0
- package/dist/src/motion/frame.js.map +1 -0
- package/dist/src/motion/hooks.d.ts +75 -0
- package/dist/src/motion/hooks.d.ts.map +1 -0
- package/dist/src/motion/hooks.js +190 -0
- package/dist/src/motion/hooks.js.map +1 -0
- package/dist/src/motion/index.d.ts +4 -0
- package/dist/src/motion/index.d.ts.map +1 -0
- package/dist/src/motion/index.js +7 -0
- package/dist/src/motion/index.js.map +1 -0
- package/dist/src/motion/motion-value.d.ts +40 -0
- package/dist/src/motion/motion-value.d.ts.map +1 -0
- package/dist/src/motion/motion-value.js +109 -0
- package/dist/src/motion/motion-value.js.map +1 -0
- package/dist/src/motion/motion-value.test.d.ts +2 -0
- package/dist/src/motion/motion-value.test.d.ts.map +1 -0
- package/dist/src/motion/motion-value.test.js +177 -0
- package/dist/src/motion/motion-value.test.js.map +1 -0
- package/dist/src/motion/spring-math.d.ts +28 -0
- package/dist/src/motion/spring-math.d.ts.map +1 -0
- package/dist/src/motion/spring-math.js +81 -0
- package/dist/src/motion/spring-math.js.map +1 -0
- package/dist/src/motion/types.d.ts +25 -0
- package/dist/src/motion/types.d.ts.map +1 -0
- package/dist/src/motion/types.js +13 -0
- package/dist/src/motion/types.js.map +1 -0
- package/dist/src/output.d.ts +47 -0
- package/dist/src/output.d.ts.map +1 -0
- package/dist/src/output.js +125 -0
- package/dist/src/output.js.map +1 -0
- package/dist/src/profiler.d.ts +6 -0
- package/dist/src/profiler.d.ts.map +1 -0
- package/dist/src/profiler.js +73 -0
- package/dist/src/profiler.js.map +1 -0
- package/dist/src/reconciler/host-config.d.ts +16 -0
- package/dist/src/reconciler/host-config.d.ts.map +1 -0
- package/dist/src/reconciler/host-config.js +174 -0
- package/dist/src/reconciler/host-config.js.map +1 -0
- package/dist/src/reconciler/types.d.ts +52 -0
- package/dist/src/reconciler/types.d.ts.map +1 -0
- package/dist/src/reconciler/types.js +2 -0
- package/dist/src/reconciler/types.js.map +1 -0
- package/dist/src/renderer.d.ts +101 -0
- package/dist/src/renderer.d.ts.map +1 -0
- package/dist/src/renderer.js +509 -0
- package/dist/src/renderer.js.map +1 -0
- package/dist/src/terminal.d.ts +37 -0
- package/dist/src/terminal.d.ts.map +1 -0
- package/dist/src/terminal.js +65 -0
- package/dist/src/terminal.js.map +1 -0
- package/dist/src/test/index.d.ts +3 -0
- package/dist/src/test/index.d.ts.map +1 -0
- package/dist/src/test/index.js +3 -0
- package/dist/src/test/index.js.map +1 -0
- package/dist/src/test/mock-streams.d.ts +44 -0
- package/dist/src/test/mock-streams.d.ts.map +1 -0
- package/dist/src/test/mock-streams.js +136 -0
- package/dist/src/test/mock-streams.js.map +1 -0
- package/dist/src/test/render-tui.d.ts +47 -0
- package/dist/src/test/render-tui.d.ts.map +1 -0
- package/dist/src/test/render-tui.js +76 -0
- package/dist/src/test/render-tui.js.map +1 -0
- package/dist/src/trace/SpanTree.d.ts +10 -0
- package/dist/src/trace/SpanTree.d.ts.map +1 -0
- package/dist/src/trace/SpanTree.js +104 -0
- package/dist/src/trace/SpanTree.js.map +1 -0
- package/dist/src/trace/index.d.ts +30 -0
- package/dist/src/trace/index.d.ts.map +1 -0
- package/dist/src/trace/index.js +142 -0
- package/dist/src/trace/index.js.map +1 -0
- package/dist/src/trace/location.d.ts +9 -0
- package/dist/src/trace/location.d.ts.map +1 -0
- package/dist/src/trace/location.js +88 -0
- package/dist/src/trace/location.js.map +1 -0
- package/dist/src/trace/span-processor.d.ts +16 -0
- package/dist/src/trace/span-processor.d.ts.map +1 -0
- package/dist/src/trace/span-processor.js +54 -0
- package/dist/src/trace/span-processor.js.map +1 -0
- package/dist/src/trace/span-state.d.ts +79 -0
- package/dist/src/trace/span-state.d.ts.map +1 -0
- package/dist/src/trace/span-state.js +229 -0
- package/dist/src/trace/span-state.js.map +1 -0
- package/dist/src/trace/tui-logger.d.ts +8 -0
- package/dist/src/trace/tui-logger.d.ts.map +1 -0
- package/dist/src/trace/tui-logger.js +70 -0
- package/dist/src/trace/tui-logger.js.map +1 -0
- package/dist/src/utils/border.d.ts +31 -0
- package/dist/src/utils/border.d.ts.map +1 -0
- package/dist/src/utils/border.js +81 -0
- package/dist/src/utils/border.js.map +1 -0
- package/dist/src/utils/flex-layout.d.ts +20 -0
- package/dist/src/utils/flex-layout.d.ts.map +1 -0
- package/dist/src/utils/flex-layout.js +85 -0
- package/dist/src/utils/flex-layout.js.map +1 -0
- package/dist/src/utils/index.d.ts +5 -0
- package/dist/src/utils/index.d.ts.map +1 -0
- package/dist/src/utils/index.js +5 -0
- package/dist/src/utils/index.js.map +1 -0
- package/dist/src/utils/padding.d.ts +26 -0
- package/dist/src/utils/padding.d.ts.map +1 -0
- package/dist/src/utils/padding.js +34 -0
- package/dist/src/utils/padding.js.map +1 -0
- package/dist/src/utils/styles.d.ts +13 -0
- package/dist/src/utils/styles.d.ts.map +1 -0
- package/dist/src/utils/styles.js +5 -0
- package/dist/src/utils/styles.js.map +1 -0
- package/dist/src/visualize/index.d.ts +50 -0
- package/dist/src/visualize/index.d.ts.map +1 -0
- package/dist/src/visualize/index.js +194 -0
- package/dist/src/visualize/index.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +94 -0
- package/src/codeblock.tsx +47 -0
- package/src/constants.ts +2 -0
- package/src/debug/DiagnosticsPanel.tsx +38 -0
- package/src/highlight.ts +76 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/use-keyboard.ts +37 -0
- package/src/hooks/use-paste.ts +14 -0
- package/src/hooks/useFrameStats.ts +32 -0
- package/src/hosts/base.ts +65 -0
- package/src/hosts/box.ts +105 -0
- package/src/hosts/canvas.ts +155 -0
- package/src/hosts/codeblock.ts +145 -0
- package/src/hosts/hstack.ts +64 -0
- package/src/hosts/index.ts +45 -0
- package/src/hosts/spacer.ts +40 -0
- package/src/hosts/text.ts +175 -0
- package/src/hosts/vstack.ts +64 -0
- package/src/hosts/zstack.ts +77 -0
- package/src/index.ts +62 -0
- package/src/inline/index.tsx +181 -0
- package/src/jsx.ts +3 -0
- package/src/motion/color-motion-value.ts +90 -0
- package/src/motion/color.test.ts +115 -0
- package/src/motion/color.ts +191 -0
- package/src/motion/event-emitter.ts +35 -0
- package/src/motion/frame.ts +59 -0
- package/src/motion/hooks.ts +237 -0
- package/src/motion/index.ts +17 -0
- package/src/motion/motion-value.test.ts +222 -0
- package/src/motion/motion-value.ts +140 -0
- package/src/motion/spring-math.ts +114 -0
- package/src/motion/types.ts +34 -0
- package/src/output.ts +156 -0
- package/src/profiler.ts +88 -0
- package/src/reconciler/host-config.ts +277 -0
- package/src/reconciler/types.ts +66 -0
- package/src/renderer.ts +661 -0
- package/src/terminal.ts +67 -0
- package/src/test/index.ts +8 -0
- package/src/test/mock-streams.ts +149 -0
- package/src/test/render-tui.ts +118 -0
- package/src/trace/SpanTree.tsx +195 -0
- package/src/trace/index.tsx +205 -0
- package/src/trace/location.ts +90 -0
- package/src/trace/span-processor.ts +65 -0
- package/src/trace/span-state.ts +286 -0
- package/src/trace/tui-logger.ts +72 -0
- package/src/utils/border.ts +108 -0
- package/src/utils/flex-layout.ts +125 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/padding.ts +45 -0
- package/src/utils/styles.ts +14 -0
- package/src/visualize/index.tsx +305 -0
package/package.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@effect-tui/react",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"description": "React bindings for @effect-tui/core",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"src",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": "./dist/src/index.js",
|
|
15
|
+
"types": "./dist/src/index.d.ts"
|
|
16
|
+
},
|
|
17
|
+
"./test": {
|
|
18
|
+
"import": "./dist/src/test/index.js",
|
|
19
|
+
"types": "./dist/src/test/index.d.ts"
|
|
20
|
+
},
|
|
21
|
+
"./visualize": {
|
|
22
|
+
"import": "./dist/src/visualize/index.js",
|
|
23
|
+
"types": "./dist/src/visualize/index.d.ts"
|
|
24
|
+
},
|
|
25
|
+
"./trace": {
|
|
26
|
+
"import": "./dist/src/trace/index.js",
|
|
27
|
+
"types": "./dist/src/trace/index.d.ts"
|
|
28
|
+
},
|
|
29
|
+
"./inline": {
|
|
30
|
+
"import": "./dist/src/inline/index.js",
|
|
31
|
+
"types": "./dist/src/inline/index.d.ts"
|
|
32
|
+
},
|
|
33
|
+
"./jsx-runtime": {
|
|
34
|
+
"import": "./dist/jsx-runtime.js",
|
|
35
|
+
"types": "./dist/jsx-runtime.d.ts"
|
|
36
|
+
},
|
|
37
|
+
"./jsx-dev-runtime": {
|
|
38
|
+
"import": "./dist/jsx-dev-runtime.js",
|
|
39
|
+
"types": "./dist/jsx-dev-runtime.d.ts"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"types": "./dist/src/index.d.ts",
|
|
43
|
+
"keywords": [
|
|
44
|
+
"terminal",
|
|
45
|
+
"tui",
|
|
46
|
+
"effect",
|
|
47
|
+
"react",
|
|
48
|
+
"cli"
|
|
49
|
+
],
|
|
50
|
+
"author": "Kit Langton",
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "https://github.com/kitlangton/effect-tui.git",
|
|
55
|
+
"directory": "packages/effect-tui-react"
|
|
56
|
+
},
|
|
57
|
+
"homepage": "https://github.com/kitlangton/effect-tui",
|
|
58
|
+
"bugs": "https://github.com/kitlangton/effect-tui/issues",
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public",
|
|
61
|
+
"tag": "alpha"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "tsc -p .",
|
|
65
|
+
"typecheck": "tsc -p . --noEmit",
|
|
66
|
+
"test": "vitest run",
|
|
67
|
+
"test:watch": "vitest",
|
|
68
|
+
"format": "biome format --write .",
|
|
69
|
+
"format:check": "biome format .",
|
|
70
|
+
"prepublishOnly": "bun run typecheck && bun run build"
|
|
71
|
+
},
|
|
72
|
+
"dependencies": {
|
|
73
|
+
"@effect/opentelemetry": "^0.59.1",
|
|
74
|
+
"@opentelemetry/api": "^1.9.0",
|
|
75
|
+
"@opentelemetry/sdk-trace-base": "^2.2.0",
|
|
76
|
+
"@effect-tui/core": "workspace:*",
|
|
77
|
+
"react-reconciler": "^0.33.0",
|
|
78
|
+
"shiki": "^3.17.0"
|
|
79
|
+
},
|
|
80
|
+
"peerDependencies": {
|
|
81
|
+
"effect": "^3.0.0",
|
|
82
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
83
|
+
},
|
|
84
|
+
"devDependencies": {
|
|
85
|
+
"@effect/vitest": "^0.27.0",
|
|
86
|
+
"@types/node": "^24.10.1",
|
|
87
|
+
"@types/react": "^19.0.0",
|
|
88
|
+
"@types/react-reconciler": "^0.32.3",
|
|
89
|
+
"effect": "^3.19.8",
|
|
90
|
+
"react": "^19.0.0",
|
|
91
|
+
"typescript": "^5.9.3",
|
|
92
|
+
"vitest": "^4.0.14"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useEffect, useState } from "react"
|
|
2
|
+
import type { BundledLanguage, BundledTheme } from "shiki"
|
|
3
|
+
import { highlightCode, toPlainLines, type HighlightLine } from "./highlight.js"
|
|
4
|
+
import type { CodeBlockProps as HostCodeBlockProps } from "./hosts/codeblock.js"
|
|
5
|
+
|
|
6
|
+
export interface CodeBlockProps extends Omit<HostCodeBlockProps, "lines"> {
|
|
7
|
+
code: string
|
|
8
|
+
language?: BundledLanguage
|
|
9
|
+
theme?: BundledTheme
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function CodeBlock({
|
|
13
|
+
code,
|
|
14
|
+
language = "ts",
|
|
15
|
+
theme = "nord",
|
|
16
|
+
lineNumbers = true,
|
|
17
|
+
padding = 1,
|
|
18
|
+
background,
|
|
19
|
+
...rest
|
|
20
|
+
}: CodeBlockProps) {
|
|
21
|
+
const [lines, setLines] = useState<HighlightLine[]>(() => toPlainLines(code))
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
let cancelled = false
|
|
25
|
+
highlightCode(code, { lang: language, theme })
|
|
26
|
+
.then((result) => {
|
|
27
|
+
if (!cancelled) setLines(result)
|
|
28
|
+
})
|
|
29
|
+
.catch((err) => {
|
|
30
|
+
console.warn("CodeBlock: highlighting failed, falling back to plain text", err)
|
|
31
|
+
if (!cancelled) setLines(toPlainLines(code))
|
|
32
|
+
})
|
|
33
|
+
return () => {
|
|
34
|
+
cancelled = true
|
|
35
|
+
}
|
|
36
|
+
}, [code, language, theme])
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<codeblock
|
|
40
|
+
{...(rest as Record<string, unknown>)}
|
|
41
|
+
lines={lines}
|
|
42
|
+
lineNumbers={lineNumbers as boolean}
|
|
43
|
+
padding={padding as HostCodeBlockProps["padding"]}
|
|
44
|
+
background={background as HostCodeBlockProps["background"]}
|
|
45
|
+
/>
|
|
46
|
+
)
|
|
47
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Colors } from "@effect-tui/core"
|
|
2
|
+
import { useFrameStats } from "../hooks/useFrameStats.js"
|
|
3
|
+
import "../jsx.js"
|
|
4
|
+
|
|
5
|
+
export interface DiagnosticsPanelProps {
|
|
6
|
+
sampleMs?: number
|
|
7
|
+
title?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function DiagnosticsPanel({ sampleMs = 200, title = "Diagnostics" }: DiagnosticsPanelProps) {
|
|
11
|
+
const stats = useFrameStats(sampleMs)
|
|
12
|
+
|
|
13
|
+
const fps = stats ? (stats.frameMs > 0 ? (1000 / stats.frameMs).toFixed(1) : "∞") : "—"
|
|
14
|
+
const bytes = stats ? stats.bytes : 0
|
|
15
|
+
const h = stats ? stats.contentHeight : 0
|
|
16
|
+
const phases = stats?.phases
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<vstack spacing={1}>
|
|
20
|
+
<text fg={Colors.cyan} bold>
|
|
21
|
+
{title} ({stats?.mode ?? "?"})
|
|
22
|
+
</text>
|
|
23
|
+
{stats ? (
|
|
24
|
+
<>
|
|
25
|
+
<text fg={Colors.gray(12)}>
|
|
26
|
+
size {stats.width}×{stats.height} content {h} rows — {bytes} bytes/frame — {fps} fps
|
|
27
|
+
</text>
|
|
28
|
+
<text fg={Colors.gray(12)}>
|
|
29
|
+
clear {phases?.clear.toFixed(2)}ms · layout {phases?.layout.toFixed(2)}ms · render{" "}
|
|
30
|
+
{phases?.render.toFixed(2)}ms · diff {phases?.diffAnsi.toFixed(2)}ms · write {phases?.write.toFixed(2)}ms
|
|
31
|
+
</text>
|
|
32
|
+
</>
|
|
33
|
+
) : (
|
|
34
|
+
<text fg={Colors.gray(10)}>Waiting for first frame…</text>
|
|
35
|
+
)}
|
|
36
|
+
</vstack>
|
|
37
|
+
)
|
|
38
|
+
}
|
package/src/highlight.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createHighlighter, type Highlighter, type BundledLanguage, type BundledTheme } from "shiki"
|
|
2
|
+
import type { ColorLike } from "@effect-tui/core"
|
|
3
|
+
|
|
4
|
+
export interface HighlightTokenStyle {
|
|
5
|
+
fg?: ColorLike
|
|
6
|
+
bg?: ColorLike
|
|
7
|
+
bold?: boolean
|
|
8
|
+
italic?: boolean
|
|
9
|
+
underline?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface HighlightToken {
|
|
13
|
+
text: string
|
|
14
|
+
style?: HighlightTokenStyle
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type HighlightLine = HighlightToken[]
|
|
18
|
+
|
|
19
|
+
const DEFAULT_LANGS: BundledLanguage[] = ["ts", "tsx", "js", "jsx", "json"]
|
|
20
|
+
const DEFAULT_THEME: BundledTheme = "nord"
|
|
21
|
+
|
|
22
|
+
const highlighterCache = new Map<string, Promise<Highlighter>>()
|
|
23
|
+
|
|
24
|
+
async function getCachedHighlighter(theme: BundledTheme): Promise<Highlighter> {
|
|
25
|
+
const key = String(theme)
|
|
26
|
+
let cached = highlighterCache.get(key)
|
|
27
|
+
if (!cached) {
|
|
28
|
+
cached = createHighlighter({
|
|
29
|
+
themes: [theme],
|
|
30
|
+
langs: DEFAULT_LANGS,
|
|
31
|
+
})
|
|
32
|
+
highlighterCache.set(key, cached)
|
|
33
|
+
}
|
|
34
|
+
return cached
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function highlightCode(
|
|
38
|
+
code: string,
|
|
39
|
+
opts?: { lang?: BundledLanguage; theme?: BundledTheme },
|
|
40
|
+
): Promise<HighlightLine[]> {
|
|
41
|
+
const lang = opts?.lang ?? ("ts" satisfies BundledLanguage)
|
|
42
|
+
const theme = opts?.theme ?? DEFAULT_THEME
|
|
43
|
+
|
|
44
|
+
const highlighter = await getCachedHighlighter(theme)
|
|
45
|
+
|
|
46
|
+
if (!highlighter.getLoadedLanguages().includes(lang)) {
|
|
47
|
+
await highlighter.loadLanguage(lang)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const tokensResult = await highlighter.codeToTokens(code, { lang, theme })
|
|
51
|
+
const tokenLines = Array.isArray(tokensResult)
|
|
52
|
+
? tokensResult
|
|
53
|
+
: // Shiki v3 returns { tokens, theme }
|
|
54
|
+
(tokensResult as any).tokens
|
|
55
|
+
|
|
56
|
+
if (!Array.isArray(tokenLines)) return toPlainLines(code)
|
|
57
|
+
|
|
58
|
+
return tokenLines.map((line: any[]) =>
|
|
59
|
+
line.map((token) => {
|
|
60
|
+
const style: HighlightTokenStyle = {}
|
|
61
|
+
if (token.color) style.fg = token.color
|
|
62
|
+
const fs = token.fontStyle ?? 0
|
|
63
|
+
// fontStyle bitmask is: 1 = Italic, 2 = Bold, 4 = Underline
|
|
64
|
+
if (fs & 2) style.bold = true
|
|
65
|
+
if (fs & 1) style.italic = true
|
|
66
|
+
if (fs & 4) style.underline = true
|
|
67
|
+
|
|
68
|
+
return Object.keys(style).length > 0 ? { text: token.content, style } : { text: token.content }
|
|
69
|
+
}),
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function toPlainLines(code: string): HighlightLine[] {
|
|
74
|
+
if (code.length === 0) return [[]]
|
|
75
|
+
return code.split(/\r?\n/).map((line) => [{ text: line }])
|
|
76
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useEffect } from "react"
|
|
2
|
+
import type { KeyMsg } from "@effect-tui/core"
|
|
3
|
+
import { useRenderer } from "../renderer.js"
|
|
4
|
+
|
|
5
|
+
export type UseKeyboardOptions = {
|
|
6
|
+
/** Which phase to listen for; defaults to "press" and treats missing phase as "press". */
|
|
7
|
+
phase?: "press" | "repeat" | "release" | "any"
|
|
8
|
+
/** Optional predicate to drop keys before they reach the handler. */
|
|
9
|
+
filter?: (key: KeyMsg) => boolean
|
|
10
|
+
/**
|
|
11
|
+
* If true, call preventDefault when available before invoking handler.
|
|
12
|
+
* This stops further renderer-level handlers (not a terminal effect).
|
|
13
|
+
*/
|
|
14
|
+
stopPropagation?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Subscribe to keyboard events.
|
|
19
|
+
* Handler is called for every key press while mounted.
|
|
20
|
+
*/
|
|
21
|
+
export function useKeyboard(handler: (key: KeyMsg) => void, opts?: UseKeyboardOptions): void {
|
|
22
|
+
const renderer = useRenderer()
|
|
23
|
+
const phase = opts?.phase ?? "press"
|
|
24
|
+
const filter = opts?.filter
|
|
25
|
+
const stopPropagation = opts?.stopPropagation ?? false
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const wrapped = (key: KeyMsg) => {
|
|
29
|
+
const keyPhase = key.phase ?? "press"
|
|
30
|
+
if (phase !== "any" && phase !== keyPhase) return
|
|
31
|
+
if (filter && !filter(key)) return
|
|
32
|
+
if (stopPropagation && key.preventDefault) key.preventDefault()
|
|
33
|
+
handler(key)
|
|
34
|
+
}
|
|
35
|
+
return renderer.onKey(wrapped)
|
|
36
|
+
}, [renderer, handler, phase, filter, stopPropagation])
|
|
37
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useEffect } from "react"
|
|
2
|
+
import { useRenderer } from "../renderer.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Subscribe to bracketed paste events (if supported by renderer/terminal).
|
|
6
|
+
*/
|
|
7
|
+
export function usePaste(handler: (text: string) => void): void {
|
|
8
|
+
const renderer = useRenderer()
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (!renderer.onPaste) return
|
|
12
|
+
return renderer.onPaste(handler)
|
|
13
|
+
}, [renderer, handler])
|
|
14
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useEffect, useState } from "react"
|
|
2
|
+
import { useRenderer } from "../renderer.js"
|
|
3
|
+
import type { FrameStats } from "../renderer.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Subscribe to per-frame renderer stats (if enabled).
|
|
7
|
+
* Falls back to null when the renderer does not support onFrameStats.
|
|
8
|
+
*/
|
|
9
|
+
export function useFrameStats(sampleMs = 200): FrameStats | null {
|
|
10
|
+
const renderer = useRenderer()
|
|
11
|
+
const [stats, setStats] = useState<FrameStats | null>(null)
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!renderer.onFrameStats) return
|
|
15
|
+
|
|
16
|
+
let last: FrameStats | null = null
|
|
17
|
+
const unsub = renderer.onFrameStats((s) => {
|
|
18
|
+
last = s
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const id = setInterval(() => {
|
|
22
|
+
if (last) setStats(last)
|
|
23
|
+
}, sampleMs)
|
|
24
|
+
|
|
25
|
+
return () => {
|
|
26
|
+
clearInterval(id)
|
|
27
|
+
unsub?.()
|
|
28
|
+
}
|
|
29
|
+
}, [renderer, sampleMs])
|
|
30
|
+
|
|
31
|
+
return stats
|
|
32
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { CellBuffer, Palette } from "@effect-tui/core"
|
|
2
|
+
import type { HostInstance, Rect, Size, HostContext, CommonProps } from "../reconciler/types.js"
|
|
3
|
+
|
|
4
|
+
let idCounter = 0
|
|
5
|
+
|
|
6
|
+
export abstract class BaseHost implements HostInstance {
|
|
7
|
+
id: string
|
|
8
|
+
type: string
|
|
9
|
+
parent: HostInstance | null = null
|
|
10
|
+
children: HostInstance[] = []
|
|
11
|
+
rect: Rect | null = null
|
|
12
|
+
|
|
13
|
+
// Common flex props
|
|
14
|
+
flexGrow = 0
|
|
15
|
+
flexShrink = 1
|
|
16
|
+
|
|
17
|
+
protected ctx: HostContext
|
|
18
|
+
|
|
19
|
+
constructor(type: string, props: CommonProps, ctx: HostContext) {
|
|
20
|
+
this.id = `${type}-${idCounter++}`
|
|
21
|
+
this.type = type
|
|
22
|
+
this.ctx = ctx
|
|
23
|
+
this.updateProps(props)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
abstract measure(maxW: number, maxH: number): Size
|
|
27
|
+
abstract render(buffer: CellBuffer, palette: Palette): void
|
|
28
|
+
|
|
29
|
+
layout(rect: Rect): void {
|
|
30
|
+
this.rect = rect
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
updateProps(props: Record<string, unknown>): void {
|
|
34
|
+
if (props.flexGrow !== undefined) this.flexGrow = props.flexGrow as number
|
|
35
|
+
if (props.flexShrink !== undefined) this.flexShrink = props.flexShrink as number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
destroy(): void {
|
|
39
|
+
// Override in subclasses if cleanup needed
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Child management helpers
|
|
43
|
+
appendChild(child: HostInstance): void {
|
|
44
|
+
this.children.push(child)
|
|
45
|
+
child.parent = this
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
removeChild(child: HostInstance): void {
|
|
49
|
+
const idx = this.children.indexOf(child)
|
|
50
|
+
if (idx >= 0) {
|
|
51
|
+
this.children.splice(idx, 1)
|
|
52
|
+
child.parent = null
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
insertBefore(child: HostInstance, before: HostInstance): void {
|
|
57
|
+
const idx = this.children.indexOf(before)
|
|
58
|
+
if (idx >= 0) {
|
|
59
|
+
this.children.splice(idx, 0, child)
|
|
60
|
+
} else {
|
|
61
|
+
this.children.push(child)
|
|
62
|
+
}
|
|
63
|
+
child.parent = this
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/hosts/box.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { CellBuffer, Palette, ColorValue } from "@effect-tui/core"
|
|
2
|
+
import { Colors } from "@effect-tui/core"
|
|
3
|
+
import type { HostContext, Rect, Size, CommonProps } from "../reconciler/types.js"
|
|
4
|
+
import { BaseHost } from "./base.js"
|
|
5
|
+
import { type BorderKind, borderChars, drawBorder, type Padding, type PaddingInput, resolvePadding } from "../utils/index.js"
|
|
6
|
+
|
|
7
|
+
export type { BorderKind }
|
|
8
|
+
|
|
9
|
+
export interface BoxProps extends CommonProps {
|
|
10
|
+
padding?: PaddingInput
|
|
11
|
+
border?: BorderKind
|
|
12
|
+
borderColor?: ColorValue
|
|
13
|
+
bg?: ColorValue
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class BoxHost extends BaseHost {
|
|
17
|
+
padding: Padding = { top: 0, right: 0, bottom: 0, left: 0 }
|
|
18
|
+
border: BorderKind = "none"
|
|
19
|
+
borderColor?: ColorValue
|
|
20
|
+
bg?: ColorValue
|
|
21
|
+
|
|
22
|
+
constructor(props: BoxProps, ctx: HostContext) {
|
|
23
|
+
super("box", props, ctx)
|
|
24
|
+
this.updateProps(props)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private get borderThickness(): number {
|
|
28
|
+
return this.border === "none" ? 0 : 1
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private get insetX(): number {
|
|
32
|
+
return this.borderThickness + this.padding.left + this.padding.right + this.borderThickness
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private get insetY(): number {
|
|
36
|
+
return this.borderThickness + this.padding.top + this.padding.bottom + this.borderThickness
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
measure(maxW: number, maxH: number): Size {
|
|
40
|
+
const innerMaxW = Math.max(0, maxW - this.insetX)
|
|
41
|
+
const innerMaxH = Math.max(0, maxH - this.insetY)
|
|
42
|
+
|
|
43
|
+
// Measure single child (box should have at most one child)
|
|
44
|
+
let childW = 0
|
|
45
|
+
let childH = 0
|
|
46
|
+
if (this.children.length > 0) {
|
|
47
|
+
const childSize = this.children[0].measure(innerMaxW, innerMaxH)
|
|
48
|
+
childW = childSize.w
|
|
49
|
+
childH = childSize.h
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
w: childW + this.insetX,
|
|
54
|
+
h: childH + this.insetY,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
override layout(rect: Rect): void {
|
|
59
|
+
super.layout(rect)
|
|
60
|
+
|
|
61
|
+
const t = this.borderThickness
|
|
62
|
+
const innerRect: Rect = {
|
|
63
|
+
x: rect.x + t + this.padding.left,
|
|
64
|
+
y: rect.y + t + this.padding.top,
|
|
65
|
+
w: Math.max(0, rect.w - this.insetX),
|
|
66
|
+
h: Math.max(0, rect.h - this.insetY),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Layout single child
|
|
70
|
+
if (this.children.length > 0) {
|
|
71
|
+
this.children[0].layout(innerRect)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
render(buffer: CellBuffer, palette: Palette): void {
|
|
76
|
+
if (!this.rect) return
|
|
77
|
+
const { x, y, w, h } = this.rect
|
|
78
|
+
|
|
79
|
+
// Draw background if set
|
|
80
|
+
if (this.bg !== undefined) {
|
|
81
|
+
const bgStyle = palette.id({ bg: this.bg })
|
|
82
|
+
buffer.fillRect(x, y, w, h, " ".codePointAt(0)!, bgStyle)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Draw border
|
|
86
|
+
if (this.border !== "none" && w >= 2 && h >= 2) {
|
|
87
|
+
const chars = borderChars(this.border)
|
|
88
|
+
const borderStyle = palette.id({ fg: this.borderColor ?? Colors.gray(8) })
|
|
89
|
+
drawBorder(buffer, x, y, w, h, chars, borderStyle)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Render children
|
|
93
|
+
for (const child of this.children) {
|
|
94
|
+
child.render(buffer, palette)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
override updateProps(props: Record<string, unknown>): void {
|
|
99
|
+
super.updateProps(props)
|
|
100
|
+
if (props.padding !== undefined) this.padding = resolvePadding(props.padding as BoxProps["padding"])
|
|
101
|
+
if (props.border !== undefined) this.border = props.border as BorderKind
|
|
102
|
+
if (props.borderColor !== undefined) this.borderColor = props.borderColor as ColorValue
|
|
103
|
+
if (props.bg !== undefined) this.bg = props.bg as ColorValue
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { CellBuffer, Palette, ColorValue } from "@effect-tui/core"
|
|
2
|
+
import { Colors } from "@effect-tui/core"
|
|
3
|
+
import type { HostContext, Size, CommonProps } from "../reconciler/types.js"
|
|
4
|
+
import { BaseHost } from "./base.js"
|
|
5
|
+
import { type BorderKind, borderChars, drawBorder } from "../utils/index.js"
|
|
6
|
+
|
|
7
|
+
export type { BorderKind }
|
|
8
|
+
|
|
9
|
+
export interface DrawContext {
|
|
10
|
+
/** Canvas width in cells */
|
|
11
|
+
width: number
|
|
12
|
+
/** Canvas height in cells */
|
|
13
|
+
height: number
|
|
14
|
+
|
|
15
|
+
/** Draw text at position */
|
|
16
|
+
text(x: number, y: number, str: string, opts?: { fg?: ColorValue; bg?: ColorValue }): void
|
|
17
|
+
|
|
18
|
+
/** Fill rectangle with character */
|
|
19
|
+
fill(x: number, y: number, w: number, h: number, char?: string, opts?: { fg?: ColorValue; bg?: ColorValue }): void
|
|
20
|
+
|
|
21
|
+
/** Draw box with optional border */
|
|
22
|
+
box(
|
|
23
|
+
x: number,
|
|
24
|
+
y: number,
|
|
25
|
+
w: number,
|
|
26
|
+
h: number,
|
|
27
|
+
opts?: {
|
|
28
|
+
border?: BorderKind
|
|
29
|
+
borderColor?: ColorValue
|
|
30
|
+
bg?: ColorValue
|
|
31
|
+
fg?: ColorValue
|
|
32
|
+
},
|
|
33
|
+
): void
|
|
34
|
+
|
|
35
|
+
/** Clear entire canvas */
|
|
36
|
+
clear(): void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CanvasProps extends CommonProps {
|
|
40
|
+
/** Draw function called each render */
|
|
41
|
+
draw: (ctx: DrawContext) => void
|
|
42
|
+
/** Fixed width (default: fill available) */
|
|
43
|
+
width?: number
|
|
44
|
+
/** Fixed height (default: fill available) */
|
|
45
|
+
height?: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class CanvasHost extends BaseHost {
|
|
49
|
+
draw: CanvasProps["draw"] = () => {}
|
|
50
|
+
fixedWidth?: number
|
|
51
|
+
fixedHeight?: number
|
|
52
|
+
|
|
53
|
+
constructor(props: CanvasProps, ctx: HostContext) {
|
|
54
|
+
super("canvas", props, ctx)
|
|
55
|
+
this.updateProps(props)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
measure(maxW: number, maxH: number): Size {
|
|
59
|
+
return {
|
|
60
|
+
w: this.fixedWidth ?? maxW,
|
|
61
|
+
h: this.fixedHeight ?? maxH,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
render(buffer: CellBuffer, palette: Palette): void {
|
|
66
|
+
if (!this.rect) return
|
|
67
|
+
const { x: ox, y: oy, w, h } = this.rect
|
|
68
|
+
|
|
69
|
+
// Create draw context
|
|
70
|
+
const ctx: DrawContext = {
|
|
71
|
+
width: w,
|
|
72
|
+
height: h,
|
|
73
|
+
|
|
74
|
+
text: (x, y, str, opts) => {
|
|
75
|
+
const px = Math.round(ox + x)
|
|
76
|
+
const py = Math.round(oy + y)
|
|
77
|
+
if (py < oy || py >= oy + h) return
|
|
78
|
+
const style = palette.id({ fg: opts?.fg, bg: opts?.bg })
|
|
79
|
+
let col = px
|
|
80
|
+
for (const char of str) {
|
|
81
|
+
if (col >= ox + w) break
|
|
82
|
+
if (col >= ox) {
|
|
83
|
+
buffer.drawCP(col, py, char.codePointAt(0)!, style)
|
|
84
|
+
}
|
|
85
|
+
col++
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
fill: (x, y, fw, fh, char = " ", opts) => {
|
|
90
|
+
const px = Math.round(ox + x)
|
|
91
|
+
const py = Math.round(oy + y)
|
|
92
|
+
const cp = char.codePointAt(0)!
|
|
93
|
+
const style = palette.id({ fg: opts?.fg, bg: opts?.bg })
|
|
94
|
+
for (let row = 0; row < fh; row++) {
|
|
95
|
+
const yy = py + row
|
|
96
|
+
if (yy < oy || yy >= oy + h) continue
|
|
97
|
+
for (let col = 0; col < fw; col++) {
|
|
98
|
+
const xx = px + col
|
|
99
|
+
if (xx < ox || xx >= ox + w) continue
|
|
100
|
+
buffer.drawCP(xx, yy, cp, style)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
box: (x, y, bw, bh, opts) => {
|
|
106
|
+
const px = Math.round(ox + x)
|
|
107
|
+
const py = Math.round(oy + y)
|
|
108
|
+
const border = opts?.border ?? "none"
|
|
109
|
+
const bgStyle = opts?.bg !== undefined ? palette.id({ bg: opts.bg }) : undefined
|
|
110
|
+
|
|
111
|
+
// Fill background (with clipping)
|
|
112
|
+
if (bgStyle !== undefined) {
|
|
113
|
+
for (let row = 0; row < bh; row++) {
|
|
114
|
+
const yy = py + row
|
|
115
|
+
if (yy < oy || yy >= oy + h) continue
|
|
116
|
+
for (let col = 0; col < bw; col++) {
|
|
117
|
+
const xx = px + col
|
|
118
|
+
if (xx < ox || xx >= ox + w) continue
|
|
119
|
+
buffer.drawCP(xx, yy, " ".codePointAt(0)!, bgStyle)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Draw border (with clipping)
|
|
125
|
+
if (border !== "none" && bw >= 2 && bh >= 2) {
|
|
126
|
+
const chars = borderChars(border)
|
|
127
|
+
const borderStyle = palette.id({ fg: opts?.borderColor ?? opts?.fg ?? Colors.gray(8) })
|
|
128
|
+
drawBorder(buffer, px, py, bw, bh, chars, borderStyle, { ox, oy, w, h })
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
clear: () => {
|
|
133
|
+
const style = palette.id({})
|
|
134
|
+
for (let row = 0; row < h; row++) {
|
|
135
|
+
for (let col = 0; col < w; col++) {
|
|
136
|
+
buffer.drawCP(ox + col, oy + row, " ".codePointAt(0)!, style)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Call user's draw function
|
|
143
|
+
this.draw(ctx)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
override updateProps(props: Record<string, unknown>): void {
|
|
147
|
+
super.updateProps(props)
|
|
148
|
+
if (props.draw !== undefined) {
|
|
149
|
+
this.draw = props.draw as CanvasProps["draw"]
|
|
150
|
+
this.ctx.requestRender() // trigger repaint when draw function changes
|
|
151
|
+
}
|
|
152
|
+
if (props.width !== undefined) this.fixedWidth = props.width as number
|
|
153
|
+
if (props.height !== undefined) this.fixedHeight = props.height as number
|
|
154
|
+
}
|
|
155
|
+
}
|