@effect-tui/react 0.1.6 → 0.2.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.
- package/dist/src/dev.js +2 -2
- package/dist/src/dev.js.map +1 -1
- package/dist/src/hooks/index.d.ts +1 -0
- package/dist/src/hooks/index.d.ts.map +1 -1
- package/dist/src/hooks/index.js +1 -0
- package/dist/src/hooks/index.js.map +1 -1
- package/dist/src/hooks/use-quit.d.ts +21 -0
- package/dist/src/hooks/use-quit.d.ts.map +1 -0
- package/dist/src/hooks/use-quit.js +29 -0
- package/dist/src/hooks/use-quit.js.map +1 -0
- package/dist/src/hosts/base.d.ts +6 -2
- package/dist/src/hosts/base.d.ts.map +1 -1
- package/dist/src/hosts/base.js +56 -6
- package/dist/src/hosts/base.js.map +1 -1
- package/dist/src/hosts/box.d.ts.map +1 -1
- package/dist/src/hosts/box.js +3 -5
- package/dist/src/hosts/box.js.map +1 -1
- package/dist/src/hosts/flex-container.d.ts.map +1 -1
- package/dist/src/hosts/flex-container.js +4 -1
- package/dist/src/hosts/flex-container.js.map +1 -1
- package/dist/src/hosts/overlay-item.d.ts +2 -2
- package/dist/src/hosts/overlay-item.d.ts.map +1 -1
- package/dist/src/hosts/overlay-item.js +7 -22
- package/dist/src/hosts/overlay-item.js.map +1 -1
- package/dist/src/hosts/overlay.d.ts.map +1 -1
- package/dist/src/hosts/overlay.js +3 -35
- package/dist/src/hosts/overlay.js.map +1 -1
- package/dist/src/hosts/scroll.d.ts +1 -0
- package/dist/src/hosts/scroll.d.ts.map +1 -1
- package/dist/src/hosts/scroll.js +14 -7
- package/dist/src/hosts/scroll.js.map +1 -1
- package/dist/src/hosts/spacer.d.ts +1 -0
- package/dist/src/hosts/spacer.d.ts.map +1 -1
- package/dist/src/hosts/spacer.js +10 -9
- package/dist/src/hosts/spacer.js.map +1 -1
- package/dist/src/hosts/text.js +2 -2
- package/dist/src/hosts/text.js.map +1 -1
- package/dist/src/hosts/zstack.d.ts +3 -2
- package/dist/src/hosts/zstack.d.ts.map +1 -1
- package/dist/src/hosts/zstack.js +3 -18
- package/dist/src/hosts/zstack.js.map +1 -1
- package/dist/src/reconciler/types.d.ts +9 -4
- package/dist/src/reconciler/types.d.ts.map +1 -1
- package/dist/src/remote/Procedures.d.ts +4 -0
- package/dist/src/remote/Procedures.d.ts.map +1 -1
- package/dist/src/remote/Procedures.js +4 -0
- package/dist/src/remote/Procedures.js.map +1 -1
- package/dist/src/remote/Router.d.ts +2 -0
- package/dist/src/remote/Router.d.ts.map +1 -1
- package/dist/src/remote/Router.js.map +1 -1
- package/dist/src/remote/index.d.ts +8 -2
- package/dist/src/remote/index.d.ts.map +1 -1
- package/dist/src/remote/index.js +31 -3
- package/dist/src/remote/index.js.map +1 -1
- package/dist/src/renderer/input/InputProcessor.d.ts +1 -0
- package/dist/src/renderer/input/InputProcessor.d.ts.map +1 -1
- package/dist/src/renderer/input/InputProcessor.js +6 -1
- package/dist/src/renderer/input/InputProcessor.js.map +1 -1
- package/dist/src/renderer/lifecycle/TerminalSetup.d.ts +1 -0
- package/dist/src/renderer/lifecycle/TerminalSetup.d.ts.map +1 -1
- package/dist/src/renderer/lifecycle/TerminalSetup.js +26 -17
- package/dist/src/renderer/lifecycle/TerminalSetup.js.map +1 -1
- package/dist/src/renderer/modes/FullscreenRenderer.d.ts.map +1 -1
- package/dist/src/renderer/modes/FullscreenRenderer.js +8 -6
- package/dist/src/renderer/modes/FullscreenRenderer.js.map +1 -1
- package/dist/src/renderer/modes/InlineRenderer.d.ts.map +1 -1
- package/dist/src/renderer/modes/InlineRenderer.js +9 -7
- package/dist/src/renderer/modes/InlineRenderer.js.map +1 -1
- package/dist/src/renderer.d.ts +2 -0
- package/dist/src/renderer.d.ts.map +1 -1
- package/dist/src/renderer.js +39 -7
- package/dist/src/renderer.js.map +1 -1
- package/dist/src/test/render-tui.d.ts +5 -0
- package/dist/src/test/render-tui.d.ts.map +1 -1
- package/dist/src/test/render-tui.js +3 -0
- package/dist/src/test/render-tui.js.map +1 -1
- package/dist/src/utils/alignment.d.ts +15 -0
- package/dist/src/utils/alignment.d.ts.map +1 -0
- package/dist/src/utils/alignment.js +37 -0
- package/dist/src/utils/alignment.js.map +1 -0
- package/dist/src/utils/flex-layout.d.ts.map +1 -1
- package/dist/src/utils/flex-layout.js +20 -6
- package/dist/src/utils/flex-layout.js.map +1 -1
- package/dist/src/utils/index.d.ts +2 -1
- package/dist/src/utils/index.d.ts.map +1 -1
- package/dist/src/utils/index.js +2 -1
- package/dist/src/utils/index.js.map +1 -1
- package/dist/src/utils/styles.d.ts +9 -0
- package/dist/src/utils/styles.d.ts.map +1 -1
- package/dist/src/utils/styles.js +9 -0
- package/dist/src/utils/styles.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -2
- package/src/dev.tsx +2 -2
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-quit.ts +33 -0
- package/src/hosts/base.ts +50 -6
- package/src/hosts/box.ts +3 -6
- package/src/hosts/flex-container.ts +3 -1
- package/src/hosts/overlay-item.ts +7 -22
- package/src/hosts/overlay.ts +3 -37
- package/src/hosts/scroll.ts +16 -7
- package/src/hosts/spacer.ts +11 -9
- package/src/hosts/text.ts +3 -3
- package/src/hosts/zstack.ts +5 -18
- package/src/reconciler/types.ts +9 -4
- package/src/remote/Procedures.ts +4 -0
- package/src/remote/Router.ts +7 -1
- package/src/remote/index.ts +41 -3
- package/src/renderer/input/InputProcessor.ts +6 -1
- package/src/renderer/lifecycle/TerminalSetup.ts +28 -17
- package/src/renderer/modes/FullscreenRenderer.ts +8 -6
- package/src/renderer/modes/InlineRenderer.ts +9 -7
- package/src/renderer.ts +44 -6
- package/src/test/render-tui.ts +7 -0
- package/src/utils/alignment.ts +50 -0
- package/src/utils/flex-layout.ts +19 -6
- package/src/utils/index.ts +10 -1
- package/src/utils/styles.ts +16 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-tui/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "React bindings for @effect-tui/core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"prepublishOnly": "bun run typecheck && bun run build"
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
|
-
"@effect-tui/core": "^0.
|
|
86
|
+
"@effect-tui/core": "^0.2.0",
|
|
87
87
|
"@effect/platform": "^0.94.0",
|
|
88
88
|
"@effect/platform-bun": "^0.87.0",
|
|
89
89
|
"@effect/rpc": "^0.73.0",
|
|
@@ -110,6 +110,7 @@
|
|
|
110
110
|
}
|
|
111
111
|
},
|
|
112
112
|
"devDependencies": {
|
|
113
|
+
"@effect-tui/emulator": "workspace:^",
|
|
113
114
|
"@effect/opentelemetry": "^0.60.0",
|
|
114
115
|
"@effect/vitest": "^0.27.0",
|
|
115
116
|
"@opentelemetry/api": "^1.9.0",
|
package/src/dev.tsx
CHANGED
|
@@ -395,8 +395,8 @@ export async function devRender(entryPath: string, options?: DevRenderOptions):
|
|
|
395
395
|
const renderer = createRenderer(options)
|
|
396
396
|
const root = createRoot(renderer)
|
|
397
397
|
|
|
398
|
-
// Dev mode always enables remote control
|
|
399
|
-
const stopRemote = enableRemote(renderer)
|
|
398
|
+
// Dev mode always enables remote control with entry path for identification
|
|
399
|
+
const stopRemote = enableRemote(renderer, { entryPath })
|
|
400
400
|
|
|
401
401
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
|
402
402
|
let version = 0
|
package/src/hooks/index.ts
CHANGED
|
@@ -3,5 +3,6 @@ export type { UseKeyboardOptions } from "./use-keyboard.js"
|
|
|
3
3
|
export { useMouse } from "./use-mouse.js"
|
|
4
4
|
export type { UseMouseOptions } from "./use-mouse.js"
|
|
5
5
|
export { usePaste } from "./use-paste.js"
|
|
6
|
+
export { useQuit } from "./use-quit.js"
|
|
6
7
|
export { useScroll } from "./use-scroll.js"
|
|
7
8
|
export type { UseScrollOptions, UseScrollReturn, ScrollState } from "./use-scroll.js"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useCallback } from "react"
|
|
2
|
+
import { useRenderer } from "../renderer-context.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook that returns a quit function for cleanly exiting the TUI application.
|
|
6
|
+
*
|
|
7
|
+
* This ensures terminal state is properly restored (alternate buffer exit,
|
|
8
|
+
* cursor shown, raw mode disabled) before the process exits.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* function App() {
|
|
13
|
+
* const quit = useQuit()
|
|
14
|
+
*
|
|
15
|
+
* useKeyboard((key) => {
|
|
16
|
+
* if (key.text === "q") quit()
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* return <text>Press q to quit</text>
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function useQuit(): (code?: number) => void {
|
|
24
|
+
const renderer = useRenderer()
|
|
25
|
+
|
|
26
|
+
return useCallback(
|
|
27
|
+
(code = 0) => {
|
|
28
|
+
renderer.stop()
|
|
29
|
+
process.exit(code)
|
|
30
|
+
},
|
|
31
|
+
[renderer],
|
|
32
|
+
)
|
|
33
|
+
}
|
package/src/hosts/base.ts
CHANGED
|
@@ -32,9 +32,11 @@ export abstract class BaseHost implements HostInstance {
|
|
|
32
32
|
children: HostInstance[] = []
|
|
33
33
|
rect: Rect | null = null
|
|
34
34
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
// Greedy layout - expands to fill remaining space
|
|
36
|
+
// undefined = not greedy (hug content)
|
|
37
|
+
// true or 1 = 1 share
|
|
38
|
+
// number > 1 = proportionally more shares
|
|
39
|
+
greedy: boolean | number | undefined = undefined
|
|
38
40
|
|
|
39
41
|
// ─────────────────────────────────────────────────────────────
|
|
40
42
|
// Frame constraints (like SwiftUI's .frame modifier)
|
|
@@ -65,7 +67,20 @@ export abstract class BaseHost implements HostInstance {
|
|
|
65
67
|
abstract render(buffer: CellBuffer, palette: Palette): void
|
|
66
68
|
|
|
67
69
|
layout(rect: Rect): void {
|
|
68
|
-
|
|
70
|
+
// Apply frame constraints to the assigned rect
|
|
71
|
+
let { w, h } = rect
|
|
72
|
+
|
|
73
|
+
// Fixed width/height override
|
|
74
|
+
if (this.frameWidth !== undefined) w = this.frameWidth
|
|
75
|
+
if (this.frameHeight !== undefined) h = this.frameHeight
|
|
76
|
+
|
|
77
|
+
// Apply min/max clamps
|
|
78
|
+
if (this.frameMinWidth !== undefined) w = Math.max(this.frameMinWidth, w)
|
|
79
|
+
if (this.frameMaxWidth !== undefined) w = Math.min(this.frameMaxWidth, w)
|
|
80
|
+
if (this.frameMinHeight !== undefined) h = Math.max(this.frameMinHeight, h)
|
|
81
|
+
if (this.frameMaxHeight !== undefined) h = Math.min(this.frameMaxHeight, h)
|
|
82
|
+
|
|
83
|
+
this.rect = { x: rect.x, y: rect.y, w, h }
|
|
69
84
|
}
|
|
70
85
|
|
|
71
86
|
/**
|
|
@@ -144,8 +159,23 @@ export abstract class BaseHost implements HostInstance {
|
|
|
144
159
|
}
|
|
145
160
|
|
|
146
161
|
updateProps(props: Record<string, unknown>): void {
|
|
147
|
-
|
|
148
|
-
|
|
162
|
+
// Greedy layout - reset to undefined unless explicitly set
|
|
163
|
+
// Subclasses like Spacer/Scroll set their own default before calling super
|
|
164
|
+
if ("greedy" in props) {
|
|
165
|
+
const greedy = props.greedy
|
|
166
|
+
if (greedy === true) {
|
|
167
|
+
this.greedy = 1
|
|
168
|
+
} else if (typeof greedy === "number") {
|
|
169
|
+
this.greedy = greedy
|
|
170
|
+
} else {
|
|
171
|
+
// greedy is false, undefined, or null
|
|
172
|
+
this.greedy = undefined
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
// No greedy prop - reset to default (undefined)
|
|
176
|
+
// Subclasses like Spacer/Scroll handle this before calling super
|
|
177
|
+
this.greedy = undefined
|
|
178
|
+
}
|
|
149
179
|
|
|
150
180
|
// Frame constraints - only accept valid numbers (ignore strings like "100%")
|
|
151
181
|
this.frameWidth = typeof props.width === "number" ? props.width : undefined
|
|
@@ -183,4 +213,18 @@ export abstract class BaseHost implements HostInstance {
|
|
|
183
213
|
}
|
|
184
214
|
child.parent = this
|
|
185
215
|
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Render a child with optional clipping to its rect.
|
|
219
|
+
* Common pattern used by containers (ZStack, Overlay, etc.)
|
|
220
|
+
*/
|
|
221
|
+
protected renderChildWithClip(child: HostInstance, buffer: CellBuffer, palette: Palette): void {
|
|
222
|
+
if (child.rect) {
|
|
223
|
+
buffer.withClip(child.rect.x, child.rect.y, child.rect.w, child.rect.h, () => {
|
|
224
|
+
child.render(buffer, palette)
|
|
225
|
+
})
|
|
226
|
+
} else {
|
|
227
|
+
child.render(buffer, palette)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
186
230
|
}
|
package/src/hosts/box.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { CellBuffer, Palette, Color } from "@effect-tui/core"
|
|
2
2
|
import { Colors } from "@effect-tui/core"
|
|
3
3
|
import type { HostContext, Rect, Size, CommonProps } from "../reconciler/types.js"
|
|
4
|
-
import { getInheritedBg } from "./base.js"
|
|
5
4
|
import { SingleChildHost } from "./single-child.js"
|
|
6
5
|
import {
|
|
7
6
|
type BorderKind,
|
|
@@ -10,7 +9,7 @@ import {
|
|
|
10
9
|
type Padding,
|
|
11
10
|
type PaddingInput,
|
|
12
11
|
resolvePadding,
|
|
13
|
-
|
|
12
|
+
resolveInheritedBgStyle,
|
|
14
13
|
toColorValue,
|
|
15
14
|
} from "../utils/index.js"
|
|
16
15
|
|
|
@@ -94,11 +93,9 @@ export class BoxHost extends SingleChildHost {
|
|
|
94
93
|
if (!this.rect) return
|
|
95
94
|
const { x, y, w, h } = this.rect
|
|
96
95
|
|
|
97
|
-
// If no explicit bg, inherit from parent (so child boxes are transparent over parents)
|
|
98
|
-
const rawBg: Color | undefined = this.bg ?? getInheritedBg(this.parent)
|
|
99
|
-
|
|
100
96
|
// Always clear our rect to ensure stale backgrounds are removed.
|
|
101
|
-
|
|
97
|
+
// If no explicit bg, inherit from parent (so child boxes are transparent over parents)
|
|
98
|
+
const { styleId: bgStyleId } = resolveInheritedBgStyle(palette, this.bg, this.parent)
|
|
102
99
|
buffer.fillRect(x, y, w, h, " ".codePointAt(0)!, bgStyleId)
|
|
103
100
|
|
|
104
101
|
// Draw border
|
|
@@ -84,12 +84,14 @@ export class FlexContainerHost<A extends FlexAxis> extends BaseHost {
|
|
|
84
84
|
|
|
85
85
|
override layout(rect: Rect): void {
|
|
86
86
|
super.layout(rect)
|
|
87
|
+
// Use this.rect (constrained) not rect (raw input)
|
|
88
|
+
if (!this.rect) return
|
|
87
89
|
const stretchCross = this.axis === "vertical" ? this.alignment === "leading" : this.alignment === "top"
|
|
88
90
|
layoutFlex(
|
|
89
91
|
this.axis,
|
|
90
92
|
this.layoutChildren,
|
|
91
93
|
this.cachedSizes,
|
|
92
|
-
rect,
|
|
94
|
+
this.rect,
|
|
93
95
|
this.spacing,
|
|
94
96
|
toFlexAlignment(this.axis, this.alignment),
|
|
95
97
|
stretchCross,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { CellBuffer, Palette, Rect, Size } from "@effect-tui/core"
|
|
9
9
|
import type { HostContext, CommonProps } from "../reconciler/types.js"
|
|
10
|
-
import {
|
|
10
|
+
import { SingleChildHost } from "./single-child.js"
|
|
11
11
|
|
|
12
12
|
type HAlign = "left" | "center" | "right"
|
|
13
13
|
type VAlign = "top" | "center" | "bottom"
|
|
@@ -17,7 +17,7 @@ export interface OverlayItemProps extends CommonProps {
|
|
|
17
17
|
alignment?: { h?: HAlign; v?: VAlign }
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export class OverlayItemHost extends
|
|
20
|
+
export class OverlayItemHost extends SingleChildHost {
|
|
21
21
|
/** Stored alignment for parent Overlay to read */
|
|
22
22
|
alignment: { h?: HAlign; v?: VAlign } = {}
|
|
23
23
|
|
|
@@ -29,37 +29,22 @@ export class OverlayItemHost extends BaseHost {
|
|
|
29
29
|
override measure(maxW: number, maxH: number): Size {
|
|
30
30
|
const constrained = this.constrainProposal(maxW, maxH)
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
const child = this.children[0]
|
|
34
|
-
if (!child) {
|
|
32
|
+
if (!this.child) {
|
|
35
33
|
return this.constrainResult({ w: 0, h: 0 })
|
|
36
34
|
}
|
|
37
35
|
|
|
38
|
-
const childSize = child.measure(constrained.w, constrained.h)
|
|
36
|
+
const childSize = this.child.measure(constrained.w, constrained.h)
|
|
39
37
|
return this.constrainResult(childSize)
|
|
40
38
|
}
|
|
41
39
|
|
|
42
40
|
override layout(rect: Rect): void {
|
|
43
41
|
this.rect = rect
|
|
44
|
-
|
|
45
|
-
// Pass rect to child
|
|
46
|
-
const child = this.children[0]
|
|
47
|
-
if (child) {
|
|
48
|
-
child.layout(rect)
|
|
49
|
-
}
|
|
42
|
+
this.child?.layout(rect)
|
|
50
43
|
}
|
|
51
44
|
|
|
52
45
|
override render(buffer: CellBuffer, palette: Palette): void {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (child) {
|
|
56
|
-
if (child.rect) {
|
|
57
|
-
buffer.withClip(child.rect.x, child.rect.y, child.rect.w, child.rect.h, () => {
|
|
58
|
-
child.render(buffer, palette)
|
|
59
|
-
})
|
|
60
|
-
} else {
|
|
61
|
-
child.render(buffer, palette)
|
|
62
|
-
}
|
|
46
|
+
if (this.child) {
|
|
47
|
+
this.renderChildWithClip(this.child, buffer, palette)
|
|
63
48
|
}
|
|
64
49
|
}
|
|
65
50
|
|
package/src/hosts/overlay.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import type { CellBuffer, Palette, Rect, Size } from "@effect-tui/core"
|
|
20
20
|
import type { HostContext, CommonProps } from "../reconciler/types.js"
|
|
21
21
|
import { BaseHost } from "./base.js"
|
|
22
|
+
import { alignInRect } from "../utils/index.js"
|
|
22
23
|
import type { OverlayItemHost } from "./overlay-item.js"
|
|
23
24
|
|
|
24
25
|
export interface OverlayProps extends CommonProps {}
|
|
@@ -70,36 +71,7 @@ export class OverlayHost extends BaseHost {
|
|
|
70
71
|
|
|
71
72
|
// Read alignment from OverlayItemHost (use type check, not instanceof, for bundler compatibility)
|
|
72
73
|
const alignment = child.type === "overlayItem" ? (child as OverlayItemHost).alignment : {}
|
|
73
|
-
const
|
|
74
|
-
const vAlign = alignment.v ?? "center"
|
|
75
|
-
|
|
76
|
-
// Calculate position based on alignment
|
|
77
|
-
let x = rect.x
|
|
78
|
-
let y = rect.y
|
|
79
|
-
|
|
80
|
-
switch (hAlign) {
|
|
81
|
-
case "left":
|
|
82
|
-
x = rect.x
|
|
83
|
-
break
|
|
84
|
-
case "center":
|
|
85
|
-
x = rect.x + Math.floor((rect.w - size.w) / 2)
|
|
86
|
-
break
|
|
87
|
-
case "right":
|
|
88
|
-
x = rect.x + rect.w - size.w
|
|
89
|
-
break
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
switch (vAlign) {
|
|
93
|
-
case "top":
|
|
94
|
-
y = rect.y
|
|
95
|
-
break
|
|
96
|
-
case "center":
|
|
97
|
-
y = rect.y + Math.floor((rect.h - size.h) / 2)
|
|
98
|
-
break
|
|
99
|
-
case "bottom":
|
|
100
|
-
y = rect.y + rect.h - size.h
|
|
101
|
-
break
|
|
102
|
-
}
|
|
74
|
+
const { x, y } = alignInRect(rect, size, alignment.h ?? "center", alignment.v ?? "center")
|
|
103
75
|
|
|
104
76
|
child.layout({
|
|
105
77
|
x,
|
|
@@ -113,13 +85,7 @@ export class OverlayHost extends BaseHost {
|
|
|
113
85
|
override render(buffer: CellBuffer, palette: Palette): void {
|
|
114
86
|
// Render all children in order (base first, then overlays)
|
|
115
87
|
for (const child of this.children) {
|
|
116
|
-
|
|
117
|
-
buffer.withClip(child.rect.x, child.rect.y, child.rect.w, child.rect.h, () => {
|
|
118
|
-
child.render(buffer, palette)
|
|
119
|
-
})
|
|
120
|
-
} else {
|
|
121
|
-
child.render(buffer, palette)
|
|
122
|
-
}
|
|
88
|
+
this.renderChildWithClip(child, buffer, palette)
|
|
123
89
|
}
|
|
124
90
|
}
|
|
125
91
|
}
|
package/src/hosts/scroll.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// scroll.ts — Scrollable container host
|
|
2
2
|
import type { CellBuffer, Palette, Color } from "@effect-tui/core"
|
|
3
3
|
import type { HostContext, Rect, Size, CommonProps } from "../reconciler/types.js"
|
|
4
|
-
import { getInheritedBg } from "./base.js"
|
|
5
4
|
import { SingleChildHost } from "./single-child.js"
|
|
6
|
-
import {
|
|
5
|
+
import { resolveInheritedBgStyle } from "../utils/index.js"
|
|
7
6
|
|
|
8
7
|
export interface ScrollProps extends CommonProps {
|
|
9
8
|
/** Scroll axis: "vertical" (default), "horizontal", or "both" */
|
|
@@ -39,6 +38,9 @@ export class ScrollHost extends SingleChildHost {
|
|
|
39
38
|
bg?: Color
|
|
40
39
|
showScrollbar = true
|
|
41
40
|
sticky = false
|
|
41
|
+
|
|
42
|
+
// Scroll is greedy by default - expands to fill available space
|
|
43
|
+
override greedy: boolean | number | undefined = 1
|
|
42
44
|
onContentSize?: (width: number, height: number) => void
|
|
43
45
|
onViewportSize?: (width: number, height: number) => void
|
|
44
46
|
onEffectiveOffset?: (offset: number) => void
|
|
@@ -86,8 +88,12 @@ export class ScrollHost extends SingleChildHost {
|
|
|
86
88
|
// Note: onContentSize callback is deferred to layout() to keep measure() pure
|
|
87
89
|
}
|
|
88
90
|
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
+
// Report natural content size (clamped to constrained bounds)
|
|
92
|
+
// Greedy expansion happens in layout phase via layoutFlex
|
|
93
|
+
const naturalW = Math.min(this.contentWidth, constrained.w)
|
|
94
|
+
const naturalH = Math.min(this.contentHeight, constrained.h)
|
|
95
|
+
|
|
96
|
+
return this.constrainResult({ w: naturalW, h: naturalH })
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
override layout(rect: Rect): void {
|
|
@@ -175,9 +181,8 @@ export class ScrollHost extends SingleChildHost {
|
|
|
175
181
|
if (!this.rect) return
|
|
176
182
|
const { x, y, w, h } = this.rect
|
|
177
183
|
|
|
178
|
-
// Fill background
|
|
179
|
-
const
|
|
180
|
-
const { value: bgValue, styleId: bgStyleId } = resolveBgStyle(palette, rawBg)
|
|
184
|
+
// Fill background (inherit from parent if not explicitly set)
|
|
185
|
+
const { value: bgValue, styleId: bgStyleId } = resolveInheritedBgStyle(palette, this.bg, this.parent)
|
|
181
186
|
if (bgValue !== undefined) {
|
|
182
187
|
buffer.fillRect(x, y, w, h, " ".codePointAt(0)!, bgStyleId)
|
|
183
188
|
}
|
|
@@ -241,6 +246,10 @@ export class ScrollHost extends SingleChildHost {
|
|
|
241
246
|
|
|
242
247
|
override updateProps(props: Record<string, unknown>): void {
|
|
243
248
|
super.updateProps(props)
|
|
249
|
+
// Scroll is greedy by default unless explicitly set to false
|
|
250
|
+
if (!("greedy" in props)) {
|
|
251
|
+
this.greedy = 1
|
|
252
|
+
}
|
|
244
253
|
if (props.axis !== undefined) this.axis = (props.axis as ScrollProps["axis"]) ?? "vertical"
|
|
245
254
|
if (props.offset !== undefined) this.offset = props.offset as number
|
|
246
255
|
if (props.offsetX !== undefined) this.offsetX = props.offsetX as number
|
package/src/hosts/spacer.ts
CHANGED
|
@@ -13,15 +13,16 @@ export class SpacerHost extends BaseHost {
|
|
|
13
13
|
minWidth = 0
|
|
14
14
|
minHeight = 0
|
|
15
15
|
|
|
16
|
+
// Spacers are greedy by default
|
|
17
|
+
override greedy: boolean | number | undefined = 1
|
|
18
|
+
|
|
16
19
|
constructor(props: SpacerProps, ctx: HostContext) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
super("spacer", propsWithDefaults, ctx)
|
|
20
|
-
this.updateProps(propsWithDefaults)
|
|
20
|
+
super("spacer", props, ctx)
|
|
21
|
+
this.updateProps(props)
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
measure(_maxW: number, _maxH: number): Size {
|
|
24
|
-
// Spacers have no natural size, they expand via
|
|
25
|
+
// Spacers have no natural size, they expand via greedy
|
|
25
26
|
return { w: this.minWidth, h: this.minHeight }
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -34,10 +35,11 @@ export class SpacerHost extends BaseHost {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
override updateProps(props: Record<string, unknown>): void {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
super.updateProps(props)
|
|
39
|
+
// Spacer is greedy by default unless explicitly set to false
|
|
40
|
+
if (!("greedy" in props)) {
|
|
41
|
+
this.greedy = 1
|
|
42
|
+
}
|
|
41
43
|
if (props.minWidth !== undefined) this.minWidth = props.minWidth as number
|
|
42
44
|
if (props.minHeight !== undefined) this.minHeight = props.minHeight as number
|
|
43
45
|
}
|
package/src/hosts/text.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { displayWidth, type CellBuffer, type Palette, type Color } from "@effect-tui/core"
|
|
2
2
|
import type { HostContext, Rect, Size, CommonProps } from "../reconciler/types.js"
|
|
3
3
|
import { BaseHost, getInheritedBg } from "./base.js"
|
|
4
|
-
import {
|
|
4
|
+
import { resolveInheritedBgStyle, styleIdFromProps } from "../utils/index.js"
|
|
5
5
|
|
|
6
6
|
export interface TextProps extends CommonProps {
|
|
7
7
|
fg?: Color
|
|
@@ -114,8 +114,8 @@ export class TextHost extends BaseHost {
|
|
|
114
114
|
if (!this.rect) return
|
|
115
115
|
|
|
116
116
|
// If text has no bg, inherit from parent box for proper highlight rendering
|
|
117
|
-
const
|
|
118
|
-
const
|
|
117
|
+
const { value: bgValue, styleId: bgStyleId } = resolveInheritedBgStyle(palette, this.bg, this.parent)
|
|
118
|
+
const inheritedBg = this.bg ?? getInheritedBg(this.parent)
|
|
119
119
|
|
|
120
120
|
const styleId = styleIdFromProps(palette, {
|
|
121
121
|
fg: this.fg,
|
package/src/hosts/zstack.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CellBuffer, Palette } from "@effect-tui/core"
|
|
2
2
|
import type { HostContext, Rect, Size, CommonProps } from "../reconciler/types.js"
|
|
3
3
|
import { BaseHost } from "./base.js"
|
|
4
|
+
import { alignInRect, type HAlign, type VAlign } from "../utils/index.js"
|
|
4
5
|
|
|
5
6
|
export interface ZStackProps extends CommonProps {
|
|
6
7
|
alignment?: { h?: "leading" | "center" | "trailing"; v?: "top" | "center" | "bottom" }
|
|
@@ -8,8 +9,8 @@ export interface ZStackProps extends CommonProps {
|
|
|
8
9
|
|
|
9
10
|
// Overlay children in the same rect, honoring alignment for each child.
|
|
10
11
|
export class ZStackHost extends BaseHost {
|
|
11
|
-
alignmentH:
|
|
12
|
-
alignmentV:
|
|
12
|
+
alignmentH: HAlign = "center"
|
|
13
|
+
alignmentV: VAlign = "center"
|
|
13
14
|
private cachedSizes: Size[] = []
|
|
14
15
|
|
|
15
16
|
constructor(props: ZStackProps, ctx: HostContext) {
|
|
@@ -47,15 +48,7 @@ export class ZStackHost extends BaseHost {
|
|
|
47
48
|
for (let i = 0; i < this.children.length; i++) {
|
|
48
49
|
const child = this.children[i]
|
|
49
50
|
const size = this.cachedSizes[i] ?? child.measure(rect.w, rect.h)
|
|
50
|
-
|
|
51
|
-
let x = rect.x
|
|
52
|
-
let y = rect.y
|
|
53
|
-
|
|
54
|
-
if (this.alignmentH === "center") x += Math.floor((rect.w - size.w) / 2)
|
|
55
|
-
else if (this.alignmentH === "trailing") x += Math.max(0, rect.w - size.w)
|
|
56
|
-
|
|
57
|
-
if (this.alignmentV === "center") y += Math.floor((rect.h - size.h) / 2)
|
|
58
|
-
else if (this.alignmentV === "bottom") y += Math.max(0, rect.h - size.h)
|
|
51
|
+
const { x, y } = alignInRect(rect, size, this.alignmentH, this.alignmentV)
|
|
59
52
|
|
|
60
53
|
child.layout({
|
|
61
54
|
x,
|
|
@@ -68,13 +61,7 @@ export class ZStackHost extends BaseHost {
|
|
|
68
61
|
|
|
69
62
|
render(buffer: CellBuffer, palette: Palette): void {
|
|
70
63
|
for (const child of this.children) {
|
|
71
|
-
|
|
72
|
-
buffer.withClip(child.rect.x, child.rect.y, child.rect.w, child.rect.h, () => {
|
|
73
|
-
child.render(buffer, palette)
|
|
74
|
-
})
|
|
75
|
-
} else {
|
|
76
|
-
child.render(buffer, palette)
|
|
77
|
-
}
|
|
64
|
+
this.renderChildWithClip(child, buffer, palette)
|
|
78
65
|
}
|
|
79
66
|
}
|
|
80
67
|
|
package/src/reconciler/types.ts
CHANGED
|
@@ -92,10 +92,15 @@ export interface HostContext {
|
|
|
92
92
|
* With the constraint, it reports exactly 10, so spacer gets the rest.
|
|
93
93
|
*/
|
|
94
94
|
export interface CommonProps {
|
|
95
|
-
/**
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Greedy layout - element expands to fill remaining space.
|
|
97
|
+
* - `true` or `1`: takes 1 share of remaining space
|
|
98
|
+
* - number > 1: takes proportionally more (e.g., `greedy={2}` gets 2x space)
|
|
99
|
+
* - `false` or undefined: hugs content (default)
|
|
100
|
+
*
|
|
101
|
+
* Scroll and Spacer are greedy by default.
|
|
102
|
+
*/
|
|
103
|
+
greedy?: boolean | number
|
|
99
104
|
|
|
100
105
|
// ─────────────────────────────────────────────────────────────
|
|
101
106
|
// Frame constraints (like SwiftUI's .frame modifier)
|
package/src/remote/Procedures.ts
CHANGED
|
@@ -39,6 +39,10 @@ const Info = Rpc.make("Info", {
|
|
|
39
39
|
pid: Schema.Number,
|
|
40
40
|
width: Schema.Number,
|
|
41
41
|
height: Schema.Number,
|
|
42
|
+
/** Entry file path (e.g., /Users/kit/code/my-app/src/tui.tsx) */
|
|
43
|
+
entryPath: Schema.optional(Schema.String),
|
|
44
|
+
/** Short name derived from entry path (e.g., my-app/src/tui.tsx) */
|
|
45
|
+
name: Schema.optional(Schema.String),
|
|
42
46
|
}),
|
|
43
47
|
})
|
|
44
48
|
|
package/src/remote/Router.ts
CHANGED
|
@@ -10,7 +10,13 @@ export interface TuiSessionImpl {
|
|
|
10
10
|
readonly dispatchKey: (key: KeyMsg) => void
|
|
11
11
|
readonly dispatchPaste: (text: string) => void
|
|
12
12
|
readonly dispatchResize: (width: number, height: number) => void
|
|
13
|
-
readonly getInfo: () => {
|
|
13
|
+
readonly getInfo: () => {
|
|
14
|
+
pid: number
|
|
15
|
+
width: number
|
|
16
|
+
height: number
|
|
17
|
+
entryPath?: string
|
|
18
|
+
name?: string
|
|
19
|
+
}
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
export class TuiSession extends Context.Tag("TuiSession")<
|
package/src/remote/index.ts
CHANGED
|
@@ -17,19 +17,55 @@ import type { TuiRenderer } from "../renderer-types.js"
|
|
|
17
17
|
import type { TuiSessionImpl } from "./Router.js"
|
|
18
18
|
import { makeServerLayer, getSocketPath } from "./Server.js"
|
|
19
19
|
|
|
20
|
+
export interface EnableRemoteOptions {
|
|
21
|
+
/** Custom socket path (defaults to /tmp/effect-tui-sessions/<pid>.sock) */
|
|
22
|
+
socketPath?: string
|
|
23
|
+
/** Entry file path for identification (e.g., import.meta.path) */
|
|
24
|
+
entryPath?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Derive a short name from the entry path.
|
|
29
|
+
* Uses the last directory + filename (e.g., "/Users/kit/code/my-app/src/tui.tsx" -> "my-app/src/tui.tsx")
|
|
30
|
+
*/
|
|
31
|
+
function deriveSessionName(entryPath: string): string {
|
|
32
|
+
const parts = entryPath.split("/")
|
|
33
|
+
// Find the last directory that looks like a project name (not src, lib, etc)
|
|
34
|
+
const commonDirs = ["src", "lib", "dist", "build", "packages"]
|
|
35
|
+
let srcIdx = -1
|
|
36
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
37
|
+
if (commonDirs.includes(parts[i])) {
|
|
38
|
+
srcIdx = i
|
|
39
|
+
break
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (srcIdx > 0) {
|
|
43
|
+
// Include project name + relative path from there
|
|
44
|
+
return parts.slice(srcIdx - 1).join("/")
|
|
45
|
+
}
|
|
46
|
+
// Fallback: last 3 path segments
|
|
47
|
+
return parts.slice(-3).join("/")
|
|
48
|
+
}
|
|
49
|
+
|
|
20
50
|
/**
|
|
21
51
|
* Enable remote control for a TUI renderer.
|
|
22
52
|
* Starts an RPC server on a Unix socket at /tmp/effect-tui-sessions/<pid>.sock
|
|
23
53
|
*
|
|
24
54
|
* @param renderer - The TUI renderer to control
|
|
25
|
-
* @param
|
|
55
|
+
* @param options - Socket path and entry path for session identification
|
|
26
56
|
* @returns A cleanup function to stop the server
|
|
27
57
|
*/
|
|
28
58
|
export function enableRemote(
|
|
29
59
|
renderer: TuiRenderer,
|
|
30
|
-
|
|
60
|
+
options?: EnableRemoteOptions | string,
|
|
31
61
|
): () => void {
|
|
32
|
-
|
|
62
|
+
// Support old API: enableRemote(renderer, socketPath?)
|
|
63
|
+
const opts: EnableRemoteOptions =
|
|
64
|
+
typeof options === "string" ? { socketPath: options } : options ?? {}
|
|
65
|
+
const actualPath = opts.socketPath ?? getSocketPath()
|
|
66
|
+
|
|
67
|
+
// Derive session name from entry path
|
|
68
|
+
const name = opts.entryPath ? deriveSessionName(opts.entryPath) : undefined
|
|
33
69
|
|
|
34
70
|
// Create session implementation from renderer
|
|
35
71
|
const session: TuiSessionImpl = {
|
|
@@ -41,6 +77,8 @@ export function enableRemote(
|
|
|
41
77
|
pid: process.pid,
|
|
42
78
|
width: renderer.width,
|
|
43
79
|
height: renderer.height,
|
|
80
|
+
entryPath: opts.entryPath,
|
|
81
|
+
name,
|
|
44
82
|
}),
|
|
45
83
|
}
|
|
46
84
|
|
|
@@ -7,6 +7,7 @@ export interface InputProcessorConfig {
|
|
|
7
7
|
dispatchPaste: (text: string) => void
|
|
8
8
|
flushSync: <T>(fn: () => T) => T
|
|
9
9
|
onInputProcessed: () => void
|
|
10
|
+
onQuit?: () => void // Called instead of process.exit() for proper cleanup
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -86,7 +87,11 @@ export class InputProcessor {
|
|
|
86
87
|
// Default Ctrl+C handling - exit unless user called preventDefault()
|
|
87
88
|
// Only match Ctrl+C (not Ctrl+Shift+C which should be available for copy)
|
|
88
89
|
if (this.config.exitOnCtrlC && !wrapped.defaultPrevented && event.ctrl && !event.shift && event.text === "c") {
|
|
89
|
-
|
|
90
|
+
if (this.config.onQuit) {
|
|
91
|
+
this.config.onQuit()
|
|
92
|
+
} else {
|
|
93
|
+
process.exit(0)
|
|
94
|
+
}
|
|
90
95
|
}
|
|
91
96
|
}
|
|
92
97
|
}
|