@effect-tui/core 0.1.0 → 0.1.4
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/README.md +31 -11
- package/dist/ansi.d.ts +127 -32
- package/dist/ansi.d.ts.map +1 -1
- package/dist/ansi.js +159 -37
- package/dist/ansi.js.map +1 -1
- package/dist/colors.d.ts +139 -0
- package/dist/colors.d.ts.map +1 -0
- package/dist/colors.js +339 -0
- package/dist/colors.js.map +1 -0
- package/dist/index.d.ts +6 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -11
- package/dist/index.js.map +1 -1
- package/dist/keys.d.ts +21 -0
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +199 -58
- package/dist/keys.js.map +1 -1
- package/dist/layout/axis-helpers.d.ts +19 -0
- package/dist/layout/axis-helpers.d.ts.map +1 -0
- package/dist/layout/axis-helpers.js +19 -0
- package/dist/layout/axis-helpers.js.map +1 -0
- package/dist/output.d.ts +59 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +142 -0
- package/dist/output.js.map +1 -0
- package/dist/render/buffer.d.ts.map +1 -1
- package/dist/render/buffer.js +6 -25
- package/dist/render/buffer.js.map +1 -1
- package/dist/render/graphemes.d.ts +15 -0
- package/dist/render/graphemes.d.ts.map +1 -0
- package/dist/render/graphemes.js +28 -0
- package/dist/render/graphemes.js.map +1 -0
- package/dist/render/measure.d.ts +1 -0
- package/dist/render/measure.d.ts.map +1 -1
- package/dist/render/measure.js +14 -36
- package/dist/render/measure.js.map +1 -1
- package/dist/render/palette.d.ts.map +1 -1
- package/dist/render/palette.js +26 -1
- package/dist/render/palette.js.map +1 -1
- package/dist/render/segmenter.d.ts +8 -0
- package/dist/render/segmenter.d.ts.map +1 -0
- package/dist/render/segmenter.js +23 -0
- package/dist/render/segmenter.js.map +1 -0
- package/dist/render/surface.d.ts +6 -32
- package/dist/render/surface.d.ts.map +1 -1
- package/dist/render/surface.js +11 -80
- package/dist/render/surface.js.map +1 -1
- package/dist/runtime/backend_node.d.ts.map +1 -1
- package/dist/runtime/backend_node.js.map +1 -1
- package/dist/tailwind-colors.d.ts +291 -0
- package/dist/tailwind-colors.d.ts.map +1 -0
- package/dist/tailwind-colors.js +291 -0
- package/dist/tailwind-colors.js.map +1 -0
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +55 -55
- package/src/ansi.ts +201 -73
- package/src/colors.ts +468 -0
- package/src/index.ts +28 -14
- package/src/keys.ts +467 -287
- package/src/layout/axis-helpers.ts +33 -0
- package/src/output.ts +175 -0
- package/src/render/buffer.ts +161 -184
- package/src/render/graphemes.ts +34 -0
- package/src/render/measure.ts +15 -38
- package/src/render/palette.ts +98 -77
- package/src/render/segmenter.ts +27 -0
- package/src/render/surface.ts +139 -225
- package/src/runtime/backend_node.ts +71 -71
- package/src/tailwind-colors.ts +295 -0
- package/src/types.ts +18 -0
- package/dist/anim.d.ts +0 -4
- package/dist/anim.d.ts.map +0 -1
- package/dist/anim.js +0 -5
- package/dist/anim.js.map +0 -1
- package/dist/layout/linearStack.d.ts +0 -17
- package/dist/layout/linearStack.d.ts.map +0 -1
- package/dist/layout/linearStack.js +0 -86
- package/dist/layout/linearStack.js.map +0 -1
- package/dist/motion-value.d.ts +0 -58
- package/dist/motion-value.d.ts.map +0 -1
- package/dist/motion-value.js +0 -250
- package/dist/motion-value.js.map +0 -1
- package/dist/present/display.d.ts +0 -58
- package/dist/present/display.d.ts.map +0 -1
- package/dist/present/display.js +0 -168
- package/dist/present/display.js.map +0 -1
- package/dist/present/writers/fullscreen.d.ts +0 -19
- package/dist/present/writers/fullscreen.d.ts.map +0 -1
- package/dist/present/writers/fullscreen.js +0 -55
- package/dist/present/writers/fullscreen.js.map +0 -1
- package/dist/present/writers/inline.d.ts +0 -20
- package/dist/present/writers/inline.d.ts.map +0 -1
- package/dist/present/writers/inline.js +0 -92
- package/dist/present/writers/inline.js.map +0 -1
- package/dist/render/color-utils.d.ts +0 -18
- package/dist/render/color-utils.d.ts.map +0 -1
- package/dist/render/color-utils.js +0 -58
- package/dist/render/color-utils.js.map +0 -1
- package/dist/render/diff.d.ts +0 -30
- package/dist/render/diff.d.ts.map +0 -1
- package/dist/render/diff.js +0 -83
- package/dist/render/diff.js.map +0 -1
- package/dist/spring-physics.d.ts +0 -36
- package/dist/spring-physics.d.ts.map +0 -1
- package/dist/spring-physics.js +0 -113
- package/dist/spring-physics.js.map +0 -1
- package/dist/spring.d.ts +0 -73
- package/dist/spring.d.ts.map +0 -1
- package/dist/spring.js +0 -136
- package/dist/spring.js.map +0 -1
- package/dist/ui/containers/canvas.d.ts +0 -13
- package/dist/ui/containers/canvas.d.ts.map +0 -1
- package/dist/ui/containers/canvas.js +0 -16
- package/dist/ui/containers/canvas.js.map +0 -1
- package/dist/ui/containers/geometry-reader.d.ts +0 -17
- package/dist/ui/containers/geometry-reader.d.ts.map +0 -1
- package/dist/ui/containers/geometry-reader.js +0 -24
- package/dist/ui/containers/geometry-reader.js.map +0 -1
- package/dist/ui/containers/hstack.d.ts +0 -12
- package/dist/ui/containers/hstack.d.ts.map +0 -1
- package/dist/ui/containers/hstack.js +0 -28
- package/dist/ui/containers/hstack.js.map +0 -1
- package/dist/ui/containers/scroll.d.ts +0 -28
- package/dist/ui/containers/scroll.d.ts.map +0 -1
- package/dist/ui/containers/scroll.js +0 -97
- package/dist/ui/containers/scroll.js.map +0 -1
- package/dist/ui/containers/shared.d.ts +0 -12
- package/dist/ui/containers/shared.d.ts.map +0 -1
- package/dist/ui/containers/shared.js +0 -19
- package/dist/ui/containers/shared.js.map +0 -1
- package/dist/ui/containers/vstack.d.ts +0 -12
- package/dist/ui/containers/vstack.d.ts.map +0 -1
- package/dist/ui/containers/vstack.js +0 -28
- package/dist/ui/containers/vstack.js.map +0 -1
- package/dist/ui/containers/zstack.d.ts +0 -14
- package/dist/ui/containers/zstack.d.ts.map +0 -1
- package/dist/ui/containers/zstack.js +0 -36
- package/dist/ui/containers/zstack.js.map +0 -1
- package/dist/ui/core/geometry-store.d.ts +0 -22
- package/dist/ui/core/geometry-store.d.ts.map +0 -1
- package/dist/ui/core/geometry-store.js +0 -29
- package/dist/ui/core/geometry-store.js.map +0 -1
- package/dist/ui/core/geometry.d.ts +0 -34
- package/dist/ui/core/geometry.d.ts.map +0 -1
- package/dist/ui/core/geometry.js +0 -14
- package/dist/ui/core/geometry.js.map +0 -1
- package/dist/ui/core/view.d.ts +0 -25
- package/dist/ui/core/view.d.ts.map +0 -1
- package/dist/ui/core/view.js +0 -34
- package/dist/ui/core/view.js.map +0 -1
- package/dist/ui/index.d.ts +0 -44
- package/dist/ui/index.d.ts.map +0 -1
- package/dist/ui/index.js +0 -39
- package/dist/ui/index.js.map +0 -1
- package/dist/ui/inlinetext.d.ts +0 -24
- package/dist/ui/inlinetext.d.ts.map +0 -1
- package/dist/ui/inlinetext.js +0 -131
- package/dist/ui/inlinetext.js.map +0 -1
- package/dist/ui/install.d.ts +0 -22
- package/dist/ui/install.d.ts.map +0 -1
- package/dist/ui/install.js +0 -66
- package/dist/ui/install.js.map +0 -1
- package/dist/ui/markdown.d.ts +0 -40
- package/dist/ui/markdown.d.ts.map +0 -1
- package/dist/ui/markdown.js +0 -351
- package/dist/ui/markdown.js.map +0 -1
- package/dist/ui/modifiers/border.d.ts +0 -33
- package/dist/ui/modifiers/border.d.ts.map +0 -1
- package/dist/ui/modifiers/border.js +0 -82
- package/dist/ui/modifiers/border.js.map +0 -1
- package/dist/ui/modifiers/fill.d.ts +0 -14
- package/dist/ui/modifiers/fill.d.ts.map +0 -1
- package/dist/ui/modifiers/fill.js +0 -25
- package/dist/ui/modifiers/fill.js.map +0 -1
- package/dist/ui/modifiers/frame.d.ts +0 -23
- package/dist/ui/modifiers/frame.d.ts.map +0 -1
- package/dist/ui/modifiers/frame.js +0 -54
- package/dist/ui/modifiers/frame.js.map +0 -1
- package/dist/ui/modifiers/offset.d.ts +0 -15
- package/dist/ui/modifiers/offset.d.ts.map +0 -1
- package/dist/ui/modifiers/offset.js +0 -21
- package/dist/ui/modifiers/offset.js.map +0 -1
- package/dist/ui/modifiers/opacity.d.ts +0 -15
- package/dist/ui/modifiers/opacity.d.ts.map +0 -1
- package/dist/ui/modifiers/opacity.js +0 -95
- package/dist/ui/modifiers/opacity.js.map +0 -1
- package/dist/ui/modifiers/padding.d.ts +0 -20
- package/dist/ui/modifiers/padding.d.ts.map +0 -1
- package/dist/ui/modifiers/padding.js +0 -36
- package/dist/ui/modifiers/padding.js.map +0 -1
- package/dist/ui/modifiers/styled.d.ts +0 -14
- package/dist/ui/modifiers/styled.d.ts.map +0 -1
- package/dist/ui/modifiers/styled.js +0 -26
- package/dist/ui/modifiers/styled.js.map +0 -1
- package/dist/ui/primitives/rectangle.d.ts +0 -15
- package/dist/ui/primitives/rectangle.d.ts.map +0 -1
- package/dist/ui/primitives/rectangle.js +0 -23
- package/dist/ui/primitives/rectangle.js.map +0 -1
- package/dist/ui/primitives/spacer.d.ts +0 -13
- package/dist/ui/primitives/spacer.d.ts.map +0 -1
- package/dist/ui/primitives/spacer.js +0 -16
- package/dist/ui/primitives/spacer.js.map +0 -1
- package/dist/ui/primitives/text.d.ts +0 -15
- package/dist/ui/primitives/text.d.ts.map +0 -1
- package/dist/ui/primitives/text.js +0 -79
- package/dist/ui/primitives/text.js.map +0 -1
- package/dist/ui/primitives/wrapped-text.d.ts +0 -30
- package/dist/ui/primitives/wrapped-text.d.ts.map +0 -1
- package/dist/ui/primitives/wrapped-text.js +0 -117
- package/dist/ui/primitives/wrapped-text.js.map +0 -1
- package/dist/ui/shinytext.d.ts +0 -66
- package/dist/ui/shinytext.d.ts.map +0 -1
- package/dist/ui/shinytext.js +0 -99
- package/dist/ui/shinytext.js.map +0 -1
- package/dist/ui/text/layout.d.ts +0 -35
- package/dist/ui/text/layout.d.ts.map +0 -1
- package/dist/ui/text/layout.js +0 -102
- package/dist/ui/text/layout.js.map +0 -1
- package/dist/ui/textinput.d.ts +0 -140
- package/dist/ui/textinput.d.ts.map +0 -1
- package/dist/ui/textinput.js +0 -402
- package/dist/ui/textinput.js.map +0 -1
- package/dist/ui/view-constructors.d.ts +0 -72
- package/dist/ui/view-constructors.d.ts.map +0 -1
- package/dist/ui/view-constructors.js +0 -74
- package/dist/ui/view-constructors.js.map +0 -1
- package/src/anim.ts +0 -5
- package/src/layout/linearStack.ts +0 -115
- package/src/motion-value.ts +0 -335
- package/src/present/display.ts +0 -206
- package/src/present/writers/fullscreen.ts +0 -58
- package/src/present/writers/inline.ts +0 -101
- package/src/render/color-utils.ts +0 -60
- package/src/render/diff.ts +0 -95
- package/src/spring-physics.ts +0 -151
- package/src/spring.ts +0 -234
- package/src/ui/__snapshots__/wrappedtext.test.ts.snap +0 -57
- package/src/ui/containers/canvas.ts +0 -18
- package/src/ui/containers/geometry-reader.ts +0 -32
- package/src/ui/containers/hstack.ts +0 -33
- package/src/ui/containers/scroll.ts +0 -106
- package/src/ui/containers/shared.ts +0 -27
- package/src/ui/containers/vstack.ts +0 -34
- package/src/ui/containers/zstack.ts +0 -37
- package/src/ui/core/geometry-store.ts +0 -42
- package/src/ui/core/geometry.ts +0 -30
- package/src/ui/core/view.ts +0 -49
- package/src/ui/index.ts +0 -84
- package/src/ui/inlinetext.ts +0 -135
- package/src/ui/install.ts +0 -110
- package/src/ui/markdown.test.ts +0 -74
- package/src/ui/markdown.ts +0 -388
- package/src/ui/modifiers/border.ts +0 -100
- package/src/ui/modifiers/fill.ts +0 -28
- package/src/ui/modifiers/frame.ts +0 -74
- package/src/ui/modifiers/offset.ts +0 -23
- package/src/ui/modifiers/opacity.ts +0 -93
- package/src/ui/modifiers/padding.ts +0 -53
- package/src/ui/modifiers/styled.ts +0 -31
- package/src/ui/primitives/rectangle.ts +0 -25
- package/src/ui/primitives/spacer.ts +0 -18
- package/src/ui/primitives/text.ts +0 -85
- package/src/ui/primitives/wrapped-text.ts +0 -131
- package/src/ui/shinytext.ts +0 -159
- package/src/ui/text/layout.ts +0 -119
- package/src/ui/textinput.ts +0 -496
- package/src/ui/view-constructors.ts +0 -96
- package/src/ui/wrappedtext.test.ts +0 -138
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
// linearStack.ts — SwiftUI-like linear stacks with flexible children
|
|
2
|
-
|
|
3
|
-
export type Axis = "horizontal" | "vertical"
|
|
4
|
-
export type Size = { w: number; h: number }
|
|
5
|
-
export type Rect = { x: number; y: number; w: number; h: number }
|
|
6
|
-
export type HAlign = "leading" | "center" | "trailing"
|
|
7
|
-
export type VAlign = "top" | "center" | "bottom"
|
|
8
|
-
|
|
9
|
-
/** flexBase[i] = null for fixed, or the child's base main-size for flexible. */
|
|
10
|
-
export function measureLinear(
|
|
11
|
-
axis: Axis,
|
|
12
|
-
maxW: number,
|
|
13
|
-
maxH: number,
|
|
14
|
-
spacing: number,
|
|
15
|
-
sizes: Size[],
|
|
16
|
-
flexBase: (number | null)[],
|
|
17
|
-
): Size {
|
|
18
|
-
const n = sizes.length
|
|
19
|
-
const gaps = Math.max(0, n - 1) * Math.max(0, spacing)
|
|
20
|
-
|
|
21
|
-
let mainSum = 0
|
|
22
|
-
let crossMax = 0
|
|
23
|
-
let hasFlex = false
|
|
24
|
-
|
|
25
|
-
for (let i = 0; i < n; i++) {
|
|
26
|
-
const s = sizes[i]
|
|
27
|
-
if (!s) continue
|
|
28
|
-
const main = axis === "horizontal" ? s.w : s.h
|
|
29
|
-
const cross = axis === "horizontal" ? s.h : s.w
|
|
30
|
-
crossMax = Math.max(crossMax, cross)
|
|
31
|
-
hasFlex ||= flexBase[i] != null
|
|
32
|
-
mainSum += flexBase[i] ?? main
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const maxMain = axis === "horizontal" ? maxW : maxH
|
|
36
|
-
const totalMain = hasFlex ? maxMain : Math.min(maxMain, mainSum + gaps)
|
|
37
|
-
|
|
38
|
-
return axis === "horizontal"
|
|
39
|
-
? { w: Math.min(maxW, totalMain), h: Math.min(maxH, crossMax) }
|
|
40
|
-
: { w: Math.min(maxW, crossMax), h: Math.min(maxH, totalMain) }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function layoutLinear(
|
|
44
|
-
axis: Axis,
|
|
45
|
-
rect: Rect,
|
|
46
|
-
spacing: number,
|
|
47
|
-
crossAlign: HAlign | VAlign,
|
|
48
|
-
sizes: Size[],
|
|
49
|
-
flexBase: (number | null)[],
|
|
50
|
-
): Rect[] {
|
|
51
|
-
const n = sizes.length
|
|
52
|
-
const out: Rect[] = new Array(n)
|
|
53
|
-
const gaps = Math.max(0, n - 1) * Math.max(0, spacing)
|
|
54
|
-
|
|
55
|
-
const mainAvail = axis === "horizontal" ? rect.w : rect.h
|
|
56
|
-
const crossAvail = axis === "horizontal" ? rect.h : rect.w
|
|
57
|
-
|
|
58
|
-
let baseSum = 0
|
|
59
|
-
let flexCount = 0
|
|
60
|
-
for (let i = 0; i < n; i++) {
|
|
61
|
-
if (flexBase[i] != null) {
|
|
62
|
-
baseSum += flexBase[i]!
|
|
63
|
-
flexCount++
|
|
64
|
-
} else {
|
|
65
|
-
const size = sizes[i]
|
|
66
|
-
if (size) {
|
|
67
|
-
baseSum += axis === "horizontal" ? size.w : size.h
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const leftover = Math.max(0, mainAvail - baseSum - gaps)
|
|
73
|
-
const per = flexCount > 0 ? Math.floor(leftover / flexCount) : 0
|
|
74
|
-
let rem = flexCount > 0 ? leftover - per * flexCount : 0
|
|
75
|
-
|
|
76
|
-
const crossOffset = (childCross: number) => {
|
|
77
|
-
if (crossAlign === "center") return Math.floor((crossAvail - childCross) / 2)
|
|
78
|
-
if ((crossAlign === "trailing" && axis === "vertical") || (crossAlign === "bottom" && axis === "horizontal"))
|
|
79
|
-
return Math.max(0, crossAvail - childCross)
|
|
80
|
-
return 0
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
let pos = 0
|
|
84
|
-
for (let i = 0; i < n; i++) {
|
|
85
|
-
const s = sizes[i]
|
|
86
|
-
if (!s) continue
|
|
87
|
-
const baseMain = flexBase[i] ?? (axis === "horizontal" ? s.w : s.h)
|
|
88
|
-
const flexExtra = flexBase[i] != null ? per + (rem > 0 ? (rem--, 1) : 0) : 0
|
|
89
|
-
const wantMain = baseMain + flexExtra
|
|
90
|
-
const childCross = Math.min(crossAvail, axis === "horizontal" ? s.h : s.w)
|
|
91
|
-
const remaining = Math.max(0, mainAvail - pos)
|
|
92
|
-
const takeMain = Math.min(wantMain, remaining)
|
|
93
|
-
const cs = crossOffset(childCross)
|
|
94
|
-
|
|
95
|
-
out[i] =
|
|
96
|
-
axis === "horizontal"
|
|
97
|
-
? { x: rect.x + pos, y: rect.y + cs, w: Math.max(0, takeMain), h: Math.max(0, childCross) }
|
|
98
|
-
: { x: rect.x + cs, y: rect.y + pos, w: Math.max(0, childCross), h: Math.max(0, takeMain) }
|
|
99
|
-
|
|
100
|
-
pos += takeMain
|
|
101
|
-
if (i < n - 1) pos += spacing
|
|
102
|
-
|
|
103
|
-
if (pos >= mainAvail) {
|
|
104
|
-
for (let j = i + 1; j < n; j++) {
|
|
105
|
-
out[j] =
|
|
106
|
-
axis === "horizontal"
|
|
107
|
-
? { x: rect.x + mainAvail, y: rect.y, w: 0, h: 0 }
|
|
108
|
-
: { x: rect.x, y: rect.y + mainAvail, w: 0, h: 0 }
|
|
109
|
-
}
|
|
110
|
-
break
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return out
|
|
115
|
-
}
|
package/src/motion-value.ts
DELETED
|
@@ -1,335 +0,0 @@
|
|
|
1
|
-
// motion-value.ts - Generic MotionValue<T> with adapters and animate()
|
|
2
|
-
|
|
3
|
-
import type { ColorLike, ColorValue } from "./render/surface.js"
|
|
4
|
-
import { parseColor } from "./render/surface.js"
|
|
5
|
-
import { idxToRGB, clamp255 } from "./render/color-utils.js"
|
|
6
|
-
import { DEFAULTS, fromDurationBounce, stepSpring1D, type SpringConfig } from "./spring-physics.js"
|
|
7
|
-
import { colorLikeToRGB } from "./spring.js"
|
|
8
|
-
|
|
9
|
-
// ---- Types ----
|
|
10
|
-
|
|
11
|
-
export type SpringOptions = SpringConfig & { maxDt?: number }
|
|
12
|
-
export type Unsubscribe = () => void
|
|
13
|
-
|
|
14
|
-
export interface AnimationControls {
|
|
15
|
-
readonly time: number
|
|
16
|
-
readonly speed: number
|
|
17
|
-
isRunning(): boolean
|
|
18
|
-
play(): void
|
|
19
|
-
pause(): void
|
|
20
|
-
stop(): void // complete & settle
|
|
21
|
-
cancel(): void // cancel and keep current value
|
|
22
|
-
seek(t: number): void
|
|
23
|
-
finished: Promise<void>
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface Adapter<T> {
|
|
27
|
-
toArray(v: T): number[]
|
|
28
|
-
fromArray(a: ReadonlyArray<number>): T
|
|
29
|
-
clamp?(a: number[]): number[]
|
|
30
|
-
equals?(a: T, b: T): boolean
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface MotionValue<T> {
|
|
34
|
-
get(): T
|
|
35
|
-
set(next: T): void
|
|
36
|
-
setTarget(next: T): void
|
|
37
|
-
getVelocity(): T
|
|
38
|
-
onChange(fn: (v: T) => void): Unsubscribe
|
|
39
|
-
onSettle(fn: () => void): Unsubscribe
|
|
40
|
-
animate(to: T, options?: SpringOptions): AnimationControls
|
|
41
|
-
stop(): void
|
|
42
|
-
destroy(): void
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface ColorMotionValue extends MotionValue<ColorValue> {
|
|
46
|
-
animate(to: ColorLike | ColorValue, options?: SpringOptions): AnimationControls
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// ---- Built-in adapters ----
|
|
50
|
-
|
|
51
|
-
export const numberAdapter: Adapter<number> = {
|
|
52
|
-
toArray: (n) => [n],
|
|
53
|
-
fromArray: (a) => a[0] ?? 0,
|
|
54
|
-
clamp: (a) => a,
|
|
55
|
-
equals: (a, b) => Object.is(a, b),
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export const rgbAdapter: Adapter<ColorValue> = {
|
|
59
|
-
toArray: (c) => [
|
|
60
|
-
typeof c === "number" ? idxToRGB(c).r : c.r,
|
|
61
|
-
typeof c === "number" ? idxToRGB(c).g : c.g,
|
|
62
|
-
typeof c === "number" ? idxToRGB(c).b : c.b,
|
|
63
|
-
],
|
|
64
|
-
fromArray: (a) => ({ r: clamp255(a[0] ?? 0), g: clamp255(a[1] ?? 0), b: clamp255(a[2] ?? 0) }),
|
|
65
|
-
clamp: (a) => [clamp255(a[0] ?? 0), clamp255(a[1] ?? 0), clamp255(a[2] ?? 0)],
|
|
66
|
-
equals: (a, b) =>
|
|
67
|
-
typeof a === "number" || typeof b === "number"
|
|
68
|
-
? JSON.stringify(colorLikeToRGB(a as any)) === JSON.stringify(colorLikeToRGB(b as any))
|
|
69
|
-
: a.r === b.r && a.g === b.g && a.b === b.b,
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ---- Adapter registry ----
|
|
73
|
-
|
|
74
|
-
type AdapterEntry = {
|
|
75
|
-
test: (value: unknown) => boolean
|
|
76
|
-
create: (value: any) => Adapter<any>
|
|
77
|
-
normalize?: (value: any) => any
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const ADAPTERS: AdapterEntry[] = []
|
|
81
|
-
|
|
82
|
-
export function registerAdapter(entry: AdapterEntry) {
|
|
83
|
-
ADAPTERS.unshift(entry)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function isColorObject(x: any): x is { r: number; g: number; b: number } {
|
|
87
|
-
return x && typeof x === "object" && typeof x.r === "number" && typeof x.g === "number" && typeof x.b === "number"
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function isColorLike(v: unknown): v is ColorLike {
|
|
91
|
-
if (isColorObject(v)) return true
|
|
92
|
-
if (typeof v === "string" || typeof v === "number") {
|
|
93
|
-
const parsed = parseColor(v as any)
|
|
94
|
-
return typeof parsed === "number" || isColorObject(parsed)
|
|
95
|
-
}
|
|
96
|
-
return false
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Register built-in adapters
|
|
100
|
-
registerAdapter({
|
|
101
|
-
test: (v) => isColorLike(v),
|
|
102
|
-
create: () => rgbAdapter,
|
|
103
|
-
normalize: (v) => colorLikeToRGB(v as ColorLike),
|
|
104
|
-
})
|
|
105
|
-
registerAdapter({
|
|
106
|
-
test: (v) => typeof v === "number",
|
|
107
|
-
create: () => numberAdapter,
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
// ---- Frameloop (Node-friendly setTimeout-based) ----
|
|
111
|
-
|
|
112
|
-
type Tickable = () => boolean // keep ticking?
|
|
113
|
-
const tickers = new Set<Tickable>()
|
|
114
|
-
let timer: NodeJS.Timeout | null = null
|
|
115
|
-
|
|
116
|
-
function pump() {
|
|
117
|
-
const copy = Array.from(tickers)
|
|
118
|
-
for (const t of copy) {
|
|
119
|
-
const keep = t()
|
|
120
|
-
if (!keep) tickers.delete(t)
|
|
121
|
-
}
|
|
122
|
-
if (tickers.size === 0 && timer) {
|
|
123
|
-
clearTimeout(timer)
|
|
124
|
-
timer = null
|
|
125
|
-
} else {
|
|
126
|
-
// aim ~60fps
|
|
127
|
-
timer = setTimeout(pump, 16)
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function ensureLoop() {
|
|
132
|
-
if (!timer) timer = setTimeout(pump, 16)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ---- AnimationControls implementation ----
|
|
136
|
-
|
|
137
|
-
class AnimationControlsImpl implements AnimationControls {
|
|
138
|
-
private _running = false
|
|
139
|
-
private _resolve!: () => void
|
|
140
|
-
finished: Promise<void> = new Promise<void>((res) => (this._resolve = res))
|
|
141
|
-
time = 0
|
|
142
|
-
speed = 1
|
|
143
|
-
isRunning(): boolean {
|
|
144
|
-
return this._running
|
|
145
|
-
}
|
|
146
|
-
play(): void {
|
|
147
|
-
if (!this._running) {
|
|
148
|
-
this._running = true
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
pause(): void {
|
|
152
|
-
this._running = false
|
|
153
|
-
}
|
|
154
|
-
stop(): void {
|
|
155
|
-
this._running = false
|
|
156
|
-
this._resolve()
|
|
157
|
-
}
|
|
158
|
-
cancel(): void {
|
|
159
|
-
this._running = false
|
|
160
|
-
}
|
|
161
|
-
seek(t: number): void {
|
|
162
|
-
this.time = Math.max(0, t)
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// ---- MotionValue implementation ----
|
|
167
|
-
|
|
168
|
-
class MotionValueImpl<T> implements MotionValue<T> {
|
|
169
|
-
private adapter: Adapter<T>
|
|
170
|
-
private opts: Required<SpringOptions>
|
|
171
|
-
private x: number[]
|
|
172
|
-
private v: number[]
|
|
173
|
-
private target: number[]
|
|
174
|
-
private last: number | undefined
|
|
175
|
-
private changeSubs = new Set<(v: T) => void>()
|
|
176
|
-
private settleSubs = new Set<() => void>()
|
|
177
|
-
private controls?: AnimationControlsImpl
|
|
178
|
-
|
|
179
|
-
constructor(initial: T, adapter: Adapter<T>, opts?: SpringOptions) {
|
|
180
|
-
this.adapter = adapter
|
|
181
|
-
const base = { ...DEFAULTS, maxDt: 0.1, ...(opts ?? {}) }
|
|
182
|
-
this.opts = base as Required<SpringOptions>
|
|
183
|
-
if (this.opts.duration != null) {
|
|
184
|
-
const derived = fromDurationBounce(this.opts.duration, this.opts.bounce ?? DEFAULTS.bounce)
|
|
185
|
-
this.opts.frequency = derived.frequency
|
|
186
|
-
this.opts.dampingRatio = derived.dampingRatio
|
|
187
|
-
}
|
|
188
|
-
this.x = adapter.toArray(initial)
|
|
189
|
-
this.v = this.x.map(() => 0)
|
|
190
|
-
this.target = this.x.slice()
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
get(): T {
|
|
194
|
-
return this.adapter.fromArray(this.x)
|
|
195
|
-
}
|
|
196
|
-
set(next: T): void {
|
|
197
|
-
this.x = this.adapter.toArray(next)
|
|
198
|
-
if (this.adapter.clamp) this.x = this.adapter.clamp(this.x)
|
|
199
|
-
this.v = this.v.map(() => 0)
|
|
200
|
-
this.target = this.x.slice()
|
|
201
|
-
this.emitChange()
|
|
202
|
-
}
|
|
203
|
-
setTarget(next: T): void {
|
|
204
|
-
this.target = this.adapter.toArray(next)
|
|
205
|
-
if (this.adapter.clamp) this.target = this.adapter.clamp(this.target)
|
|
206
|
-
}
|
|
207
|
-
getVelocity(): T {
|
|
208
|
-
return this.adapter.fromArray(this.v)
|
|
209
|
-
}
|
|
210
|
-
onChange(fn: (v: T) => void): Unsubscribe {
|
|
211
|
-
this.changeSubs.add(fn)
|
|
212
|
-
return () => this.changeSubs.delete(fn)
|
|
213
|
-
}
|
|
214
|
-
onSettle(fn: () => void): Unsubscribe {
|
|
215
|
-
this.settleSubs.add(fn)
|
|
216
|
-
return () => this.settleSubs.delete(fn)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
animate(to: T, options?: SpringOptions): AnimationControls {
|
|
220
|
-
this.setTarget(to)
|
|
221
|
-
this.opts = { ...this.opts, ...(options ?? {}) } as Required<SpringOptions>
|
|
222
|
-
if (this.opts.duration != null) {
|
|
223
|
-
const derived = fromDurationBounce(this.opts.duration, this.opts.bounce ?? DEFAULTS.bounce)
|
|
224
|
-
this.opts.frequency = derived.frequency
|
|
225
|
-
this.opts.dampingRatio = derived.dampingRatio
|
|
226
|
-
}
|
|
227
|
-
this.controls?.cancel()
|
|
228
|
-
const controls = new AnimationControlsImpl()
|
|
229
|
-
controls.play()
|
|
230
|
-
this.controls = controls
|
|
231
|
-
this.last = undefined
|
|
232
|
-
|
|
233
|
-
const tick = () => {
|
|
234
|
-
if (!controls.isRunning()) return false
|
|
235
|
-
const now = Date.now()
|
|
236
|
-
const last = this.last ?? now
|
|
237
|
-
const dt = Math.max(0, Math.min(this.opts.maxDt ?? 0.1, (now - last) / 1000))
|
|
238
|
-
this.last = now
|
|
239
|
-
const { frequency: f, dampingRatio: z, epsilon, bounce } = this.opts
|
|
240
|
-
let settled = true
|
|
241
|
-
for (let i = 0; i < this.x.length; i++) {
|
|
242
|
-
const prevX = this.x[i]
|
|
243
|
-
let { x: nextX, v: nextV } = stepSpring1D(prevX, this.v[i], this.target[i], dt, f!, z!)
|
|
244
|
-
if ((bounce ?? DEFAULTS.bounce) <= 0) {
|
|
245
|
-
const crossed = (prevX - this.target[i]) * (nextX - this.target[i]) <= 0
|
|
246
|
-
if (crossed) {
|
|
247
|
-
nextX = this.target[i]
|
|
248
|
-
nextV = 0
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
this.x[i] = nextX
|
|
252
|
-
this.v[i] = nextV
|
|
253
|
-
if (
|
|
254
|
-
!(
|
|
255
|
-
Math.abs(this.x[i] - this.target[i]) < (epsilon ?? DEFAULTS.epsilon!) &&
|
|
256
|
-
Math.abs(this.v[i]) < (epsilon ?? DEFAULTS.epsilon!)
|
|
257
|
-
)
|
|
258
|
-
) {
|
|
259
|
-
settled = false
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
if (this.adapter.clamp) this.x = this.adapter.clamp(this.x)
|
|
263
|
-
this.emitChange()
|
|
264
|
-
if (settled) {
|
|
265
|
-
this.x = this.target.slice()
|
|
266
|
-
this.v = this.v.map(() => 0)
|
|
267
|
-
this.emitChange()
|
|
268
|
-
this.emitSettle()
|
|
269
|
-
controls.stop()
|
|
270
|
-
return false
|
|
271
|
-
}
|
|
272
|
-
return true
|
|
273
|
-
}
|
|
274
|
-
tickers.add(tick)
|
|
275
|
-
ensureLoop()
|
|
276
|
-
return controls
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
stop(): void {
|
|
280
|
-
this.controls?.stop()
|
|
281
|
-
}
|
|
282
|
-
destroy(): void {
|
|
283
|
-
this.changeSubs.clear()
|
|
284
|
-
this.settleSubs.clear()
|
|
285
|
-
this.controls?.cancel()
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
private emitChange() {
|
|
289
|
-
const v = this.get()
|
|
290
|
-
this.changeSubs.forEach((fn) => fn(v))
|
|
291
|
-
}
|
|
292
|
-
private emitSettle() {
|
|
293
|
-
this.settleSubs.forEach((fn) => fn())
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
class ColorMotionValueImpl extends MotionValueImpl<ColorValue> implements ColorMotionValue {
|
|
298
|
-
override animate(to: ColorLike | ColorValue, options?: SpringOptions): AnimationControls {
|
|
299
|
-
const target =
|
|
300
|
-
typeof to === "string" || typeof to === "number" ? colorLikeToRGB(to as ColorLike) : (to as ColorValue)
|
|
301
|
-
return super.animate(target, options)
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// ---- Factory ----
|
|
306
|
-
|
|
307
|
-
export function motionValue(initial: number, opts?: SpringOptions & { adapter?: Adapter<number> }): MotionValue<number>
|
|
308
|
-
export function motionValue(
|
|
309
|
-
initial: ColorLike,
|
|
310
|
-
opts?: SpringOptions & { adapter?: Adapter<ColorValue> },
|
|
311
|
-
): ColorMotionValue
|
|
312
|
-
export function motionValue<T>(initial: T, opts?: SpringOptions & { adapter?: Adapter<T> }): MotionValue<T>
|
|
313
|
-
export function motionValue(initial: any, opts?: SpringOptions & { adapter?: Adapter<any> }): MotionValue<any> {
|
|
314
|
-
if (opts?.adapter) {
|
|
315
|
-
const norm = opts.adapter.fromArray(opts.adapter.toArray(initial))
|
|
316
|
-
return new MotionValueImpl(norm, opts.adapter, opts)
|
|
317
|
-
}
|
|
318
|
-
for (const entry of ADAPTERS) {
|
|
319
|
-
if (entry.test(initial)) {
|
|
320
|
-
const adapter = entry.create(initial)
|
|
321
|
-
const normalized = entry.normalize ? entry.normalize(initial) : adapter.fromArray(adapter.toArray(initial))
|
|
322
|
-
if (adapter === rgbAdapter) return new ColorMotionValueImpl(normalized as ColorValue, adapter, opts)
|
|
323
|
-
return new MotionValueImpl(normalized, adapter, opts)
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
throw new Error("No adapter found for initial value. Provide { adapter } explicitly.")
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// ---- Top-level animate helper ----
|
|
330
|
-
|
|
331
|
-
export function animate<T>(mv: MotionValue<T>, to: T, opts?: SpringOptions): AnimationControls
|
|
332
|
-
export function animate(mv: ColorMotionValue, to: ColorLike | ColorValue, opts?: SpringOptions): AnimationControls
|
|
333
|
-
export function animate(mv: any, to: any, opts?: SpringOptions): AnimationControls {
|
|
334
|
-
return mv.animate(to as any, opts)
|
|
335
|
-
}
|
package/src/present/display.ts
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import { Palette, Surface, type Surface as SurfaceType } from "../render/surface.js"
|
|
2
|
-
import { diffFrames, type RunWriter } from "../render/diff.js"
|
|
3
|
-
import { FullscreenWriter } from "./writers/fullscreen.js"
|
|
4
|
-
import { LineDiffWriter } from "./writers/inline.js"
|
|
5
|
-
import type { View } from "../ui/core/view.js"
|
|
6
|
-
import { ANSI, Terminal } from "../ansi.js"
|
|
7
|
-
import type { TerminalBackend } from "../runtime/backend_node.js"
|
|
8
|
-
import { Effect } from "effect"
|
|
9
|
-
import { geometryStore } from "../ui/core/geometry-store.js"
|
|
10
|
-
|
|
11
|
-
export interface DisplayRenderer {
|
|
12
|
-
start(): void
|
|
13
|
-
draw(view: View): void
|
|
14
|
-
resize(cols: number, rows: number): void
|
|
15
|
-
end(): void
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
abstract class BaseRenderer implements DisplayRenderer {
|
|
19
|
-
protected palette = new Palette()
|
|
20
|
-
protected prev?: SurfaceType
|
|
21
|
-
protected next?: SurfaceType
|
|
22
|
-
|
|
23
|
-
constructor(protected backend: TerminalBackend) {}
|
|
24
|
-
abstract start(): void
|
|
25
|
-
abstract end(): void
|
|
26
|
-
abstract makeWriter(): RunWriter
|
|
27
|
-
|
|
28
|
-
protected ensureFrames(cols: number, rows: number) {
|
|
29
|
-
const needNew =
|
|
30
|
-
!this.prev ||
|
|
31
|
-
!this.next ||
|
|
32
|
-
this.prev.w !== cols ||
|
|
33
|
-
this.prev.h !== rows ||
|
|
34
|
-
this.next.w !== cols ||
|
|
35
|
-
this.next.h !== rows
|
|
36
|
-
if (needNew) {
|
|
37
|
-
this.prev = new Surface(cols, rows, { palette: this.palette })
|
|
38
|
-
this.next = new Surface(cols, rows, { palette: this.palette })
|
|
39
|
-
this.prev.clear(0)
|
|
40
|
-
this.next.clear(0)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
protected swap() {
|
|
45
|
-
const t = this.prev!
|
|
46
|
-
this.prev = this.next!
|
|
47
|
-
this.next = t
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
draw(view: View): void {
|
|
51
|
-
const { cols, rows } = this.backend.size()
|
|
52
|
-
this.ensureFrames(cols, rows)
|
|
53
|
-
|
|
54
|
-
// render into the "next" frame
|
|
55
|
-
this.next?.clear(0)
|
|
56
|
-
geometryStore.beginFrame()
|
|
57
|
-
view.render(this.next!, this.palette, { x: 0, y: 0, w: cols, h: rows })
|
|
58
|
-
geometryStore.endFrame()
|
|
59
|
-
|
|
60
|
-
const writer = this.makeWriter()
|
|
61
|
-
diffFrames(this.prev!, this.next!, writer)
|
|
62
|
-
this.backend.write(writer.flush())
|
|
63
|
-
|
|
64
|
-
// swap for next frame
|
|
65
|
-
this.swap()
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
resize(_c: number, _r: number): void {
|
|
69
|
-
// Clear terminal and force full rerender
|
|
70
|
-
this.backend.write(Terminal.clearAndHome) // clear screen + move to top-left
|
|
71
|
-
delete (this as any).prev
|
|
72
|
-
delete (this as any).next
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export class FullscreenRenderer extends BaseRenderer {
|
|
77
|
-
/**
|
|
78
|
-
* Create a fullscreen renderer. Kept as an Effect to align with inline
|
|
79
|
-
* and allow future async setup without changing call sites.
|
|
80
|
-
*/
|
|
81
|
-
static make(backend: TerminalBackend) {
|
|
82
|
-
return Effect.sync(() => new FullscreenRenderer(backend))
|
|
83
|
-
}
|
|
84
|
-
start(): void {
|
|
85
|
-
this.backend.enterFullscreen()
|
|
86
|
-
}
|
|
87
|
-
end(): void {
|
|
88
|
-
this.backend.exitFullscreen()
|
|
89
|
-
}
|
|
90
|
-
makeWriter(): RunWriter {
|
|
91
|
-
return new FullscreenWriter(this.palette)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export class InlineRenderer extends BaseRenderer {
|
|
96
|
-
private writer = new LineDiffWriter(this.palette)
|
|
97
|
-
private baseRow: number | undefined
|
|
98
|
-
private skipDiff = false
|
|
99
|
-
private lastPrintedHeight = 1
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Create an inline renderer anchored to the current cursor row.
|
|
103
|
-
*/
|
|
104
|
-
static make(backend: TerminalBackend) {
|
|
105
|
-
return Effect.promise(() => backend.getCursorPosition()).pipe(
|
|
106
|
-
Effect.catchAll(() => Effect.succeed({ row: process.stdout.rows || 24, col: 1 })),
|
|
107
|
-
Effect.map((pos) => {
|
|
108
|
-
const r = new InlineRenderer(backend)
|
|
109
|
-
r.setBaseRow(Math.max(0, (pos.row | 0) - 1))
|
|
110
|
-
return r
|
|
111
|
-
}),
|
|
112
|
-
)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
setBaseRow(n: number) {
|
|
116
|
-
this.baseRow = Math.max(0, n | 0)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
start(): void {
|
|
120
|
-
this.backend.hideCursor()
|
|
121
|
-
}
|
|
122
|
-
end(): void {
|
|
123
|
-
const topRow = this.baseRow ?? 0
|
|
124
|
-
const nextRow1 = Math.max(1, topRow + this.lastPrintedHeight)
|
|
125
|
-
this.backend.write(ANSI.cursor.to(nextRow1 + 1, 1))
|
|
126
|
-
this.backend.showCursor()
|
|
127
|
-
}
|
|
128
|
-
makeWriter(): RunWriter {
|
|
129
|
-
return this.writer
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Override resize to clear screen and reset base row for inline mode
|
|
133
|
-
override resize(c: number, r: number): void {
|
|
134
|
-
// Clear entire screen, scrollback buffer, and move cursor to top-left
|
|
135
|
-
this.backend.write(ANSI.screen.clearScrollback + Terminal.clearAndHome)
|
|
136
|
-
// Reset base row to start from top
|
|
137
|
-
this.baseRow = 0
|
|
138
|
-
// Force full redraw on next draw
|
|
139
|
-
this.skipDiff = true
|
|
140
|
-
// Call parent to clear surfaces
|
|
141
|
-
super.resize(c, r)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Override draw to support inline base-row anchoring and scroll-up
|
|
145
|
-
override draw(view: View): void {
|
|
146
|
-
const { cols, rows } = this.backend.size()
|
|
147
|
-
this.ensureFrames(cols, rows)
|
|
148
|
-
|
|
149
|
-
let baseRow = this.baseRow ?? 0
|
|
150
|
-
|
|
151
|
-
const measured = view.measure(cols, rows)
|
|
152
|
-
const renderW = Math.max(1, Math.min(cols, measured.w || 0))
|
|
153
|
-
const renderH = Math.max(1, Math.min(rows, measured.h || 0))
|
|
154
|
-
|
|
155
|
-
// Render into the "next" frame (clamp to measured size so we don't fill the viewport)
|
|
156
|
-
this.next?.clear(0)
|
|
157
|
-
geometryStore.beginFrame()
|
|
158
|
-
view.render(this.next!, this.palette, { x: 0, y: 0, w: renderW, h: renderH })
|
|
159
|
-
geometryStore.endFrame()
|
|
160
|
-
|
|
161
|
-
// Determine content footprint
|
|
162
|
-
const bounds = this.next?.contentBounds()
|
|
163
|
-
const contentH = Math.max(1, (bounds?.h ?? 0) | 0)
|
|
164
|
-
this.lastPrintedHeight = contentH
|
|
165
|
-
|
|
166
|
-
// If bottom would overflow viewport, scroll up and reduce baseRow
|
|
167
|
-
const bottom1 = baseRow + contentH
|
|
168
|
-
if (bottom1 > rows) {
|
|
169
|
-
const scroll = bottom1 - rows
|
|
170
|
-
if (scroll > 0) {
|
|
171
|
-
this.backend.write(ANSI.scroll.up(scroll))
|
|
172
|
-
baseRow = Math.max(0, baseRow - scroll)
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Inform writer of base row for absolute addressing
|
|
177
|
-
this.writer.setBaseRow?.(baseRow)
|
|
178
|
-
|
|
179
|
-
// If we need to skip diff, render directly
|
|
180
|
-
if (this.skipDiff) {
|
|
181
|
-
// Position cursor at base row
|
|
182
|
-
this.backend.write(ANSI.cursor.to(baseRow + 1, 1))
|
|
183
|
-
// Just render what we have in the surface (viewport content)
|
|
184
|
-
const output = this.next?.buildStyledLines({ x: 0, y: 0, w: this.next?.w ?? 0, h: contentH }, this.palette)
|
|
185
|
-
this.backend.write((output ?? []).join("\n"))
|
|
186
|
-
this.skipDiff = false
|
|
187
|
-
} else {
|
|
188
|
-
// Normal diff and flush
|
|
189
|
-
diffFrames(this.prev!, this.next!, this.writer)
|
|
190
|
-
this.backend.write(this.writer.flush())
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// swap for next frame and advance baseline for subsequent renders
|
|
194
|
-
this.swap()
|
|
195
|
-
this.baseRow = baseRow
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export type RendererMode = "fullscreen" | "inline"
|
|
200
|
-
|
|
201
|
-
export class TerminalRenderer {
|
|
202
|
-
private constructor() {}
|
|
203
|
-
static make(mode: RendererMode, backend: TerminalBackend) {
|
|
204
|
-
return mode === "inline" ? InlineRenderer.make(backend) : FullscreenRenderer.make(backend)
|
|
205
|
-
}
|
|
206
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import type { RunWriter } from "../../render/diff.js"
|
|
2
|
-
import type { Palette } from "../../render/surface.js"
|
|
3
|
-
import { displayWidth } from "../../render/measure.js"
|
|
4
|
-
import { ANSI } from "../../ansi.js"
|
|
5
|
-
|
|
6
|
-
class ChunkBuilder {
|
|
7
|
-
private parts: string[] = []
|
|
8
|
-
push(s: string) {
|
|
9
|
-
if (s) this.parts.push(s)
|
|
10
|
-
}
|
|
11
|
-
concat() {
|
|
12
|
-
const out = this.parts.join("")
|
|
13
|
-
this.parts = []
|
|
14
|
-
return out
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export class FullscreenWriter implements RunWriter {
|
|
19
|
-
private out = new ChunkBuilder()
|
|
20
|
-
private curRow = -1
|
|
21
|
-
private curCol = -1
|
|
22
|
-
private curStyle = -1
|
|
23
|
-
private rows = 0
|
|
24
|
-
|
|
25
|
-
constructor(private palette: Palette) {}
|
|
26
|
-
|
|
27
|
-
begin(vp: { cols: number; rows: number }) {
|
|
28
|
-
this.rows = vp.rows
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
run(row: number, col: number, styleId: number, text: string): void {
|
|
32
|
-
if (row !== this.curRow || col !== this.curCol) {
|
|
33
|
-
this.out.push(ANSI.cursor.to(row + 1, col + 1))
|
|
34
|
-
this.curRow = row
|
|
35
|
-
this.curCol = col
|
|
36
|
-
}
|
|
37
|
-
if (this.curStyle !== styleId) {
|
|
38
|
-
this.out.push(this.palette.sgr(styleId))
|
|
39
|
-
this.curStyle = styleId
|
|
40
|
-
}
|
|
41
|
-
this.out.push(text)
|
|
42
|
-
this.curCol += displayWidth(text) // fix for wide glyphs
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
end(): void {
|
|
46
|
-
if (this.curStyle !== -1) this.out.push(ANSI.reset)
|
|
47
|
-
// park cursor bottom-left (optional)
|
|
48
|
-
this.out.push(ANSI.cursor.to(this.rows, 1))
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
flush(): string {
|
|
52
|
-
const s = this.out.concat()
|
|
53
|
-
// reset tracked cursor/style to unknown so next frame emits properly
|
|
54
|
-
this.curRow = this.curCol = -1
|
|
55
|
-
this.curStyle = -1
|
|
56
|
-
return s
|
|
57
|
-
}
|
|
58
|
-
}
|