Skip to content
Draft
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
4 changes: 4 additions & 0 deletions .github/workflows/build-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,15 @@ jobs:
run: bun install

- name: Build
env:
BUN_TEST_CONCURRENCY: 1
run: |
cd packages/core
bun run build

- name: Run tests
env:
BUN_TEST_CONCURRENCY: 1
run: |
cd packages/core
bun run test
6 changes: 6 additions & 0 deletions .github/workflows/build-solid.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,22 @@ jobs:
run: bun install

- name: Build core
env:
BUN_TEST_CONCURRENCY: 1
run: |
cd packages/core
bun run build

- name: Build
env:
BUN_TEST_CONCURRENCY: 1
run: |
cd packages/solid
bun run build --ci

- name: Run tests
env:
BUN_TEST_CONCURRENCY: 1
run: |
cd packages/solid
bun run test
16 changes: 15 additions & 1 deletion bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,18 @@

"@opentui/core": ["@opentui/core@workspace:packages/core"],

"@opentui/core-darwin-arm64": ["@opentui/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zlOdbzzFwNMqBqiWCvzDC/VMs1Gsjgp7Wogoh3bZYYxYyjpCVGcEjP38hY7Db36HezYhPiUnlgg9hK9Tby7hHQ=="],

"@opentui/core-darwin-x64": ["@opentui/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-GBLbF4rvDToZoLTIV5GTWElXulAdirz52YOrfEOMkpub2DTXaXidqyJTVrNrbc+qq4ca8jzYmTeZTnZ9pPd6NQ=="],

"@opentui/core-linux-arm64": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-sVv6PbOxZsZLqSSwJ9R51SQaurvRUzoGJslGWickpxk5QaGHDDxj01H4gR8LfVsRK0HeEdLGvPZLFI7csRn/5A=="],

"@opentui/core-linux-x64": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-7fM7V1SKY3iYa1kDMmOHaDskziOzFFnEzkHcfuoLBK4qxJX196GNfH3xi5URQl/JBhavzHqctmv4KvMPOE5PMQ=="],

"@opentui/core-win32-arm64": ["@opentui/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-oUzb43lv8dx0oijLE2WItciAcE4+c+p78Cl922T3UDyYXUqNP0r3sZDV+Fo3j8XrgpmRWXBj1s7P5igIEGjMUw=="],

"@opentui/core-win32-x64": ["@opentui/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-oxx55mF6pbC9ARqy4gAmchqFXvCLsEOGQmwMrF5xS8KsBGp5As9tB1ZEYRa6zqiPhAHyp/dAv5OgO6Xvcy2Hew=="],

"@opentui/react": ["@opentui/react@workspace:packages/react"],

"@opentui/solid": ["@opentui/solid@workspace:packages/solid"],
Expand All @@ -247,7 +259,7 @@

"@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="],

"@types/node": ["@types/[email protected].1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
"@types/node": ["@types/[email protected].2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA=="],

"@types/react": ["@types/[email protected]", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="],

Expand Down Expand Up @@ -529,6 +541,8 @@

"babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/[email protected]", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],

"bun-types/@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],

"image-q/@types/node": ["@types/[email protected]", "", {}, "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="],

"path-scurry/lru-cache": ["[email protected]", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
Expand Down
41 changes: 41 additions & 0 deletions packages/core/src/graphics/protocol.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { env, registerEnvVar } from "../lib/env"
import type { RenderContext } from "../types"

export type ImageProtocol = "kitty" | "iterm2" | "none"

export interface GraphicsSupport {
readonly protocol: ImageProtocol
}

registerEnvVar({
name: "OTUI_PREFER_KITTY_GRAPHICS",
description: "Force-enable kitty graphics protocol when available.",
type: "boolean",
default: false,
})

export function detectGraphicsSupport(): GraphicsSupport {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is a better way to detect kitty graphics support, there should be already something in the codebase

const termProgram = process.env["TERM_PROGRAM"] ?? ""
const term = process.env["TERM"] ?? ""
if (termProgram === "iTerm.app") {
return { protocol: "iterm2" }
}
if (term.toLowerCase().includes("kitty") || env.OTUI_PREFER_KITTY_GRAPHICS) {
return { protocol: "kitty" }
}
return { protocol: "none" }
}

export function encodeItermImage(image: Buffer, widthPx: number, heightPx: number): string {
const base64 = image.toString("base64")
return `\u001b]1337;File=inline=1;width=${widthPx}px;height=${heightPx}px;preserveAspectRatio=1:${base64}\u0007`
}

export function encodeKittyImage(id: number, image: Buffer, widthPx: number, heightPx: number): string {
const base64 = image.toString("base64")
return `\u001b_Gf=100,a=T,s=${widthPx},v=${heightPx},i=${id};${base64}\u001b\\`
}

export function encodeKittyDelete(id: number): string {
return `\u001b_Ga=d,d=0,i=${id}\u001b\\`
}
54 changes: 54 additions & 0 deletions packages/core/src/renderables/Image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Renderable, type RenderableOptions } from "../Renderable"
import type { RenderContext } from "../types"
import type { OptimizedBuffer } from "../buffer"
import { RGBA, parseColor } from "../lib/RGBA"
import type { GraphicsSupport } from "../graphics/protocol"

export type ImageFit = "contain" | "cover" | "fill"

export interface ImageOptions extends RenderableOptions<ImageRenderable> {
src?: string | Buffer
alt?: string
width?: number
height?: number
fit?: ImageFit
pixelWidth?: number
pixelHeight?: number
}

export class ImageRenderable extends Renderable {
src?: string | Buffer
alt?: string
fit: ImageFit
pixelWidth?: number
pixelHeight?: number
constructor(ctx: RenderContext, options: ImageOptions) {
super(ctx, options)
this.src = options.src
this.alt = options.alt
this.fit = options.fit ?? "contain"
this.width = options.width ?? 0
this.height = options.height ?? 0
this.pixelWidth = options.pixelWidth
this.pixelHeight = options.pixelHeight
}

protected renderSelf(buffer: OptimizedBuffer, _deltaTime: number): void {
const width = Math.max(this.width, 0)
const height = Math.max(this.height, 0)
if (width === 0 || height === 0) return

// Clear the target area so previous frame contents do not bleed through
buffer.fillRect(this.x, this.y, width, height, RGBA.fromInts(0, 0, 0, 0))

const graphics = (this._ctx.graphicsSupport ?? null) as GraphicsSupport | null
const shouldShowFallback = !graphics || graphics.protocol === "none" || !this.src
if (!shouldShowFallback) return

const fallback = this.alt ?? ""
if (fallback.length === 0) return

const trimmed = fallback.slice(0, Math.max(width, 1))
buffer.drawText(trimmed, this.x, this.y, parseColor("#A0A0A0"))
}
}
1 change: 1 addition & 0 deletions packages/core/src/renderables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from "./composition/VRenderable"
export * from "./composition/vnode"
export * from "./Diff"
export * from "./FrameBuffer"
export * from "./Image"
export * from "./Input"
export * from "./LineNumberRenderable"
export * from "./ScrollBar"
Expand Down
Loading
Loading