Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 29 additions & 9 deletions packages/core/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export interface CliRendererConfig {
stdin?: NodeJS.ReadStream
stdout?: NodeJS.WriteStream
exitOnCtrlC?: boolean
exitSignals?: NodeJS.Signals[]
debounceDelay?: number
targetFps?: number
memorySnapshotInterval?: number
Expand Down Expand Up @@ -143,14 +144,6 @@ export enum MouseButton {
WHEEL_DOWN = 5,
}

singleton("ProcessExitSignals", () => {
;["SIGINT", "SIGTERM", "SIGQUIT", "SIGABRT"].forEach((signal) => {
process.on(signal, () => {
process.exit()
})
})
})

const rendererTracker = singleton("RendererTracker", () => {
const renderers = new Set<CliRenderer>()
return {
Expand Down Expand Up @@ -227,6 +220,8 @@ export class CliRenderer extends EventEmitter implements RenderContext {
public stdin: NodeJS.ReadStream
private stdout: NodeJS.WriteStream
private exitOnCtrlC: boolean
private exitSignals: NodeJS.Signals[]
private isExitSignalHandlerMounted: boolean = false
private _isDestroyed: boolean = false
public nextRenderBuffer: OptimizedBuffer
public currentRenderBuffer: OptimizedBuffer
Expand Down Expand Up @@ -380,6 +375,10 @@ export class CliRenderer extends EventEmitter implements RenderContext {
console.warn(JSON.stringify(warning.message, null, 2))
}).bind(this)

private exitSignalHandler: (signal: NodeJS.Signals) => void = (() => {
this.destroy()
}).bind(this)

public get controlState(): RendererControlState {
return this._controlState
}
Expand Down Expand Up @@ -417,6 +416,7 @@ export class CliRenderer extends EventEmitter implements RenderContext {

this.rendererPtr = rendererPtr
this.exitOnCtrlC = config.exitOnCtrlC === undefined ? true : config.exitOnCtrlC
this.exitSignals = config.exitSignals ?? ["SIGABRT", "SIGINT", "SIGTERM", "SIGQUIT"]
this.resizeDebounceDelay = config.debounceDelay || 100
this.targetFps = config.targetFps || 30
this.memorySnapshotInterval = config.memorySnapshotInterval ?? 0
Expand Down Expand Up @@ -447,7 +447,7 @@ export class CliRenderer extends EventEmitter implements RenderContext {

process.on("uncaughtException", this.handleError)
process.on("unhandledRejection", this.handleError)
process.on("exit", this.exitHandler)
process.on("beforeExit", this.exitHandler)

this._keyHandler = new InternalKeyHandler(config.useKittyKeyboard ?? true)
this._keyHandler.on("keypress", (event) => {
Expand All @@ -459,6 +459,8 @@ export class CliRenderer extends EventEmitter implements RenderContext {
}
})

this.mountExitSignals()

this._stdinBuffer = new StdinBuffer({ timeout: 5 })

this._console = new TerminalConsole(this, config.consoleOptions)
Expand Down Expand Up @@ -1252,6 +1254,20 @@ export class CliRenderer extends EventEmitter implements RenderContext {
this._controlState = this._isRunning ? RendererControlState.AUTO_STARTED : RendererControlState.IDLE
}

private mountExitSignals(): void {
if (!this.isExitSignalHandlerMounted && this.exitSignals.length > 0) {
this.exitSignals.forEach((signal) => process.addListener(signal, this.exitSignalHandler))
this.isExitSignalHandlerMounted = true
}
}

private unmountExitSignals(): void {
if (this.isExitSignalHandlerMounted && this.exitSignals.length > 0) {
this.exitSignals.forEach((signal) => process.removeListener(signal, this.exitSignalHandler))
this.isExitSignalHandlerMounted = false
}
}

private internalStart(): void {
if (!this._isRunning && !this._isDestroyed) {
this._isRunning = true
Expand Down Expand Up @@ -1279,6 +1295,7 @@ export class CliRenderer extends EventEmitter implements RenderContext {

this.disableMouse()
this._keyHandler.suspend()
this.unmountExitSignals()
this._stdinBuffer.clear()
if (this.stdin.setRawMode) {
this.stdin.setRawMode(false)
Expand All @@ -1292,6 +1309,7 @@ export class CliRenderer extends EventEmitter implements RenderContext {
}
this.stdin.resume()
this._keyHandler.resume()
this.mountExitSignals()

if (this._suspendedMouseEnabled) {
this.enableMouse()
Expand Down Expand Up @@ -1339,6 +1357,8 @@ export class CliRenderer extends EventEmitter implements RenderContext {
process.removeListener("warning", this.warningHandler)
capture.removeListener("write", this.captureCallback)

this.unmountExitSignals()

if (this.memorySnapshotTimer) {
clearInterval(this.memorySnapshotTimer)
}
Expand Down
16 changes: 6 additions & 10 deletions packages/react/src/reconciler/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,13 @@ import { _render } from "./reconciler"
/**
* @deprecated Use `createRoot(renderer).render(node)` instead
*/
export async function render(node: ReactNode, rendererConfig: CliRendererConfig = {}): Promise<void> {
export async function render(
node: ReactNode,
rendererConfig: CliRendererConfig = {},
): Promise<{ renderer: CliRenderer }> {
const renderer = await createCliRenderer(rendererConfig)
engine.attach(renderer)
_render(
React.createElement(
AppContext.Provider,
{ value: { keyHandler: renderer.keyInput, renderer } },
React.createElement(ErrorBoundary, null, node),
),
renderer.root,
)
createRoot(renderer).render(node)
return { renderer }
}

/**
Expand Down
4 changes: 3 additions & 1 deletion packages/solid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ preload = ["@opentui/solid/preload"]
3. Add render function to index.tsx:

```tsx
import { createCliRenderer } from "@opentui/core"
import { render } from "@opentui/solid"

render(() => <text>Hello, World!</text>)
const renderer = await createCliRenderer()
createRoot(renderer).render(() => <text>Hello, World!</text>)
```

4. Run with `bun index.tsx`.
11 changes: 6 additions & 5 deletions packages/solid/examples/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { render } from "@opentui/solid"
import { ConsolePosition } from "@opentui/core"
import { createRoot } from "@opentui/solid"
import { ConsolePosition, createCliRenderer } from "@opentui/core"
import ExampleSelector from "./components/ExampleSelector"

// Uncomment to debug solidjs reconciler
// process.env.DEBUG = "true"

const App = () => <ExampleSelector />

render(App, {
const renderer = await createCliRenderer({
targetFps: 30,
consoleOptions: {
position: ConsolePosition.BOTTOM,
maxStoredLogs: 1000,
sizePercent: 40,
},
})

const App = () => <ExampleSelector />
createRoot(renderer).render(App)
7 changes: 4 additions & 3 deletions packages/solid/examples/repro-empty-styled-text.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createSignal, Show } from "solid-js"
import { render, useKeyboard, useRenderer } from "@opentui/solid"
import { t } from "@opentui/core"
import { createRoot, useKeyboard, useRenderer } from "@opentui/solid"
import { createCliRenderer, t } from "@opentui/core"

process.env.DEBUG = "true"

Expand Down Expand Up @@ -42,4 +42,5 @@ const EmptyStyledTextTest = () => {
)
}

render(EmptyStyledTextTest)
const renderer = await createCliRenderer()
createRoot(renderer).render(EmptyStyledTextTest)
6 changes: 4 additions & 2 deletions packages/solid/examples/repro-filter-list.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createSignal, For } from "solid-js"
import { render, useRenderer } from "@opentui/solid"
import { createRoot, useRenderer } from "@opentui/solid"
import { createCliRenderer } from "@opentui/core"

process.env.DEBUG = "true"

Expand Down Expand Up @@ -37,4 +38,5 @@ const FilterListTest = () => {
)
}

render(FilterListTest)
const renderer = await createCliRenderer()
createRoot(renderer).render(FilterListTest)
6 changes: 4 additions & 2 deletions packages/solid/examples/repro-onSubmit.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createSignal, Match, Show, Switch } from "solid-js"
import { render, useKeyboard, useRenderer } from "@opentui/solid"
import { createRoot, useKeyboard, useRenderer } from "@opentui/solid"
import { createCliRenderer } from "@opentui/core"

process.env.DEBUG = "true"

Expand Down Expand Up @@ -67,4 +68,5 @@ const InputTest = () => {
)
}

render(InputTest)
const renderer = await createCliRenderer()
createRoot(renderer).render(InputTest)
55 changes: 40 additions & 15 deletions packages/solid/index.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,50 @@
import { createCliRenderer, engine, type CliRendererConfig } from "@opentui/core"
import { createCliRenderer, engine, type CliRendererConfig, type CliRenderer } from "@opentui/core"
import { createTestRenderer, type TestRendererOptions } from "@opentui/core/testing"
import type { JSX } from "./jsx-runtime"
import { RendererContext } from "./src/elements"
import { _render as renderInternal, createComponent } from "./src/reconciler"

export const render = async (node: () => JSX.Element, renderConfig: CliRendererConfig = {}) => {
/**
* @deprecated Use `createRoot(renderer).render(() => element)` instead
*/
export const render = async (
node: () => JSX.Element,
renderConfig: CliRendererConfig = {},
): Promise<{ renderer: CliRenderer }> => {
const renderer = await createCliRenderer(renderConfig)
engine.attach(renderer)
createRoot(renderer).render(node)

renderInternal(
() =>
createComponent(RendererContext.Provider, {
get value() {
return renderer
},
get children() {
return createComponent(node, {})
},
}),
renderer.root,
)
return { renderer }
}

/**
* Creates a root for rendering a Solid tree with the given CLI renderer.
* @param renderer The CLI renderer to use
* @returns A root object with a `render` method
* @example
* ```tsx
* const renderer = await createCliRenderer()
* createRoot(renderer).render(() => <App />)
* ```
*/
export const createRoot = (renderer: CliRenderer): { render: (node: () => JSX.Element) => void } => {
return {
render: (node) => {
engine.attach(renderer)
renderInternal(
() =>
createComponent(RendererContext.Provider, {
get value() {
return renderer
},
get children() {
return createComponent(node, {})
},
}),
renderer.root,
)
},
}
}

export const testRender = async (node: () => JSX.Element, renderConfig: TestRendererOptions = {}) => {
Expand Down
Loading