OpenTUI 0.5.0
This release adds image rendering to the renderer.
Added
- core:
ImageRenderableshows PNG, JPEG, WebP, first-frame GIF, and raw RGBA images. It loads from a path, URL,Blob,Response, or bytes withsource. Thefitoption accepts"fit","cover", or"fill". Theprotocoloption accepts"auto","kitty","sixel", or"blocks"."auto"picks the protocol from the terminal capabilities. See Image. (#1283) - core:
NativeImagedecodes and edits RGBA8 images. It providesNativeImage.decode(),NativeImage.load(),NativeImage.fromRgba(),resize(),extract(),extend(),rotate(),flip(),flop(),composite(),raw(),takeRaw(), andcopyTo().imageInfo()reads metadata without a full decode.OptimizedBuffer.drawImage()draws an image into a buffer. Failures throwImageErrororImageLoadError. See NativeImage. (#1283) - react: The
<image>element renders anImageRenderable. It is typed byImageProps. See React bindings. (#1283) - solid: The
<image>element renders anImageRenderable. It is typed byImageProps. See Solid bindings. (#1283) - core: Audio input capture.
Audio.openCapture()returns anAudioCaptureStreamwith areadablestream ofFloat32Arraychunks.Audio.recordToFile()returns anAudioRecorderthat writes a WAV file.Audio.listCaptureDevices(),Audio.selectCaptureDevice(), andAudio.clearCaptureDeviceSelection()manage input devices.Audio.startCapture(),Audio.readCaptureFrames(),Audio.getCaptureStats(), andAudio.stopCapture()give low-level access. See Audio. (#1297) - core:
CliRenderEvents.RENDER_ERRORfires with{ error, renderable }when the frame loop throws.renderableis the renderable that threw, if known. Errors that no listener handles use the renderer’s existing error policy. See Renderer. (#1314) - core:
CliRenderEvents.HANDLER_ERRORfires with{ error, event }when a mouse handler throws. The newMouseEvent.currentTargetis the renderable whose handler is running. See Renderer. (#1313) - core:
OTUI_STDIN_LOGwrites the raw stdin bytes to a file before they are parsed. The renderer truncates the file at start. The log can contain sensitive input and is for debugging only. See Environment variables. (#1274)
Changed
- core: Cursor reads, text measurement, audio stats, and span-list decoding use less FFI overhead. On Node, layout reads are also faster. The shape returned by
TextBufferView.measureForDimensions()is now the exported typeMeasureResult. (#1284, #1273)
Fixed
- core: An immediate render during a live render loop no longer starts a second frame timer. Repeated immediate renders could multiply the timers and render far above
targetFpsandmaxFps. See Renderer. (#1305) - core: The renderer keeps at most one terminal pixel-resolution query in flight. It combines resize and suspended refreshes into one follow-up query. Replies that arrive across suspend and resume no longer leak into later input. The renderer rejects pixel dimensions that are unsafe or overflow a signed 32-bit integer. See Renderer. (#1306)
- core: On Windows, the renderer writes console output with
WriteConsoleW. UTF-8 output no longer depends on the active console code page. See Renderer. (#1272) - core: A
type: "file"import that is also a build entry point no longer fails when its default export is not a path string. The runtime uses the source path instead. See Standalone executables. (#1293) - react: The package imports
react-reconciler/constantswith an explicit.jsextension. Strict Node ESM resolution no longer fails withERR_MODULE_NOT_FOUND. See React bindings. (#1316) - three: The package loads Jimp only when it loads a texture or exports a screenshot. Scenes without assets load under Bun’s
browsercondition, where Jimp 1.6.1 failed to import. See Three.js WebGPU. (#1312)
API changes#
@opentui/core#
Added
class AudioCaptureStream extends EventEmitter<AudioCaptureStreamEvents>class AudioCaptureStreamError extends Errorclass AudioRecorder extends EventEmitter<AudioRecorderEvents>class AudioRecorderError extends Errorclass ImageError extends Errorclass ImageLoadError extends Errorclass ImageRenderable extends Renderableclass NativeImageconstructor AudioCaptureStreamError(message: string, context: AudioCaptureStreamErrorContext, cause?: unknown)constructor AudioRecorderError(message: string, context: AudioRecorderErrorContext, cause?: unknown)constructor ImageError(status: number)constructor ImageLoadError(code: ImageLoadErrorCode, source: string, message: string, options?: { cause?: unknown; status?: number; })constructor ImageRenderable(ctx: RenderContext, options: ImageRenderableOptions)enum-member CliRenderEvents.HANDLER_ERROR = "handler:error"enum-member CliRenderEvents.RENDER_ERROR = "render:error"function imageInfo(data: Uint8Array | ArrayBuffer): ImageInfofunction resolveImageRenderProtocol(requested: ImageRenderProtocol, capabilities: TerminalCapabilities | null, hasResolution: boolean): Exclude<ImageRenderProtocol, "auto">interface AudioCaptureDeviceinterface AudioCaptureOptionsinterface AudioCaptureReadResultinterface AudioCaptureStatsinterface AudioCaptureStreamErrorContextinterface AudioCaptureStreamEventsinterface AudioCaptureStreamOptions extends AudioCaptureOptionsinterface AudioCaptureStreamStats extends AudioCaptureStatsinterface AudioRecorderErrorContextinterface AudioRecorderEventsinterface AudioRecorderStats extends AudioCaptureStatsinterface CliRendererErrorEventinterface CliRendererHandlerErrorEventinterface CompositeOptionsinterface ExtendOptionsinterface ExtractOptionsinterface ImageInfointerface ImageLoadOptionsinterface ImageRenderableOptions extends RenderableOptions<ImageRenderable>interface MeasureResultinterface OwnedRawImage extends RawImageinterface RawImageinterface ResizeOptionsmethod Audio.clearCaptureDeviceSelection(): voidmethod Audio.getCaptureStats(): AudioCaptureStats | nullmethod Audio.isCapturing(): booleanmethod Audio.listCaptureDevices(): AudioCaptureDevice[] | nullmethod Audio.openCapture(options?: AudioCaptureStreamOptions): Promise<AudioCaptureStream>method Audio.readCaptureFrames(frameCount: number): AudioCaptureReadResult | nullmethod Audio.recordToFile(filePath: string, options?: AudioRecordToFileOptions): Promise<AudioRecorder>method Audio.selectCaptureDevice(index: number): booleanmethod Audio.startCapture(options?: AudioCaptureOptions): booleanmethod Audio.stopCapture(): booleanmethod OptimizedBuffer.drawImage(image: NativeImage, x: number, y: number, width: number, height: number, pixelWidth?: number, pixelHeight?: number, sourceX?: number, sourceY?: number, sourceWidth?: number, sourceHeight?: number, protocol?: ImageRenderProtocol): booleanmethod RootRenderable.takeCurrentRenderable(): Renderable | undefinedmethod StdinParser.hasPendingPixelResolutionResponse(): booleanmethod StdinParser.pausePendingTimeout(): voidmethod StdinParser.resumePendingTimeout(): voidproperty AudioEngineLib.audioClearCaptureDeviceSelection: (engine: AudioEngineHandle) => voidproperty AudioEngineLib.audioGetCaptureDeviceCount: (engine: AudioEngineHandle) => numberproperty AudioEngineLib.audioGetCaptureDeviceName: (engine: AudioEngineHandle, index: number) => stringproperty AudioEngineLib.audioGetCaptureStats: (engine: AudioEngineHandle) => { status: number; stats: NativeAudioCaptureStats | null; }property AudioEngineLib.audioIsCaptureDeviceDefault: (engine: AudioEngineHandle, index: number) => booleanproperty AudioEngineLib.audioIsCaptureRunning: (engine: AudioEngineHandle) => booleanproperty AudioEngineLib.audioReadCapture: (engine: AudioEngineHandle, outBuffer: Float32Array, frameCount: number) => { status: number; framesRead: number; }property AudioEngineLib.audioRefreshCaptureDevices: (engine: AudioEngineHandle) => numberproperty AudioEngineLib.audioSelectCaptureDevice: (engine: AudioEngineHandle, index: number) => numberproperty AudioEngineLib.audioStartCapture: (engine: AudioEngineHandle, options: AudioStartOptions | undefined, channels: number, capacityFrames: number) => numberproperty AudioEngineLib.audioStopCapture: (engine: AudioEngineHandle) => numberproperty AudioEvents.captureStarted: [ ]property AudioEvents.captureStopped: [ ]property EnvVarConfig.required?: booleanreadonly property MouseEvent.currentTarget: Renderable | nullproperty RenderContext.resolution?: { width: number; height: number; } | nullproperty RenderContext.terminalHeight?: numberproperty RenderContext.terminalWidth?: numberproperty RenderLib.bufferDrawImage: (buffer: OptimizedBufferHandle, image: ImageHandle, x: number, y: number, width: number, height: number, pixelWidth: number, pixelHeight: number, sourceX: number, sourceY: number, sourceWidth: number, sourceHeight: number, protocol: ImageRenderProtocol) => booleanproperty RenderLib.imageClone: (image: ImageHandle) => { status: number; handle: ImageHandle | null; }property RenderLib.imageComposite: (base: ImageHandle, overlay: ImageHandle, left: number, top: number, blend: number, opacity: number) => { status: number; handle: ImageHandle | null; }property RenderLib.imageCopyPixels: (image: ImageHandle, destination: Uint8Array, stride: number, bgra: boolean) => numberproperty RenderLib.imageCreateFromRgba: (pixels: Uint8Array, width: number, height: number, stride: number) => { status: number; handle: ImageHandle | null; }property RenderLib.imageDecode: (data: Uint8Array) => { status: number; handle: ImageHandle | null; }property RenderLib.imageDestroy: (image: ImageHandle) => voidproperty RenderLib.imageExtend: (image: ImageHandle, top: number, right: number, bottom: number, left: number, background: Uint8Array) => { status: number; handle: ImageHandle | null; }property RenderLib.imageExtract: (image: ImageHandle, left: number, top: number, width: number, height: number) => { status: number; handle: ImageHandle | null; }property RenderLib.imageGetInfo: (image: ImageHandle) => { status: number; info: NativeImageInfo; }property RenderLib.imageGetPixelsPtr: (image: ImageHandle) => Pointer | nullproperty RenderLib.imageInfo: (data: Uint8Array) => { status: number; info: NativeImageInfo; }property RenderLib.imageResize: (image: ImageHandle, width: number, height: number, filter: number) => { status: number; handle: ImageHandle | null; }property RenderLib.imageTransform: (image: ImageHandle, operation: number) => { status: number; handle: ImageHandle | null; }readonly property RootRenderable.currentRenderable: Renderable | undefinedproperty TerminalCapabilities.image_protocol?: ImageRenderProtocoltype AudioCaptureStreamAction = "start" | "read" | "stop" | "stats" | "destroy"type AudioCaptureStreamState = "initializing" | "capturing" | "stopping" | "stopped" | "errored" | "disposed"type AudioRecordToFileOptions = AudioCaptureStreamOptionstype AudioRecorderAction = "open" | "start" | "read" | "write" | "stop" | "finalize" | "publish" | "stats" | "destroy"type AudioRecorderState = "initializing" | "recording" | "stopping" | "stopped" | "errored" | "disposed"type BlendMode = "source-over" | "source" | "destination-over"type ImageColorStatus = "assumed-srgb" | "explicit-srgb"type ImageErrorCode = "invalid-handle" | "unsupported-format" | "unsupported-color-space" | "malformed-data" | "dimension-limit" | "memory-limit" | "invalid-argument" | "out-of-memory" | "output-too-small" | "internal-error" | "unsupported-feature"type ImageFit = "fit" | "cover" | "fill"type ImageFormat = "png" | "raw-rgba" | "jpeg" | "webp" | "gif"type ImageHandle = NativeHandle<"image">type ImageLoadErrorCode = "file-read" | "network" | "http-status" | "unsupported-url-scheme"type ImageRenderProtocol = "auto" | "kitty" | "sixel" | "blocks"type ImageSource = string | URL | Uint8Array | ArrayBuffer | Blob | Responsetype NativeAudioCaptureStats = { framesReceived: bigint; framesRead: bigint; framesDropped: bigint; sampleRate: number; channels: number; bufferedFrames: number; capacityFrames: number; }type PixelFormat = "rgba8" | "bgra8"type ResizeKernel = "default" | "area" | "triangle" | "cubic-bspline" | "catmull-rom" | "mitchell" | "nearest"
Changed
method TextBufferView.measureForDimensions(width: number, height: number): { lineCount: number; widthColsMax: number; } | nullmethod TextBufferView.measureForDimensions(width: number, height: number): MeasureResult | nullproperty RenderLib.textBufferViewMeasureForDimensions: (view: TextBufferViewHandle, width: number, height: number) => { lineCount: number; widthColsMax: number; } | nullproperty RenderLib.textBufferViewMeasureForDimensions: (view: TextBufferViewHandle, width: number, height: number) => MeasureResult | nulltype AudioAction = "createAudioEngine" | "start" | "startMixer" | "stop" | "loadSound" | "loadSoundFile" | "unloadSound" | "group" | "play" | "stopVoice" | "setVoiceGroup" | "setGroupVolume" | "setMasterVolume" | "mixFrames" | "enableTap" | "readTapFrames" | "listPlaybackDevices" | "selectPlaybackDevice" | "clearPlaybackDeviceSelection" | "getStats"type AudioAction = "createAudioEngine" | "start" | "startMixer" | "stop" | "loadSound" | "loadSoundFile" | "unloadSound" | "group" | "play" | "stopVoice" | "setVoiceGroup" | "setGroupVolume" | "setMasterVolume" | "mixFrames" | "enableTap" | "readTapFrames" | "listPlaybackDevices" | "selectPlaybackDevice" | "clearPlaybackDeviceSelection" | "listCaptureDevices" | "selectCaptureDevice" | "clearCaptureDeviceSelection" | "startCapture" | "readCaptureFrames" | "getCaptureStats" | "stopCapture" | "getStats"
@opentui/react#
Added
type ImageProps = ComponentProps<ImageRenderableOptions, ImageRenderable>
Changed
const baseComponents: { box: typeof BoxRenderable; text: typeof TextRenderable; code: typeof CodeRenderable; diff: typeof DiffRenderable; markdown: typeof MarkdownRenderable; input: typeof InputRenderable; select: typeof SelectRenderable; textarea: typeof TextareaRenderable; scrollbox: typeof ScrollBoxRenderable; "ascii-font": typeof ASCIIFontRenderable; "tab-select": typeof TabSelectRenderable; "line-number": typeof LineNumberRenderable; span: typeof SpanRenderable; br: typeof LineBreakRenderable; b: typeof BoldSpanRenderable; strong: typeof BoldSpanRenderable; i: typeof ItalicSpanRenderable; em: typeof ItalicSpanRenderable; u: typeof UnderlineSpanRenderable; a: typeof LinkRenderable; }const baseComponents: { box: typeof BoxRenderable; text: typeof TextRenderable; code: typeof CodeRenderable; diff: typeof DiffRenderable; markdown: typeof MarkdownRenderable; input: typeof InputRenderable; select: typeof SelectRenderable; textarea: typeof TextareaRenderable; scrollbox: typeof ScrollBoxRenderable; "ascii-font": typeof ASCIIFontRenderable; "tab-select": typeof TabSelectRenderable; "line-number": typeof LineNumberRenderable; image: typeof ImageRenderable; span: typeof SpanRenderable; br: typeof LineBreakRenderable; b: typeof BoldSpanRenderable; strong: typeof BoldSpanRenderable; i: typeof ItalicSpanRenderable; em: typeof ItalicSpanRenderable; u: typeof UnderlineSpanRenderable; a: typeof LinkRenderable; }type GetNonStyledProperties<TConstructor> = TConstructor extends RenderableConstructor<TextRenderable> ? NonStyledProps | "content" : TConstructor extends RenderableConstructor<BoxRenderable> ? NonStyledProps | "title" | "bottomTitle" : TConstructor extends RenderableConstructor<ASCIIFontRenderable> ? NonStyledProps | "text" | "selectable" : TConstructor extends RenderableConstructor<InputRenderable> ? NonStyledProps | "placeholder" | "value" : TConstructor extends RenderableConstructor<TextareaRenderable> ? NonStyledProps | "placeholder" | "initialValue" : TConstructor extends RenderableConstructor<CodeRenderable> ? NonStyledProps | "content" | "filetype" | "syntaxStyle" | "treeSitterClient" | "conceal" | "drawUnstyledText" : TConstructor extends RenderableConstructor<MarkdownRenderable> ? NonStyledProps | "content" | "syntaxStyle" | "treeSitterClient" | "conceal" | "renderNode" : NonStyledPropstype GetNonStyledProperties<TConstructor> = TConstructor extends RenderableConstructor<TextRenderable> ? NonStyledProps | "content" : TConstructor extends RenderableConstructor<BoxRenderable> ? NonStyledProps | "title" | "bottomTitle" : TConstructor extends RenderableConstructor<ASCIIFontRenderable> ? NonStyledProps | "text" | "selectable" : TConstructor extends RenderableConstructor<InputRenderable> ? NonStyledProps | "placeholder" | "value" : TConstructor extends RenderableConstructor<TextareaRenderable> ? NonStyledProps | "placeholder" | "initialValue" : TConstructor extends RenderableConstructor<CodeRenderable> ? NonStyledProps | "content" | "filetype" | "syntaxStyle" | "treeSitterClient" | "conceal" | "drawUnstyledText" : TConstructor extends RenderableConstructor<MarkdownRenderable> ? NonStyledProps | "content" | "syntaxStyle" | "treeSitterClient" | "conceal" | "renderNode" : TConstructor extends RenderableConstructor<ImageRenderable> ? NonStyledProps | "source" : NonStyledProps
@opentui/react/jsx-dev-runtime#
Added
property JSX.IntrinsicElements.image: ImageProps
@opentui/react/jsx-runtime#
Added
property JSX.IntrinsicElements.image: ImageProps
@opentui/solid#
Added
property JSX.IntrinsicElements.image: ImagePropstype ImageProps = ComponentProps<ImageRenderableOptions, ImageRenderable>
Changed
const baseComponents: { box: typeof BoxRenderable; text: typeof TextRenderable; input: typeof InputRenderable; select: typeof SelectRenderable; textarea: typeof TextareaRenderable; ascii_font: typeof ASCIIFontRenderable; tab_select: typeof TabSelectRenderable; scrollbox: typeof ScrollBoxRenderable; code: typeof CodeRenderable; diff: typeof DiffRenderable; line_number: typeof LineNumberRenderable; markdown: typeof MarkdownRenderable; span: typeof SpanRenderable; strong: typeof BoldSpanRenderable; b: typeof BoldSpanRenderable; em: typeof ItalicSpanRenderable; i: typeof ItalicSpanRenderable; u: typeof UnderlineSpanRenderable; br: typeof LineBreakRenderable; a: typeof LinkRenderable; }const baseComponents: { box: typeof BoxRenderable; text: typeof TextRenderable; input: typeof InputRenderable; select: typeof SelectRenderable; textarea: typeof TextareaRenderable; ascii_font: typeof ASCIIFontRenderable; tab_select: typeof TabSelectRenderable; scrollbox: typeof ScrollBoxRenderable; code: typeof CodeRenderable; diff: typeof DiffRenderable; line_number: typeof LineNumberRenderable; markdown: typeof MarkdownRenderable; image: typeof ImageRenderable; span: typeof SpanRenderable; strong: typeof BoldSpanRenderable; b: typeof BoldSpanRenderable; em: typeof ItalicSpanRenderable; i: typeof ItalicSpanRenderable; u: typeof UnderlineSpanRenderable; br: typeof LineBreakRenderable; a: typeof LinkRenderable; }type GetNonStyledProperties<TConstructor> = TConstructor extends RenderableConstructor<TextRenderable> ? NonStyledProps | "content" : TConstructor extends RenderableConstructor<BoxRenderable> ? NonStyledProps | "title" | "bottomTitle" : TConstructor extends RenderableConstructor<ASCIIFontRenderable> ? NonStyledProps | "text" | "selectable" : TConstructor extends RenderableConstructor<InputRenderable> ? NonStyledProps | "minLength" | "maxLength" | "placeholder" | "value" : TConstructor extends RenderableConstructor<CodeRenderable> ? NonStyledProps | "content" | "filetype" | "syntaxStyle" | "treeSitterClient" : TConstructor extends RenderableConstructor<MarkdownRenderable> ? NonStyledProps | "content" | "syntaxStyle" | "treeSitterClient" | "conceal" | "renderNode" : NonStyledPropstype GetNonStyledProperties<TConstructor> = TConstructor extends RenderableConstructor<TextRenderable> ? NonStyledProps | "content" : TConstructor extends RenderableConstructor<BoxRenderable> ? NonStyledProps | "title" | "bottomTitle" : TConstructor extends RenderableConstructor<ASCIIFontRenderable> ? NonStyledProps | "text" | "selectable" : TConstructor extends RenderableConstructor<InputRenderable> ? NonStyledProps | "minLength" | "maxLength" | "placeholder" | "value" : TConstructor extends RenderableConstructor<CodeRenderable> ? NonStyledProps | "content" | "filetype" | "syntaxStyle" | "treeSitterClient" : TConstructor extends RenderableConstructor<MarkdownRenderable> ? NonStyledProps | "content" | "syntaxStyle" | "treeSitterClient" | "conceal" | "renderNode" : TConstructor extends RenderableConstructor<ImageRenderable> ? NonStyledProps | "source" : NonStyledProps
@opentui/solid/jsx-dev-runtime#
Added
property JSX.IntrinsicElements.image: ImageProps
@opentui/solid/jsx-runtime#
Added
property JSX.IntrinsicElements.image: ImageProps