[READ-ONLY] Mirror of https://github.com/bombshell-dev/tty. Platform independent 2D layout engine for terminal applications based on Clay
5

Configure Feed

Select the types of activity you want to include in your feed.

๐Ÿ‘ท add CI/CD workflows and npm/jsr build tasks

Charles Lowell (Mar 16, 2026, 5:50 PM -0500) 53ea63c2 49b88549

+432 -52
+41
.github/workflows/preview.yml
··· 1 + name: preview 2 + 3 + on: [pull_request] 4 + 5 + permissions: 6 + contents: read 7 + 8 + jobs: 9 + preview: 10 + runs-on: ubuntu-latest 11 + timeout-minutes: 10 12 + steps: 13 + - name: checkout 14 + uses: actions/checkout@v4 15 + with: 16 + fetch-depth: 0 17 + submodules: true 18 + 19 + - name: setup deno 20 + uses: denoland/setup-deno@v2 21 + with: 22 + deno-version: v2.x 23 + 24 + - name: build wasm 25 + run: make 26 + 27 + - name: Get Version 28 + id: vars 29 + run: echo ::set-output name=version::$(git describe --abbrev=0 --tags | sed 's/^v//')-pr+$(git rev-parse HEAD) 30 + 31 + - name: Setup Node 32 + uses: actions/setup-node@v4 33 + with: 34 + node-version: 20.x 35 + registry-url: https://registry.npmjs.com 36 + 37 + - name: Build NPM 38 + run: deno task build:npm ${{steps.vars.outputs.version}} 39 + 40 + - name: Publish Preview Versions 41 + run: npx pkg-pr-new publish './build/npm'
+123
.github/workflows/publish.yml
··· 1 + name: Publish 2 + 3 + on: 4 + push: 5 + tags: 6 + - "v*" 7 + 8 + permissions: 9 + contents: read 10 + id-token: write 11 + 12 + jobs: 13 + verify-jsr: 14 + runs-on: ubuntu-latest 15 + steps: 16 + - name: checkout 17 + uses: actions/checkout@v4 18 + with: 19 + submodules: true 20 + 21 + - name: setup deno 22 + uses: denoland/setup-deno@v2 23 + with: 24 + deno-version: v2.x 25 + 26 + - name: build wasm 27 + run: make 28 + 29 + - name: Get Version 30 + id: vars 31 + run: echo ::set-output name=version::$(echo ${{github.ref_name}} | sed 's/^v//') 32 + 33 + - name: Build JSR 34 + run: deno task build:jsr ${{steps.vars.outputs.version}} 35 + 36 + - name: dry run publish 37 + run: npx jsr publish --dry-run --allow-dirty --token=${{secrets.JSR_TOKEN}} 38 + 39 + verify-npm: 40 + runs-on: ubuntu-latest 41 + steps: 42 + - name: checkout 43 + uses: actions/checkout@v4 44 + with: 45 + submodules: true 46 + 47 + - name: setup deno 48 + uses: denoland/setup-deno@v2 49 + with: 50 + deno-version: v2.x 51 + 52 + - name: build wasm 53 + run: make 54 + 55 + - name: Get Version 56 + id: vars 57 + run: echo ::set-output name=version::$(echo ${{github.ref_name}} | sed 's/^v//') 58 + 59 + - name: Setup Node 60 + uses: actions/setup-node@v6 61 + with: 62 + node-version: 24 63 + 64 + - name: Build NPM 65 + run: deno task build:npm ${{steps.vars.outputs.version}} 66 + 67 + - name: dry run publish 68 + run: npm publish --dry-run --tag=verify 69 + working-directory: ./build/npm 70 + 71 + - name: upload build 72 + uses: actions/upload-artifact@v4 73 + with: 74 + name: npm-build 75 + path: ./build/npm 76 + 77 + publish-npm: 78 + needs: [verify-jsr, verify-npm] 79 + runs-on: ubuntu-latest 80 + 81 + steps: 82 + - name: Setup Node 83 + uses: actions/setup-node@v6 84 + with: 85 + node-version: 24 86 + 87 + - name: download build 88 + uses: actions/download-artifact@v4 89 + with: 90 + name: npm-build 91 + path: ./build/npm 92 + 93 + - name: Publish NPM 94 + run: npm publish --access=public --tag=latest 95 + working-directory: ./build/npm 96 + 97 + publish-jsr: 98 + needs: [verify-jsr, verify-npm] 99 + runs-on: ubuntu-latest 100 + 101 + steps: 102 + - name: checkout 103 + uses: actions/checkout@v4 104 + with: 105 + submodules: true 106 + 107 + - name: setup deno 108 + uses: denoland/setup-deno@v2 109 + with: 110 + deno-version: v2.x 111 + 112 + - name: build wasm 113 + run: make 114 + 115 + - name: Get Version 116 + id: vars 117 + run: echo ::set-output name=version::$(echo ${{github.ref_name}} | sed 's/^v//') 118 + 119 + - name: Build JSR 120 + run: deno task build:jsr ${{steps.vars.outputs.version}} 121 + 122 + - name: Publish JSR 123 + run: npx jsr publish --allow-dirty --token=${{secrets.JSR_TOKEN}}
+83
.github/workflows/verify.yaml
··· 1 + name: Verify 2 + 3 + on: 4 + push: 5 + branches: main 6 + pull_request: 7 + branches: main 8 + 9 + permissions: 10 + contents: read 11 + 12 + jobs: 13 + test: 14 + runs-on: ubuntu-latest 15 + 16 + steps: 17 + - name: checkout 18 + uses: actions/checkout@v4 19 + with: 20 + submodules: true 21 + 22 + - name: setup deno 23 + uses: denoland/setup-deno@v2 24 + with: 25 + deno-version: v2.x 26 + 27 + - name: format 28 + run: deno task fmt:check 29 + 30 + - name: lint 31 + run: deno lint 32 + 33 + - name: build wasm 34 + run: make 35 + 36 + - name: test 37 + run: deno task test 38 + 39 + jsr: 40 + needs: test 41 + runs-on: ubuntu-latest 42 + steps: 43 + - name: checkout 44 + uses: actions/checkout@v4 45 + with: 46 + submodules: true 47 + 48 + - name: setup deno 49 + uses: denoland/setup-deno@v2 50 + with: 51 + deno-version: v2.x 52 + 53 + - name: Build JSR 54 + run: deno task build:jsr 0.0.0-verify.0 55 + 56 + - name: dry run publish 57 + run: deno publish --dry-run 58 + 59 + npm: 60 + needs: test 61 + runs-on: ubuntu-latest 62 + steps: 63 + - name: checkout 64 + uses: actions/checkout@v4 65 + with: 66 + submodules: true 67 + 68 + - name: setup deno 69 + uses: denoland/setup-deno@v2 70 + with: 71 + deno-version: v2.x 72 + 73 + - name: Setup Node 74 + uses: actions/setup-node@v4 75 + with: 76 + node-version: 24 77 + 78 + - name: Build 79 + run: deno task build:npm 0.0.0-verify.0 80 + 81 + - name: dry run publish 82 + run: npm publish --dry-run --tag=verify 83 + working-directory: ./build/npm
+1
.gitignore
··· 1 1 /.agent-shell/ 2 2 /clayterm.wasm 3 + /build/
+11 -9
README.md
··· 3 3 A terminal rendering backend for [Clay](https://github.com/nicbarker/clay), 4 4 compiled to WebAssembly. 5 5 6 - With every frame, the entire UI tree is packed into a flat byte array 7 - and sent to WASM in a single call. On the C side, Clay runs layout, 8 - render commands are walked into a cell buffer, and the buffer is 9 - diffed against the previous frame. Only the cells that actually 10 - changed produce output. The result is an ANSI escape sequence that can 11 - be written directly to stdout. One trip to WASM per frame, double 12 - buffered, and only the bytes that need to change hit the output 6 + With every frame, the entire UI tree is packed into a flat byte array and sent 7 + to WASM in a single call. On the C side, Clay runs layout, render commands are 8 + walked into a cell buffer, and the buffer is diffed against the previous frame. 9 + Only the cells that actually changed produce output. The result is an ANSI 10 + escape sequence that can be written directly to stdout. One trip to WASM per 11 + frame, double buffered, and only the bytes that need to change hit the output 13 12 stream. 14 13 15 14 Because the WASM module is pure computation with no I/O, it runs anywhere ··· 34 33 ## Usage 35 34 36 35 ```typescript 37 - import { open, close, text, grow, rgba, createTerm } from "clayterm"; 36 + import { close, createTerm, grow, open, rgba, text } from "clayterm"; 38 37 39 38 const term = await createTerm({ width: 80, height: 24 }); 40 39 ··· 46 45 layout: { padding: { left: 2, top: 1 } }, 47 46 border: { 48 47 color: rgba(0, 255, 0), 49 - left: 1, right: 1, top: 1, bottom: 1, 48 + left: 1, 49 + right: 1, 50 + top: 1, 51 + bottom: 1, 50 52 }, 51 53 cornerRadius: { tl: 1, tr: 1, bl: 1, br: 1 }, 52 54 }),
+14 -4
deno.json
··· 1 1 { 2 + "name": "@clayterm/clayterm", 2 3 "tasks": { 3 - "test": "deno test --allow-read" 4 + "test": "deno test --allow-read", 5 + "fmt": "deno fmt && clang-format -i src/*.c src/*.h", 6 + "fmt:check": "deno fmt --check && clang-format --dry-run --Werror src/*.c src/*.h", 7 + "build:npm": "deno run -A tasks/build-npm.ts", 8 + "build:jsr": "deno run -A tasks/build-jsr.ts" 4 9 }, 5 10 "imports": { 6 11 "@std/testing": "jsr:@std/testing@1", 7 - "@std/expect": "jsr:@std/expect@1" 12 + "@std/expect": "jsr:@std/expect@1", 13 + "dnt": "jsr:@deno/dnt@0.42.3" 14 + }, 15 + "exports": "./mod.ts", 16 + "publish": { 17 + "include": ["*.ts", "clayterm.wasm"] 8 18 }, 9 19 "fmt": { 10 - "exclude": ["clay"] 20 + "exclude": ["clay", "build"] 11 21 }, 12 22 "lint": { 13 - "exclude": ["clay"] 23 + "exclude": ["clay", "build"] 14 24 } 15 25 }
+52 -1
deno.lock
··· 1 1 { 2 2 "version": "5", 3 3 "specifiers": { 4 + "jsr:@david/code-block-writer@^13.0.3": "13.0.3", 5 + "jsr:@deno/dnt@0.42.3": "0.42.3", 4 6 "jsr:@std/assert@^1.0.14": "1.0.18", 5 7 "jsr:@std/assert@^1.0.17": "1.0.18", 6 8 "jsr:@std/expect@1": "1.0.17", 9 + "jsr:@std/fmt@1": "1.0.8", 10 + "jsr:@std/fs@1": "1.0.22", 7 11 "jsr:@std/internal@^1.0.10": "1.0.12", 8 12 "jsr:@std/internal@^1.0.12": "1.0.12", 9 - "jsr:@std/testing@1": "1.0.17" 13 + "jsr:@std/path@1": "1.1.4", 14 + "jsr:@std/path@^1.1.4": "1.1.4", 15 + "jsr:@std/testing@1": "1.0.17", 16 + "jsr:@ts-morph/bootstrap@0.27": "0.27.0", 17 + "jsr:@ts-morph/common@0.27": "0.27.0" 10 18 }, 11 19 "jsr": { 20 + "@david/code-block-writer@13.0.3": { 21 + "integrity": "f98c77d320f5957899a61bfb7a9bead7c6d83ad1515daee92dbacc861e13bb7f" 22 + }, 23 + "@deno/dnt@0.42.3": { 24 + "integrity": "62a917a0492f3c8af002dce90605bb0d41f7d29debc06aca40dba72ab65d8ae3", 25 + "dependencies": [ 26 + "jsr:@david/code-block-writer", 27 + "jsr:@std/fmt", 28 + "jsr:@std/fs", 29 + "jsr:@std/path@1", 30 + "jsr:@ts-morph/bootstrap" 31 + ] 32 + }, 12 33 "@std/assert@1.0.18": { 13 34 "integrity": "270245e9c2c13b446286de475131dc688ca9abcd94fc5db41d43a219b34d1c78", 14 35 "dependencies": [ ··· 22 43 "jsr:@std/internal@^1.0.10" 23 44 ] 24 45 }, 46 + "@std/fmt@1.0.8": { 47 + "integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7" 48 + }, 49 + "@std/fs@1.0.22": { 50 + "integrity": "de0f277a58a867147a8a01bc1b181d0dfa80bfddba8c9cf2bacd6747bcec9308", 51 + "dependencies": [ 52 + "jsr:@std/internal@^1.0.12", 53 + "jsr:@std/path@^1.1.4" 54 + ] 55 + }, 25 56 "@std/internal@1.0.12": { 26 57 "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" 27 58 }, 59 + "@std/path@1.1.4": { 60 + "integrity": "1d2d43f39efb1b42f0b1882a25486647cb851481862dc7313390b2bb044314b5", 61 + "dependencies": [ 62 + "jsr:@std/internal@^1.0.12" 63 + ] 64 + }, 28 65 "@std/testing@1.0.17": { 29 66 "integrity": "87bdc2700fa98249d48a17cd72413352d3d3680dcfbdb64947fd0982d6bbf681", 30 67 "dependencies": [ 31 68 "jsr:@std/assert@^1.0.17", 32 69 "jsr:@std/internal@^1.0.12" 33 70 ] 71 + }, 72 + "@ts-morph/bootstrap@0.27.0": { 73 + "integrity": "b8d7bc8f7942ce853dde4161b28f9aa96769cef3d8eebafb379a81800b9e2448", 74 + "dependencies": [ 75 + "jsr:@ts-morph/common" 76 + ] 77 + }, 78 + "@ts-morph/common@0.27.0": { 79 + "integrity": "c7b73592d78ce8479b356fd4f3d6ec3c460d77753a8680ff196effea7a939052", 80 + "dependencies": [ 81 + "jsr:@std/fs", 82 + "jsr:@std/path@1" 83 + ] 34 84 } 35 85 }, 36 86 "workspace": { 37 87 "dependencies": [ 88 + "jsr:@deno/dnt@0.42.3", 38 89 "jsr:@std/expect@1", 39 90 "jsr:@std/testing@1" 40 91 ]
+1 -1
src/ops.c
··· 1 1 /* ops.c โ€” command buffer reducer for Clay layout commands */ 2 2 3 3 #include "ops.h" 4 - #include "clayterm.h" 5 4 #include "clay/clay.h" 5 + #include "clayterm.h" 6 6 7 7 /* โ”€โ”€ Command buffer helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ 8 8
+9 -9
src/ops.h
··· 8 8 struct Clayterm; 9 9 10 10 /* Command buffer opcodes */ 11 - #define OP_BEGIN_LAYOUT 0x01 12 - #define OP_OPEN_ELEMENT 0x02 13 - #define OP_TEXT 0x03 11 + #define OP_BEGIN_LAYOUT 0x01 12 + #define OP_OPEN_ELEMENT 0x02 13 + #define OP_TEXT 0x03 14 14 #define OP_CLOSE_ELEMENT 0x04 15 - #define OP_END_LAYOUT 0x05 15 + #define OP_END_LAYOUT 0x05 16 16 17 17 /* Property group masks for OPEN_ELEMENT */ 18 - #define PROP_LAYOUT 0x01 19 - #define PROP_BG_COLOR 0x02 18 + #define PROP_LAYOUT 0x01 19 + #define PROP_BG_COLOR 0x02 20 20 #define PROP_CORNER_RADIUS 0x04 21 - #define PROP_BORDER 0x08 22 - #define PROP_CLIP 0x10 23 - #define PROP_FLOATING 0x20 21 + #define PROP_BORDER 0x08 22 + #define PROP_CLIP 0x10 23 + #define PROP_FLOATING 0x20 24 24 25 25 void reduce(struct Clayterm *ct, uint32_t *buf, int len); 26 26
+15
tasks/build-jsr.ts
··· 1 + import jsonDeno from "../deno.json" with { type: "json" }; 2 + 3 + const [version] = Deno.args; 4 + 5 + if (!version) { 6 + throw new Error("a version argument is required to build the jsr package"); 7 + } 8 + 9 + await Deno.writeTextFile( 10 + new URL("../deno.json", import.meta.url), 11 + JSON.stringify({ 12 + ...jsonDeno, 13 + version, 14 + }), 15 + );
+47
tasks/build-npm.ts
··· 1 + import { build, emptyDir } from "dnt"; 2 + 3 + const outDir = "./build/npm"; 4 + 5 + await emptyDir(outDir); 6 + 7 + const [version] = Deno.args; 8 + if (!version) { 9 + throw new Error("a version argument is required to build the npm package"); 10 + } 11 + 12 + await build({ 13 + entryPoints: ["./mod.ts"], 14 + outDir, 15 + shims: { 16 + deno: false, 17 + }, 18 + scriptModule: false, 19 + test: false, 20 + typeCheck: false, 21 + compilerOptions: { 22 + lib: ["ESNext"], 23 + target: "ES2020", 24 + sourceMap: true, 25 + }, 26 + package: { 27 + name: "clayterm", 28 + version, 29 + description: 30 + "A terminal rendering backend for Clay, compiled to WebAssembly", 31 + license: "MIT", 32 + repository: { 33 + type: "git", 34 + url: "git+https://github.com/cowboyd/clayterm.git", 35 + }, 36 + bugs: { 37 + url: "https://github.com/cowboyd/clayterm/issues", 38 + }, 39 + engines: { 40 + node: ">= 16", 41 + }, 42 + sideEffects: false, 43 + }, 44 + }); 45 + 46 + await Deno.copyFile("README.md", `${outDir}/README.md`); 47 + await Deno.copyFile("clayterm.wasm", `${outDir}/esm/clayterm.wasm`);
+35 -28
test/term.test.ts
··· 13 13 }); 14 14 15 15 it("renders hello world", () => { 16 - const out = print(decode(term.render([ 17 - open("root", { 18 - layout: { width: grow(), height: grow(), direction: "ttb" }, 19 - }), 20 - text("Hello, World!"), 21 - close(), 22 - ])), 40, 10); 16 + const out = print( 17 + decode(term.render([ 18 + open("root", { 19 + layout: { width: grow(), height: grow(), direction: "ttb" }, 20 + }), 21 + text("Hello, World!"), 22 + close(), 23 + ])), 24 + 40, 25 + 10, 26 + ); 23 27 24 28 expect(out).toContain("Hello, World!"); 25 29 }); 26 30 27 31 it("renders borders and padding", () => { 28 - const out = print(decode(term.render([ 29 - open("box", { 30 - layout: { 31 - width: grow(), 32 - height: grow(), 33 - padding: { left: 5, top: 5 }, 34 - direction: "ttb", 35 - }, 36 - border: { 37 - color: rgba(0, 255, 0), 38 - left: 1, 39 - right: 1, 40 - top: 1, 41 - bottom: 1, 42 - }, 43 - cornerRadius: { tl: 1, tr: 1, bl: 1, br: 1 }, 44 - }), 45 - text("padded"), 46 - close(), 47 - ])), 40, 10); 32 + const out = print( 33 + decode(term.render([ 34 + open("box", { 35 + layout: { 36 + width: grow(), 37 + height: grow(), 38 + padding: { left: 5, top: 5 }, 39 + direction: "ttb", 40 + }, 41 + border: { 42 + color: rgba(0, 255, 0), 43 + left: 1, 44 + right: 1, 45 + top: 1, 46 + bottom: 1, 47 + }, 48 + cornerRadius: { tl: 1, tr: 1, bl: 1, br: 1 }, 49 + }), 50 + text("padded"), 51 + close(), 52 + ])), 53 + 40, 54 + 10, 55 + ); 48 56 49 57 expect(out).toEqual(` 50 58 โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ ··· 58 66 โ”‚ โ”‚ 59 67 โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ`.trim()); 60 68 }); 61 - 62 69 });