From 5f12a863fd3fab28752b6e9cb78646beb90031cd Mon Sep 17 00:00:00 2001 From: shmuel hizmi Date: Wed, 25 Mar 2026 12:59:36 -0700 Subject: [PATCH 1/6] Add wmux-client-terminal: TUI client for wmux using OpenTUI New `@playfast/wmux-client-terminal` package that renders wmux in the terminal via OpenTUI's React reconciler, connecting to the same wmux server as the browser client through echoform's WebSocket transport. Features: - Sidebar with categories, tabs, and status indicators - VT100 terminal buffer with ANSI color/cursor/erase support - File viewer with line numbers - Tmux-style Ctrl+B prefix for TUI commands, all other keys pass to PTY - Auto-scroll to bottom on terminal output - Web client link in header bar - Proper lifecycle cleanup via onDestroy and error handlers Also exposes `token` and `wsUrl` on WmuxHandle so consumers can pass credentials to `renderWmuxTUI()`, and adds a TUI demo entry point. Co-Authored-By: Claude Opus 4.6 (1M context) --- bun.lock | 214 +++++++- demo/dev-server/package.json | 2 + demo/dev-server/server/tui.tsx | 47 ++ packages/wmux-client-terminal/package.json | 35 ++ .../src/components/FocusContext.tsx | 54 ++ .../src/components/Sidebar.tsx | 117 +++++ .../src/components/StatusBar.tsx | 40 ++ .../src/components/WmuxApp.tsx | 258 +++++++++ .../src/components/WmuxFileContent.tsx | 30 ++ .../src/components/WmuxIframe.tsx | 20 + .../src/components/WmuxTerminal.tsx | 122 +++++ packages/wmux-client-terminal/src/index.ts | 2 + .../wmux-client-terminal/src/transport.ts | 38 ++ packages/wmux-client-terminal/src/tui.tsx | 65 +++ packages/wmux-client-terminal/src/types.ts | 25 + .../wmux-client-terminal/src/utils/ansi.ts | 490 ++++++++++++++++++ .../wmux-client-terminal/src/utils/base64.ts | 5 + packages/wmux-client-terminal/tsconfig.json | 8 + packages/wmux/src/types.ts | 2 + packages/wmux/src/wmux.tsx | 2 +- 20 files changed, 1574 insertions(+), 2 deletions(-) create mode 100644 demo/dev-server/server/tui.tsx create mode 100644 packages/wmux-client-terminal/package.json create mode 100644 packages/wmux-client-terminal/src/components/FocusContext.tsx create mode 100644 packages/wmux-client-terminal/src/components/Sidebar.tsx create mode 100644 packages/wmux-client-terminal/src/components/StatusBar.tsx create mode 100644 packages/wmux-client-terminal/src/components/WmuxApp.tsx create mode 100644 packages/wmux-client-terminal/src/components/WmuxFileContent.tsx create mode 100644 packages/wmux-client-terminal/src/components/WmuxIframe.tsx create mode 100644 packages/wmux-client-terminal/src/components/WmuxTerminal.tsx create mode 100644 packages/wmux-client-terminal/src/index.ts create mode 100644 packages/wmux-client-terminal/src/transport.ts create mode 100644 packages/wmux-client-terminal/src/tui.tsx create mode 100644 packages/wmux-client-terminal/src/types.ts create mode 100644 packages/wmux-client-terminal/src/utils/ansi.ts create mode 100644 packages/wmux-client-terminal/src/utils/base64.ts create mode 100644 packages/wmux-client-terminal/tsconfig.json diff --git a/bun.lock b/bun.lock index 8f60581..35e611d 100644 --- a/bun.lock +++ b/bun.lock @@ -17,6 +17,7 @@ "version": "1.0.8", "dependencies": { "@playfast/wmux": "workspace:*", + "@playfast/wmux-client-terminal": "workspace:*", "react": "^19.0.0", }, "devDependencies": { @@ -172,6 +173,23 @@ "vite": "^6.0.3", }, }, + "packages/wmux-client-terminal": { + "name": "@playfast/wmux-client-terminal", + "version": "0.0.1", + "dependencies": { + "@opentui/core": "latest", + "@opentui/react": "latest", + "@playfast/echoform": "workspace:*", + "@playfast/wmux": "workspace:*", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/react": "^19.0.0", + }, + "peerDependencies": { + "react": ">=19.0.0", + }, + }, "plugins/bun-ws-client": { "name": "@playfast/echoform-bun-ws-client", "version": "1.0.4", @@ -315,6 +333,8 @@ "@demo/system-monitor": ["@demo/system-monitor@workspace:demo/system-monitor"], + "@dimforge/rapier2d-simd-compat": ["@dimforge/rapier2d-simd-compat@0.17.3", "", {}, "sha512-bijvwWz6NHsNj5e5i1vtd3dU2pDhthSaTUZSh14DUGGKJfw8eMnlWZsxwHBxB/a3AXVNDjL9abuHw1k9FGR+jg=="], + "@dnd-kit/abstract": ["@dnd-kit/abstract@0.3.2", "", { "dependencies": { "@dnd-kit/geometry": "^0.3.2", "@dnd-kit/state": "^0.3.2", "tslib": "^2.6.2" } }, "sha512-uvPVK+SZYD6Viddn9M0K0JQdXknuVSxA/EbMlFRanve3P/XTc18oLa5zGftKSGjfQGmuzkZ34E26DSbly1zi3Q=="], "@dnd-kit/collision": ["@dnd-kit/collision@0.3.2", "", { "dependencies": { "@dnd-kit/abstract": "^0.3.2", "@dnd-kit/geometry": "^0.3.2", "tslib": "^2.6.2" } }, "sha512-pNmNSLCI8S9fNQ7QJ3fBCDjiT0sqBhUFcKgmyYaGvGCAU+kq0AP8OWlh0JSisc9k5mFyxmRpmFQcnJpILz/RPA=="], @@ -409,6 +429,62 @@ "@internationalized/string": ["@internationalized/string@3.2.7", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-D4OHBjrinH+PFZPvfCXvG28n2LSykWcJ7GIioQL+ok0LON15SdfoUssoHzzOUmVZLbRoREsQXVzA6r8JKsbP6A=="], + "@jimp/core": ["@jimp/core@1.6.0", "", { "dependencies": { "@jimp/file-ops": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "await-to-js": "^3.0.0", "exif-parser": "^0.1.12", "file-type": "^16.0.0", "mime": "3" } }, "sha512-EQQlKU3s9QfdJqiSrZWNTxBs3rKXgO2W+GxNXDtwchF3a4IqxDheFX1ti+Env9hdJXDiYLp2jTRjlxhPthsk8w=="], + + "@jimp/diff": ["@jimp/diff@1.6.0", "", { "dependencies": { "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "pixelmatch": "^5.3.0" } }, "sha512-+yUAQ5gvRC5D1WHYxjBHZI7JBRusGGSLf8AmPRPCenTzh4PA+wZ1xv2+cYqQwTfQHU5tXYOhA0xDytfHUf1Zyw=="], + + "@jimp/file-ops": ["@jimp/file-ops@1.6.0", "", {}, "sha512-Dx/bVDmgnRe1AlniRpCKrGRm5YvGmUwbDzt+MAkgmLGf+jvBT75hmMEZ003n9HQI/aPnm/YKnXjg/hOpzNCpHQ=="], + + "@jimp/js-bmp": ["@jimp/js-bmp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "bmp-ts": "^1.0.9" } }, "sha512-FU6Q5PC/e3yzLyBDXupR3SnL3htU7S3KEs4e6rjDP6gNEOXRFsWs6YD3hXuXd50jd8ummy+q2WSwuGkr8wi+Gw=="], + + "@jimp/js-gif": ["@jimp/js-gif@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "gifwrap": "^0.10.1", "omggif": "^1.0.10" } }, "sha512-N9CZPHOrJTsAUoWkWZstLPpwT5AwJ0wge+47+ix3++SdSL/H2QzyMqxbcDYNFe4MoI5MIhATfb0/dl/wmX221g=="], + + "@jimp/js-jpeg": ["@jimp/js-jpeg@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "jpeg-js": "^0.4.4" } }, "sha512-6vgFDqeusblf5Pok6B2DUiMXplH8RhIKAryj1yn+007SIAQ0khM1Uptxmpku/0MfbClx2r7pnJv9gWpAEJdMVA=="], + + "@jimp/js-png": ["@jimp/js-png@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "pngjs": "^7.0.0" } }, "sha512-AbQHScy3hDDgMRNfG0tPjL88AV6qKAILGReIa3ATpW5QFjBKpisvUaOqhzJ7Reic1oawx3Riyv152gaPfqsBVg=="], + + "@jimp/js-tiff": ["@jimp/js-tiff@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "utif2": "^4.1.0" } }, "sha512-zhReR8/7KO+adijj3h0ZQUOiun3mXUv79zYEAKvE0O+rP7EhgtKvWJOZfRzdZSNv0Pu1rKtgM72qgtwe2tFvyw=="], + + "@jimp/plugin-blit": ["@jimp/plugin-blit@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-M+uRWl1csi7qilnSK8uxK4RJMSuVeBiO1AY0+7APnfUbQNZm6hCe0CCFv1Iyw1D/Dhb8ph8fQgm5mwM0eSxgVA=="], + + "@jimp/plugin-blur": ["@jimp/plugin-blur@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-zrM7iic1OTwUCb0g/rN5y+UnmdEsT3IfuCXCJJNs8SZzP0MkZ1eTvuwK9ZidCuMo4+J3xkzCidRwYXB5CyGZTw=="], + + "@jimp/plugin-circle": ["@jimp/plugin-circle@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-xt1Gp+LtdMKAXfDp3HNaG30SPZW6AQ7dtAtTnoRKorRi+5yCJjKqXRgkewS5bvj8DEh87Ko1ydJfzqS3P2tdWw=="], + + "@jimp/plugin-color": ["@jimp/plugin-color@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "tinycolor2": "^1.6.0", "zod": "^3.23.8" } }, "sha512-J5q8IVCpkBsxIXM+45XOXTrsyfblyMZg3a9eAo0P7VPH4+CrvyNQwaYatbAIamSIN1YzxmO3DkIZXzRjFSz1SA=="], + + "@jimp/plugin-contain": ["@jimp/plugin-contain@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-oN/n+Vdq/Qg9bB4yOBOxtY9IPAtEfES8J1n9Ddx+XhGBYT1/QTU/JYkGaAkIGoPnyYvmLEDqMz2SGihqlpqfzQ=="], + + "@jimp/plugin-cover": ["@jimp/plugin-cover@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-Iow0h6yqSC269YUJ8HC3Q/MpCi2V55sMlbkkTTx4zPvd8mWZlC0ykrNDeAy9IJegrQ7v5E99rJwmQu25lygKLA=="], + + "@jimp/plugin-crop": ["@jimp/plugin-crop@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-KqZkEhvs+21USdySCUDI+GFa393eDIzbi1smBqkUPTE+pRwSWMAf01D5OC3ZWB+xZsNla93BDS9iCkLHA8wang=="], + + "@jimp/plugin-displace": ["@jimp/plugin-displace@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-4Y10X9qwr5F+Bo5ME356XSACEF55485j5nGdiyJ9hYzjQP9nGgxNJaZ4SAOqpd+k5sFaIeD7SQ0Occ26uIng5Q=="], + + "@jimp/plugin-dither": ["@jimp/plugin-dither@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0" } }, "sha512-600d1RxY0pKwgyU0tgMahLNKsqEcxGdbgXadCiVCoGd6V6glyCvkNrnnwC0n5aJ56Htkj88PToSdF88tNVZEEQ=="], + + "@jimp/plugin-fisheye": ["@jimp/plugin-fisheye@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-E5QHKWSCBFtpgZarlmN3Q6+rTQxjirFqo44ohoTjzYVrDI6B6beXNnPIThJgPr0Y9GwfzgyarKvQuQuqCnnfbA=="], + + "@jimp/plugin-flip": ["@jimp/plugin-flip@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-/+rJVDuBIVOgwoyVkBjUFHtP+wmW0r+r5OQ2GpatQofToPVbJw1DdYWXlwviSx7hvixTWLKVgRWQ5Dw862emDg=="], + + "@jimp/plugin-hash": ["@jimp/plugin-hash@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "any-base": "^1.1.0" } }, "sha512-wWzl0kTpDJgYVbZdajTf+4NBSKvmI3bRI8q6EH9CVeIHps9VWVsUvEyb7rpbcwVLWYuzDtP2R0lTT6WeBNQH9Q=="], + + "@jimp/plugin-mask": ["@jimp/plugin-mask@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-Cwy7ExSJMZszvkad8NV8o/Z92X2kFUFM8mcDAhNVxU0Q6tA0op2UKRJY51eoK8r6eds/qak3FQkXakvNabdLnA=="], + + "@jimp/plugin-print": ["@jimp/plugin-print@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/types": "1.6.0", "parse-bmfont-ascii": "^1.0.6", "parse-bmfont-binary": "^1.0.6", "parse-bmfont-xml": "^1.1.6", "simple-xml-to-json": "^1.2.2", "zod": "^3.23.8" } }, "sha512-zarTIJi8fjoGMSI/M3Xh5yY9T65p03XJmPsuNet19K/Q7mwRU6EV2pfj+28++2PV2NJ+htDF5uecAlnGyxFN2A=="], + + "@jimp/plugin-quantize": ["@jimp/plugin-quantize@1.6.0", "", { "dependencies": { "image-q": "^4.0.0", "zod": "^3.23.8" } }, "sha512-EmzZ/s9StYQwbpG6rUGBCisc3f64JIhSH+ncTJd+iFGtGo0YvSeMdAd+zqgiHpfZoOL54dNavZNjF4otK+mvlg=="], + + "@jimp/plugin-resize": ["@jimp/plugin-resize@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-uSUD1mqXN9i1SGSz5ov3keRZ7S9L32/mAQG08wUwZiEi5FpbV0K8A8l1zkazAIZi9IJzLlTauRNU41Mi8IF9fA=="], + + "@jimp/plugin-rotate": ["@jimp/plugin-rotate@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-JagdjBLnUZGSG4xjCLkIpQOZZ3Mjbg8aGCCi4G69qR+OjNpOeGI7N2EQlfK/WE8BEHOW5vdjSyglNqcYbQBWRw=="], + + "@jimp/plugin-threshold": ["@jimp/plugin-threshold@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-M59m5dzLoHOVWdM41O8z9SyySzcDn43xHseOH0HavjsfQsT56GGCC4QzU1banJidbUrePhzoEdS42uFE8Fei8w=="], + + "@jimp/types": ["@jimp/types@1.6.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-7UfRsiKo5GZTAATxm2qQ7jqmUXP0DxTArztllTcYdyw6Xi5oT4RaoXynVtCD4UyLK5gJgkZJcwonoijrhYFKfg=="], + + "@jimp/utils": ["@jimp/utils@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "tinycolor2": "^1.6.0" } }, "sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -435,6 +511,22 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@opentui/core": ["@opentui/core@0.1.90", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.90", "@opentui/core-darwin-x64": "0.1.90", "@opentui/core-linux-arm64": "0.1.90", "@opentui/core-linux-x64": "0.1.90", "@opentui/core-win32-arm64": "0.1.90", "@opentui/core-win32-x64": "0.1.90", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-Os2dviqWVETU3kaK36lbSvdcI93GAWhw0xb9ng/d0DWYuM9scRmAhLHiOayp61saWv/BR8OJXeuQYHvrp5rd6A=="], + + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.90", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XFrm2zCg1SlHPQ5A2HX/I4dCrmTjYaCJIIpo3QuPIvZBGH3aBMdWDJh2tXw7AB5Mmh8X1K4hDkP5nlK9x0Ewow=="], + + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.90", "", { "os": "darwin", "cpu": "x64" }, "sha512-vbDpUsnlZ+0CeVKyBBXE+l2+X1XoVncMxMOhXTiMtud2/Cwu+Vfs/g3LC/6Zv08yaytA+9g7Z8sdf0QCqFyQ4w=="], + + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.90", "", { "os": "linux", "cpu": "arm64" }, "sha512-OTbvBTP5mVQ4uwKyuz6b59ElG+D0i1Ln+q6cVhNkLgeRLySIn1uXEzUFQGlnVgb8lFDANsn3yQmdv+R+Cpw0og=="], + + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.90", "", { "os": "linux", "cpu": "x64" }, "sha512-2PJi/LLlO7tGk9Ful/n+6iBdg1RFrA9ibU7wVneE6Z1P0LCYeu7bpwMzea1TXL0eAQWPHsjTs9aPlqPxln0EJw=="], + + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.90", "", { "os": "win32", "cpu": "arm64" }, "sha512-+sTRaOb7gCMZ6iLuuG4y9kzyweJzBDcIJN0Xh49ikFWTwVECDXEVtXahNGlw57avm2yYUoNzmpBjK/LV7zBj9A=="], + + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.90", "", { "os": "win32", "cpu": "x64" }, "sha512-aVFyErckWp4oW9NJ/ZDKBUAlTlfVUiRXGP63JXFOoeqI7EYaM8uBt6rgZAJuUdFWCN2Q66WRS8Y2mk+0BJwVBg=="], + + "@opentui/react": ["@opentui/react@0.1.90", "", { "dependencies": { "@opentui/core": "0.1.90", "react-reconciler": "^0.32.0" }, "peerDependencies": { "react": ">=19.0.0", "react-devtools-core": "^7.0.1", "ws": "^8.18.0" } }, "sha512-uYojzdqDanib5zj/fN2ikHZe+D6zZckZrTgz45ndunozeGPTSt64oRqi9GDCrt26tzTSJHqjJGGJSoIRhNvwyg=="], + "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.16.0", "", { "os": "android", "cpu": "arm" }, "sha512-/kFX4o8KISHCZzHRs8fBp/wZOPdkhYGquhMP2PQjc8ePAVbtaXXDPAFkjUKhz2jXNPS4jGA1wNW+8grhnJgstw=="], "@oxc-resolver/binding-android-arm64": ["@oxc-resolver/binding-android-arm64@11.16.0", "", { "os": "android", "cpu": "arm64" }, "sha512-kPySx7j7mPxW4mRDrdbADyzJV2XrxVeMPDmNnFvTt0/LT1IA26Uk9hzWKQb4k4aeJY58bnRY1soYSawW5wAlKQ=="], @@ -507,6 +599,8 @@ "@playfast/wmux-client": ["@playfast/wmux-client@workspace:packages/wmux-client"], + "@playfast/wmux-client-terminal": ["@playfast/wmux-client-terminal@workspace:packages/wmux-client-terminal"], + "@playwright/test": ["@playwright/test@1.58.2", "", { "dependencies": { "playwright": "1.58.2" }, "bin": { "playwright": "cli.js" } }, "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA=="], "@preact/signals-core": ["@preact/signals-core@1.14.0", "", {}, "sha512-AowtCcCU/33lFlh1zRFf/u+12rfrhtNakj7UpaGEsmMwUKpKWMVvcktOGcwBBNiB4lWrZWc01LhiyyzVklJyaQ=="], @@ -845,6 +939,8 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.2.2", "", { "dependencies": { "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "tailwindcss": "4.2.2" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w=="], + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -891,10 +987,14 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], + "@webgpu/types": ["@webgpu/types@0.1.69", "", {}, "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ=="], + "@xterm/addon-fit": ["@xterm/addon-fit@0.10.0", "", { "peerDependencies": { "@xterm/xterm": "^5.0.0" } }, "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ=="], "@xterm/xterm": ["@xterm/xterm@5.5.0", "", {}, "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="], + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], @@ -903,26 +1003,48 @@ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "any-base": ["any-base@1.1.0", "", {}, "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + "await-to-js": ["await-to-js@3.0.0", "", {}, "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="], "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="], + "bmp-ts": ["bmp-ts@1.0.9", "", {}, "sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw=="], + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + "bufferutil": ["bufferutil@4.0.7", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw=="], + "bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="], + "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], + "bun-webgpu": ["bun-webgpu@0.1.5", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.5", "bun-webgpu-darwin-x64": "^0.1.5", "bun-webgpu-linux-x64": "^0.1.5", "bun-webgpu-win32-x64": "^0.1.5" } }, "sha512-91/K6S5whZKX7CWAm9AylhyKrLGRz6BUiiPiM/kXadSnD4rffljCD/q9cNFftm5YXhx4MvLqw33yEilxogJvwA=="], + + "bun-webgpu-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lIsDkPzJzPl6yrB5CUOINJFPnTRv6fF/Q8J1mAr43ogSp86WZEg9XZKaT6f3EUJ+9ETogGoMnoj1q0AwHUTbAQ=="], + + "bun-webgpu-darwin-x64": ["bun-webgpu-darwin-x64@0.1.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-uEddf5U7GvKIkM/BV18rUKtYHL6d0KeqBjNHwfqDH9QgEo9KVSKvJXS5I/sMefk5V5pIYE+8tQhtrREevhocng=="], + + "bun-webgpu-linux-x64": ["bun-webgpu-linux-x64@0.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-Y/f15j9r8ba0xUz+3lATtS74OE+PPzQXO7Do/1eCluJcuOlfa77kMjvBK/ShWnem3Y9xqi59pebTPOGRB+CaJA=="], + + "bun-webgpu-win32-x64": ["bun-webgpu-win32-x64@0.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-MHSFAKqizISb+C5NfDrFe3g0Al5Njnu0j/A+oO2Q+bIWX+fUYjBSowiYE1ZXJx65KuryuB+tiM7Qh6cQbVvkEg=="], + "caniuse-lite": ["caniuse-lite@1.0.30001761", "", {}, "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -967,6 +1089,8 @@ "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], "dompurify": ["dompurify@3.2.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw=="], @@ -991,6 +1115,12 @@ "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + + "exif-parser": ["exif-parser@0.1.12", "", {}, "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="], + "extendable-error": ["extendable-error@0.1.7", "", {}, "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg=="], "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], @@ -1003,6 +1133,8 @@ "file-editor-demo": ["file-editor-demo@workspace:demo/file-editor"], + "file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], @@ -1021,6 +1153,8 @@ "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + "gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="], + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], @@ -1033,8 +1167,12 @@ "iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "image-q": ["image-q@4.0.0", "", { "dependencies": { "@types/node": "16.9.1" } }, "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw=="], + "input-otp": ["input-otp@1.4.2", "", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="], "intl-messageformat": ["intl-messageformat@10.7.18", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "@formatjs/fast-memoize": "2.2.7", "@formatjs/icu-messageformat-parser": "2.11.4", "tslib": "^2.8.0" } }, "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g=="], @@ -1053,8 +1191,12 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "jimp": ["jimp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/diff": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-gif": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-blur": "1.6.0", "@jimp/plugin-circle": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-contain": "1.6.0", "@jimp/plugin-cover": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-displace": "1.6.0", "@jimp/plugin-dither": "1.6.0", "@jimp/plugin-fisheye": "1.6.0", "@jimp/plugin-flip": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/plugin-mask": "1.6.0", "@jimp/plugin-print": "1.6.0", "@jimp/plugin-quantize": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/plugin-rotate": "1.6.0", "@jimp/plugin-threshold": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg=="], + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "jpeg-js": ["jpeg-js@0.4.4", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], @@ -1101,12 +1243,14 @@ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - "marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="], + "marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], @@ -1129,6 +1273,8 @@ "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + "omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="], + "outdent": ["outdent@0.5.0", "", {}, "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q=="], "oxc-resolver": ["oxc-resolver@11.16.0", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.16.0", "@oxc-resolver/binding-android-arm64": "11.16.0", "@oxc-resolver/binding-darwin-arm64": "11.16.0", "@oxc-resolver/binding-darwin-x64": "11.16.0", "@oxc-resolver/binding-freebsd-x64": "11.16.0", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.16.0", "@oxc-resolver/binding-linux-arm-musleabihf": "11.16.0", "@oxc-resolver/binding-linux-arm64-gnu": "11.16.0", "@oxc-resolver/binding-linux-arm64-musl": "11.16.0", "@oxc-resolver/binding-linux-ppc64-gnu": "11.16.0", "@oxc-resolver/binding-linux-riscv64-gnu": "11.16.0", "@oxc-resolver/binding-linux-riscv64-musl": "11.16.0", "@oxc-resolver/binding-linux-s390x-gnu": "11.16.0", "@oxc-resolver/binding-linux-x64-gnu": "11.16.0", "@oxc-resolver/binding-linux-x64-musl": "11.16.0", "@oxc-resolver/binding-openharmony-arm64": "11.16.0", "@oxc-resolver/binding-wasm32-wasi": "11.16.0", "@oxc-resolver/binding-win32-arm64-msvc": "11.16.0", "@oxc-resolver/binding-win32-ia32-msvc": "11.16.0", "@oxc-resolver/binding-win32-x64-msvc": "11.16.0" } }, "sha512-I4sHGa1fZUpTQ9ftS0E0cBYbBjNnIKXRSX/trFMIJDIJ4n21dCrLAZhnJS0TSfRIRqZNFyceNZr2kablfgNyTA=="], @@ -1147,26 +1293,44 @@ "package-manager-detector": ["package-manager-detector@0.2.11", "", { "dependencies": { "quansync": "^0.2.7" } }, "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ=="], + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + + "parse-bmfont-ascii": ["parse-bmfont-ascii@1.0.6", "", {}, "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA=="], + + "parse-bmfont-binary": ["parse-bmfont-binary@1.0.6", "", {}, "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA=="], + + "parse-bmfont-xml": ["parse-bmfont-xml@1.1.6", "", { "dependencies": { "xml-parse-from-string": "^1.0.0", "xml2js": "^0.5.0" } }, "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + "peek-readable": ["peek-readable@4.1.0", "", {}, "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], + "pixelmatch": ["pixelmatch@5.3.0", "", { "dependencies": { "pngjs": "^6.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q=="], + + "planck": ["planck@1.4.3", "", { "peerDependencies": { "stage-js": "^1.0.0-alpha.12" } }, "sha512-B+lHKhRSeg7vZOfEyEzyQVu7nx8JHcX3QgnAcHXrPW0j04XYKX5eXSiUrxH2Z5QR8OoqvjD6zKIaPMdMYAd0uA=="], + "playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="], "playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="], + "pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="], + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], "prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], + "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], + "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], @@ -1177,6 +1341,8 @@ "react-aria-components": ["react-aria-components@1.16.0", "", { "dependencies": { "@internationalized/date": "^3.12.0", "@internationalized/string": "^3.2.7", "@react-aria/autocomplete": "3.0.0-rc.6", "@react-aria/collections": "^3.0.3", "@react-aria/dnd": "^3.11.6", "@react-aria/focus": "^3.21.5", "@react-aria/interactions": "^3.27.1", "@react-aria/live-announcer": "^3.4.4", "@react-aria/overlays": "^3.31.2", "@react-aria/ssr": "^3.9.10", "@react-aria/textfield": "^3.18.5", "@react-aria/toolbar": "3.0.0-beta.24", "@react-aria/utils": "^3.33.1", "@react-aria/virtualizer": "^4.1.13", "@react-stately/autocomplete": "3.0.0-beta.4", "@react-stately/layout": "^4.6.0", "@react-stately/selection": "^3.20.9", "@react-stately/table": "^3.15.4", "@react-stately/utils": "^3.11.0", "@react-stately/virtualizer": "^4.4.6", "@react-types/form": "^3.7.18", "@react-types/grid": "^3.3.8", "@react-types/shared": "^3.33.1", "@react-types/table": "^3.13.6", "@swc/helpers": "^0.5.0", "client-only": "^0.0.1", "react-aria": "^3.47.0", "react-stately": "^3.45.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-MjHbTLpMFzzD2Tv5KbeXoZwPczuUWZcRavVvQQlNHRtXHH38D+sToMEYpNeir7Wh3K/XWtzeX3EujfJW6QNkrw=="], + "react-devtools-core": ["react-devtools-core@7.0.1", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-C3yNvRHaizlpiASzy7b9vbnBGLrhvdhl1CbdU6EnZgxPNbai60szdLtl+VL76UNOt5bOoVTOz5rNWZxgGt+Gsw=="], + "react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="], "react-reconciler": ["react-reconciler@0.31.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ=="], @@ -1193,6 +1359,10 @@ "read-yaml-file": ["read-yaml-file@1.1.0", "", { "dependencies": { "graceful-fs": "^4.1.5", "js-yaml": "^3.6.1", "pify": "^4.0.1", "strip-bom": "^3.0.0" } }, "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA=="], + "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + + "readable-web-to-node-stream": ["readable-web-to-node-stream@3.0.4", "", { "dependencies": { "readable-stream": "^4.7.0" } }, "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], @@ -1205,8 +1375,12 @@ "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], + "scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="], "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], @@ -1219,6 +1393,8 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "simple-xml-to-json": ["simple-xml-to-json@1.2.4", "", {}, "sha512-3MY16e0ocMHL7N1ufpdObURGyX+lCo0T/A+y6VCwosLdH1HSda4QZl1Sdt/O+2qWp48WFi26XEp5rF0LoaL0Dg=="], + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], "smol-toml": ["smol-toml@1.5.2", "", {}, "sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ=="], @@ -1237,16 +1413,22 @@ "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + "stage-js": ["stage-js@1.0.1", "", {}, "sha512-cz14aPp/wY0s3bkb/B93BPP5ZAEhgBbRmAT3CCDqert8eCAqIpQ0RB2zpK8Ksxf+Pisl5oTzvPHtL4CVzzeHcw=="], + "state-local": ["state-local@1.0.7", "", {}, "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], + "strtok3": ["strtok3@6.3.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^4.1.0" } }, "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw=="], + "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "systeminformation": ["systeminformation@5.31.5", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-5SyLdip4/3alxD4Kh+63bUQTJmu7YMfYQTC+koZy7X73HgNqZSD2P4wOZQWtUncvPvcEmnfIjCoygN4MRoEejQ=="], @@ -1261,12 +1443,18 @@ "term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="], + "three": ["three@0.177.0", "", {}, "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg=="], + + "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "todo-app-demo": ["todo-app-demo@workspace:demo/todo-app"], + "token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="], + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -1291,18 +1479,28 @@ "utf-8-validate": ["utf-8-validate@5.0.10", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ=="], + "utif2": ["utif2@4.1.0", "", { "dependencies": { "pako": "^1.0.11" } }, "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], "walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="], + "web-tree-sitter": ["web-tree-sitter@0.25.10", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "xml-parse-from-string": ["xml-parse-from-string@1.0.1", "", {}, "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g=="], + + "xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], + + "xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], + "xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], @@ -1313,6 +1511,8 @@ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@babel/core/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], @@ -1331,6 +1531,8 @@ "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + "@opentui/react/react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="], + "@playfast/wmux/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "@playfast/wmux-client/typescript": ["typescript@5.7.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg=="], @@ -1375,14 +1577,22 @@ "file-editor-demo/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "image-q/@types/node": ["@types/node@16.9.1", "", {}, "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="], + "knip/@types/node": ["@types/node@18.11.9", "", {}, "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg=="], "knip/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "monaco-editor/marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="], + + "pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="], + "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + "read-yaml-file/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], "socket.io-parser/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], @@ -1391,6 +1601,8 @@ "todo-app-demo/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "@opentui/react/react-reconciler/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="], "read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], diff --git a/demo/dev-server/package.json b/demo/dev-server/package.json index 7d74610..55ded81 100644 --- a/demo/dev-server/package.json +++ b/demo/dev-server/package.json @@ -8,10 +8,12 @@ "dev:server": "bun server/index.tsx", "dev:client": "cd ../../packages/wmux-client && bun run dev", "dev": "concurrently \"bun run dev:server\" \"bun run dev:client\"", + "tui": "bun server/tui.tsx", "test": "playwright test" }, "dependencies": { "@playfast/wmux": "workspace:*", + "@playfast/wmux-client-terminal": "workspace:*", "react": "^19.0.0" }, "devDependencies": { diff --git a/demo/dev-server/server/tui.tsx b/demo/dev-server/server/tui.tsx new file mode 100644 index 0000000..4667b64 --- /dev/null +++ b/demo/dev-server/server/tui.tsx @@ -0,0 +1,47 @@ +import { wmux } from "@playfast/wmux"; +import { renderWmuxTUI } from "@playfast/wmux-client-terminal"; + +const handle = await wmux({ + title: "echoform", + description: "local", + sidebarItems: [ + { + category: "background", + icon: "Activity", + tabs: [ + { name: "counter", icon: "Clock", process: { command: `bash -c 'i=0; while true; do echo "tick $((i++))"; sleep 1; done'` } }, + { name: "logger", icon: "FileText", process: { command: `bash -c 'while true; do echo "[$(date +%T)] log entry"; sleep 2; done'` } }, + ], + }, + { + category: "interactive", + icon: "Terminal", + tabs: [ + { name: "shell", icon: "SquareTerminal", process: { command: process.env.SHELL ?? "/bin/bash" } }, + ], + }, + { + category: "services", + icon: "Server", + tabs: [ + { name: "failing", icon: "Bug", description: "auto-restarts", process: { command: `bash -c 'echo "starting..."; sleep 3; echo "crash!"; exit 1'`, autoRestart: true } }, + { name: "manual", icon: "Wrench", description: "manual start", process: { command: `bash -c 'echo "manual process running"; sleep infinity'`, autoStart: false } }, + { name: "example.com", icon: "Globe", description: "iframe preview", url: "https://example.com" }, + ], + }, + { + category: "project", + icon: "Folder", + files: ".", + }, + ], + port: 4220, + token: "test-token", + open: false, +}); + +await renderWmuxTUI({ + token: handle.token, + wsUrl: handle.wsUrl, + webUrl: handle.url, +}); diff --git a/packages/wmux-client-terminal/package.json b/packages/wmux-client-terminal/package.json new file mode 100644 index 0000000..238a579 --- /dev/null +++ b/packages/wmux-client-terminal/package.json @@ -0,0 +1,35 @@ +{ + "name": "@playfast/wmux-client-terminal", + "version": "0.0.1", + "description": "Terminal UI client for wmux", + "type": "module", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "files": [ + "src" + ], + "scripts": { + "typecheck": "tsgo -p .", + "build": "tsgo -p ." + }, + "dependencies": { + "@playfast/echoform": "workspace:*", + "@playfast/wmux": "workspace:*", + "@opentui/core": "latest", + "@opentui/react": "latest" + }, + "peerDependencies": { + "react": ">=19.0.0" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/react": "^19.0.0" + }, + "author": "shmuelhizmi", + "license": "MIT" +} diff --git a/packages/wmux-client-terminal/src/components/FocusContext.tsx b/packages/wmux-client-terminal/src/components/FocusContext.tsx new file mode 100644 index 0000000..fa89514 --- /dev/null +++ b/packages/wmux-client-terminal/src/components/FocusContext.tsx @@ -0,0 +1,54 @@ +import { createContext, useContext, useRef, type ReactNode, type MutableRefObject } from "react"; + +// ── Prefix key context ───────────────────────────────────── +// Uses a ref so both WmuxApp and WmuxTerminal can read the +// prefix state synchronously within the same useKeyboard tick. + +interface PrefixContextValue { + /** True when Ctrl+B was just pressed and the next key is a TUI command */ + readonly prefixRef: MutableRefObject; + readonly activeTabId: string; +} + +const PrefixContext = createContext({ + prefixRef: { current: false }, + activeTabId: "", +}); + +export const PrefixProvider = ({ + prefixRef, + activeTabId, + children, +}: { + readonly prefixRef: MutableRefObject; + readonly activeTabId: string; + readonly children: ReactNode; +}): ReactNode => ( + + {children} + +); + +export const usePrefixContext = (): PrefixContextValue => useContext(PrefixContext); + +// ── TUI-level config context (web URL, etc.) ─────────────── + +interface TUIContextValue { + readonly webUrl?: string; +} + +const TUIContext = createContext({}); + +export const TUIProvider = ({ + webUrl, + children, +}: { + readonly webUrl?: string; + readonly children: ReactNode; +}): ReactNode => ( + + {children} + +); + +export const useTUIContext = (): TUIContextValue => useContext(TUIContext); diff --git a/packages/wmux-client-terminal/src/components/Sidebar.tsx b/packages/wmux-client-terminal/src/components/Sidebar.tsx new file mode 100644 index 0000000..2828107 --- /dev/null +++ b/packages/wmux-client-terminal/src/components/Sidebar.tsx @@ -0,0 +1,117 @@ +import type { ReactNode } from "react"; +import type { CategoryInfo, TabInfo, FileEntry } from "../types"; + +const STATUS_DOTS: Record = { + running: { char: "\u25cf", color: "#30d158" }, + idle: { char: "\u25cb", color: "#636366" }, + stopped: { char: "\u25cf", color: "#8e8e93" }, + failed: { char: "\u25cf", color: "#ff453a" }, +}; + +const MUTED = "#98989d"; +const ACTIVE_BG = "#38383a"; +const SIDEBAR_BG = "#232325"; + +interface SidebarProps { + readonly categories: ReadonlyArray; + readonly activeCategory: string; + readonly activeTabId: string; + readonly width: number; +} + +const renderStatusDot = (status: string): ReactNode => { + const info = STATUS_DOTS[status] ?? STATUS_DOTS["idle"]!; + return {info.char}; +}; + +const renderTab = (tab: TabInfo, isActive: boolean): ReactNode => ( + + + {" "} + {renderStatusDot(tab.status)} + {" "} + {tab.name} + + +); + +const renderFileEntry = (entry: FileEntry): ReactNode => { + const indent = " ".repeat(entry.depth + 1); + const icon = entry.isDir ? (entry.isExpanded ? "\u25be " : "\u25b8 ") : " "; + const color = entry.isDir ? MUTED : "#8e8e93"; + return ( + + + {indent}{icon}{entry.name} + + + ); +}; + +const renderCategory = ( + category: CategoryInfo, + isActive: boolean, + activeTabId: string, +): ReactNode => { + const headerBg = isActive ? "#2c2c2e" : undefined; + return ( + + + + {isActive ? "\u25be" : "\u25b8"} + {" "} + + {isActive ? {category.name} : category.name} + + + + {isActive && category.type === "process" ? ( + category.tabs.map((tab) => renderTab(tab, tab.id === activeTabId)) + ) : null} + {isActive && category.type === "files" && category.fileEntries ? ( + category.fileEntries.map((entry) => renderFileEntry(entry)) + ) : null} + {isActive && category.type === "files" && category.openFiles && category.openFiles.length > 0 ? ( + + + open files: + + {category.openFiles.map((file) => ( + + + {" "}{file.name} + + + ))} + + ) : null} + + ); +}; + +export const Sidebar = ({ + categories, + activeCategory, + activeTabId, + width, +}: SidebarProps): ReactNode => ( + + + {categories.map((category) => + renderCategory(category, category.name === activeCategory, activeTabId), + )} + + +); diff --git a/packages/wmux-client-terminal/src/components/StatusBar.tsx b/packages/wmux-client-terminal/src/components/StatusBar.tsx new file mode 100644 index 0000000..042c611 --- /dev/null +++ b/packages/wmux-client-terminal/src/components/StatusBar.tsx @@ -0,0 +1,40 @@ +import type { ReactNode } from "react"; + +const MUTED = "#636366"; +const ACCENT = "#0a84ff"; +const WARN = "#ffd60a"; + +interface StatusBarProps { + readonly prefixActive: boolean; +} + +export const StatusBar = ({ prefixActive }: StatusBarProps): ReactNode => ( + + {prefixActive ? ( + <> + + ^B - + + + j/k nav + + + 1-9 cat + + + r restart + + + s stop + + + q quit + + + ) : ( + + ^B tmux prefix + + )} + +); diff --git a/packages/wmux-client-terminal/src/components/WmuxApp.tsx b/packages/wmux-client-terminal/src/components/WmuxApp.tsx new file mode 100644 index 0000000..fee27f0 --- /dev/null +++ b/packages/wmux-client-terminal/src/components/WmuxApp.tsx @@ -0,0 +1,258 @@ +import React, { useState, useCallback, useMemo, useRef, type ReactNode } from "react"; +import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react"; +import { Sidebar } from "./Sidebar"; +import { StatusBar } from "./StatusBar"; +import { PrefixProvider, useTUIContext } from "./FocusContext"; +import type { CategoryInfo } from "../types"; + +const SIDEBAR_WIDTH = 30; +const PREFIX_TIMEOUT_MS = 2000; +const BG = "#1c1c1e"; +const HEADER_BG = "#232325"; +const BORDER_COLOR = "#38383a"; +const MUTED = "#98989d"; + +interface SidebarNavigationItem { + readonly categoryName: string; + readonly tabId: string; +} + +const buildAllNavigationItems = ( + categories: ReadonlyArray, +): readonly SidebarNavigationItem[] => + categories + .filter((c) => c.type !== "files") + .flatMap((c) => c.tabs.map((tab) => ({ categoryName: c.name, tabId: tab.id }))); + +const navigateItem = ( + items: readonly SidebarNavigationItem[], + activeTabId: string, + delta: number, +): SidebarNavigationItem | undefined => { + if (items.length === 0) return undefined; + const currentIndex = items.findIndex((item) => item.tabId === activeTabId); + const nextIndex = currentIndex === -1 ? 0 : (currentIndex + delta + items.length) % items.length; + return items[nextIndex]; +}; + +export const WmuxApp = (props: { + readonly title: string; + readonly description: string; + readonly categories: ReadonlyArray; + readonly activeCategory: string; + readonly activeTabId: string; + readonly children?: ReactNode; + readonly onSelectCategory: { readonly mutate: (name: string) => void }; + readonly onSelectTab: { readonly mutate: (id: string) => void }; + readonly onStartProcess: { readonly mutate: (id: string) => void }; + readonly onStopProcess: { readonly mutate: (id: string) => void }; + readonly onRestartProcess: { readonly mutate: (id: string) => void }; + readonly onToggleDir: { readonly mutate: (path: string) => void }; + readonly onOpenFile: { readonly mutate: (path: string) => void }; + readonly onCloseFile: { readonly mutate: (path: string) => void }; +}): ReactNode => { + const { title, description, categories, activeCategory, activeTabId, children } = props; + const selectCategory = props.onSelectCategory.mutate; + const selectTab = props.onSelectTab.mutate; + const startProcess = props.onStartProcess.mutate; + const stopProcess = props.onStopProcess.mutate; + const restartProcess = props.onRestartProcess.mutate; + const openFile = props.onOpenFile.mutate; + + const renderer = useRenderer(); + const { width, height } = useTerminalDimensions(); + const { webUrl } = useTUIContext(); + + // Prefix key state — ref for synchronous reads across useKeyboard handlers + const prefixRef = useRef(false); + const prefixTimerRef = useRef | null>(null); + const [prefixVisible, setPrefixVisible] = useState(false); + + const activatePrefix = useCallback(() => { + prefixRef.current = true; + setPrefixVisible(true); + if (prefixTimerRef.current) clearTimeout(prefixTimerRef.current); + prefixTimerRef.current = setTimeout(() => { + prefixRef.current = false; + setPrefixVisible(false); + prefixTimerRef.current = null; + }, PREFIX_TIMEOUT_MS); + }, []); + + const consumePrefix = useCallback(() => { + prefixRef.current = false; + setPrefixVisible(false); + if (prefixTimerRef.current) { + clearTimeout(prefixTimerRef.current); + prefixTimerRef.current = null; + } + }, []); + + const allNavItems = useMemo(() => buildAllNavigationItems(categories), [categories]); + + const registry = useMemo(() => { + const map = new Map(); + for (const child of React.Children.toArray(children) as React.ReactElement[]) { + const id = (child.props as { id?: string }).id; + if (id) map.set(id, child); + } + return map; + }, [children]); + + const handleNavigate = useCallback((delta: number) => { + const target = navigateItem(allNavItems, activeTabId, delta); + if (!target) return; + if (target.categoryName !== activeCategory) selectCategory(target.categoryName); + selectTab(target.tabId); + }, [allNavItems, activeTabId, activeCategory, selectCategory, selectTab]); + + useKeyboard((key) => { + // ── Ctrl+B: activate prefix ────────────────────────── + if (key.ctrl && key.name === "b") { + activatePrefix(); + return; + } + + // ── Not in prefix mode: all keys pass through to terminal + if (!prefixRef.current) return; + + // ── Prefix mode: interpret next key as TUI command ─── + consumePrefix(); + + // Navigation + if (key.name === "j" || key.name === "down" || key.name === "n") { + handleNavigate(1); + return; + } + if (key.name === "k" || key.name === "up" || key.name === "p") { + handleNavigate(-1); + return; + } + + // Category by number + if (key.name >= "1" && key.name <= "9") { + const idx = parseInt(key.name, 10) - 1; + if (idx < categories.length) { + const cat = categories[idx]!; + selectCategory(cat.name); + if (cat.type !== "files" && cat.tabs.length > 0) { + selectTab(cat.tabs[0]!.id); + } + } + return; + } + + // Next/prev category + if (key.name === "tab" || key.name === "]") { + const catIdx = categories.findIndex((c) => c.name === activeCategory); + const nextIdx = (catIdx + 1) % categories.length; + const nextCat = categories[nextIdx]!; + selectCategory(nextCat.name); + if (nextCat.type !== "files" && nextCat.tabs.length > 0) { + selectTab(nextCat.tabs[0]!.id); + } + return; + } + if (key.name === "[") { + const catIdx = categories.findIndex((c) => c.name === activeCategory); + const prevIdx = (catIdx - 1 + categories.length) % categories.length; + const prevCat = categories[prevIdx]!; + selectCategory(prevCat.name); + if (prevCat.type !== "files" && prevCat.tabs.length > 0) { + selectTab(prevCat.tabs[0]!.id); + } + return; + } + + // Process controls + if (key.name === "enter") { + const activeCat = categories.find((c) => c.name === activeCategory); + if (!activeCat) return; + if (activeCat.type === "files") { + const firstFile = (activeCat.fileEntries ?? []).find((e) => !e.isDir); + if (firstFile) openFile(firstFile.path); + return; + } + const activeTab = activeCat.tabs.find((t) => t.id === activeTabId); + if (activeTab?.status === "idle") startProcess(activeTabId); + return; + } + + if (key.name === "r") { + const activeCat = categories.find((c) => c.name === activeCategory); + const activeTab = activeCat?.tabs.find((t) => t.id === activeTabId); + if (activeTab?.status === "running") restartProcess(activeTabId); + return; + } + + if (key.name === "s") { + const activeCat = categories.find((c) => c.name === activeCategory); + const activeTab = activeCat?.tabs.find((t) => t.id === activeTabId); + if (activeTab?.status === "running") stopProcess(activeTabId); + return; + } + + // Quit + if (key.name === "q") { + renderer.destroy(); + return; + } + + // Ctrl+B Ctrl+B → send literal Ctrl+B to terminal + if (key.ctrl && key.name === "b") { + // Will be picked up by WmuxTerminal on next event since prefix is now false + return; + } + }); + + const activeChild = registry.get(activeTabId); + + return ( + + + {/* Top bar */} + + + {title} + {description ? {" \u2014 "}{description} : null} + + {webUrl ? ( + + {"\u2197 web"} + + ) : null} + + + {/* Main area */} + + + + {/* Content area */} + + {activeChild ?? ( + + No active tab + + )} + + + + {/* Separator */} + + + {/* Status bar */} + + + + ); +}; diff --git a/packages/wmux-client-terminal/src/components/WmuxFileContent.tsx b/packages/wmux-client-terminal/src/components/WmuxFileContent.tsx new file mode 100644 index 0000000..c28b38a --- /dev/null +++ b/packages/wmux-client-terminal/src/components/WmuxFileContent.tsx @@ -0,0 +1,30 @@ +import type { ReactNode } from "react"; + +interface WmuxFileContentProps { + readonly id: string; + readonly path: string; + readonly name: string; + readonly content: string; + readonly children?: ReactNode; +} + +export const WmuxFileContent = ({ name, content }: WmuxFileContentProps): ReactNode => { + const lines = content.split("\n"); + const gutterWidth = String(lines.length).length + 1; + + return ( + + + {name} + + + {lines.map((line, i) => ( + + {String(i + 1).padStart(gutterWidth, " ")} + {line || " "} + + ))} + + + ); +}; diff --git a/packages/wmux-client-terminal/src/components/WmuxIframe.tsx b/packages/wmux-client-terminal/src/components/WmuxIframe.tsx new file mode 100644 index 0000000..45b6421 --- /dev/null +++ b/packages/wmux-client-terminal/src/components/WmuxIframe.tsx @@ -0,0 +1,20 @@ +import type { ReactNode } from "react"; + +interface WmuxIframeProps { + readonly id: string; + readonly name: string; + readonly url: string; + readonly children?: ReactNode; +} + +export const WmuxIframe = ({ name, url }: WmuxIframeProps): ReactNode => ( + + + {name} + + + {url} + + Open in browser to view + +); diff --git a/packages/wmux-client-terminal/src/components/WmuxTerminal.tsx b/packages/wmux-client-terminal/src/components/WmuxTerminal.tsx new file mode 100644 index 0000000..e49fa7b --- /dev/null +++ b/packages/wmux-client-terminal/src/components/WmuxTerminal.tsx @@ -0,0 +1,122 @@ +import { useState, useEffect, useRef, useCallback, type ReactNode } from "react"; +import { useKeyboard, useTerminalDimensions } from "@opentui/react"; +import { fromBase64, toBase64 } from "../utils/base64"; +import { TerminalBuffer, type StyledLine, type StyledSegment } from "../utils/ansi"; +import { usePrefixContext } from "./FocusContext"; + +const SIDEBAR_WIDTH = 30; + +interface WmuxTerminalProps { + readonly id: string; + readonly name: string; + readonly status: string; + readonly output: { readonly subscribe: (listener: (chunk: string) => void) => () => void }; + readonly onInput: { readonly mutate: (data: string) => void }; + readonly onResize: { readonly mutate: (size: { cols: number; rows: number }) => void }; + readonly children?: ReactNode; +} + +const renderSegment = (seg: StyledSegment, key: number): ReactNode => { + const needsWrap = seg.fg !== undefined || seg.bg !== undefined || seg.bold || seg.italic || seg.underline; + if (!needsWrap) return {seg.text}; + + const inner = seg.bold + ? {seg.text} + : seg.italic + ? {seg.text} + : seg.underline + ? {seg.text} + : seg.text; + + return {inner}; +}; + +const renderLine = (line: StyledLine, key: number): ReactNode => ( + + {line.segments.length === 0 + ? " " + : line.segments.map((seg, j) => renderSegment(seg, j))} + +); + +export const WmuxTerminal = (props: WmuxTerminalProps): ReactNode => { + const { id, output, status } = props; + const sendInput = props.onInput.mutate; + const sendResize = props.onResize.mutate; + const { prefixRef, activeTabId } = usePrefixContext(); + + const [lines, setLines] = useState([]); + const { width, height } = useTerminalDimensions(); + const sentResizeRef = useRef(""); + const termBufRef = useRef(null); + + const getTerminalBuffer = useCallback((): TerminalBuffer => { + if (!termBufRef.current) { + const cols = Math.max(10, width - SIDEBAR_WIDTH - 2); + termBufRef.current = new TerminalBuffer(cols); + } + return termBufRef.current; + }, [width]); + + const isActiveTerminal = activeTabId === id && status === "running"; + + // All keys go to PTY unless prefix is active or Ctrl+B (consumed by WmuxApp) + useKeyboard((key) => { + if (!isActiveTerminal) return; + if (key.ctrl && key.name === "b") return; // prefix key, handled by WmuxApp + if (prefixRef.current) return; // next key after prefix, handled by WmuxApp + + const data = key.sequence; + if (data) { + sendInput(toBase64(new TextEncoder().encode(data))); + } + }); + + const handleOutput = useCallback((b64: string) => { + const bytes = fromBase64(b64); + const text = new TextDecoder().decode(bytes); + const buf = getTerminalBuffer(); + buf.write(text); + setLines(buf.getLines()); + }, [getTerminalBuffer]); + + useEffect(() => { + return output.subscribe(handleOutput); + }, [output, handleOutput]); + + useEffect(() => { + const contentWidth = Math.max(10, width - SIDEBAR_WIDTH - 2); + const contentHeight = Math.max(5, height - 4); + const key = `${contentWidth}x${contentHeight}`; + if (key === sentResizeRef.current) return; + sentResizeRef.current = key; + + const buf = getTerminalBuffer(); + buf.resize(contentWidth, contentHeight); + sendResize({ cols: contentWidth, rows: contentHeight }); + }, [width, height, sendResize, getTerminalBuffer]); + + if (status === "idle") { + return ( + + Process not started + Press ^B then Enter to start + + ); + } + + return ( + + {(status === "stopped" || status === "failed") ? ( + + + {status === "stopped" ? "Process exited" : "Process failed"} + + + ) : null} + + {lines.map((line, i) => renderLine(line, i))} + + + ); +}; diff --git a/packages/wmux-client-terminal/src/index.ts b/packages/wmux-client-terminal/src/index.ts new file mode 100644 index 0000000..a2086ef --- /dev/null +++ b/packages/wmux-client-terminal/src/index.ts @@ -0,0 +1,2 @@ +export { renderWmuxTUI } from "./tui"; +export type { WmuxTUIOptions, WmuxTUIHandle } from "./tui"; diff --git a/packages/wmux-client-terminal/src/transport.ts b/packages/wmux-client-terminal/src/transport.ts new file mode 100644 index 0000000..8556cbc --- /dev/null +++ b/packages/wmux-client-terminal/src/transport.ts @@ -0,0 +1,38 @@ +import type { Transport } from "@playfast/echoform/shared"; +import { createWebSocketTransport, type WebSocketLike } from "@playfast/echoform/shared"; + +export interface TransportConnection { + readonly transport: Transport>; + readonly waitForConnection: () => Promise; + readonly destroy: () => void; +} + +export const connectTransport = (wsUrl: string, token: string): TransportConnection => { + const authenticatedUrl = `${wsUrl}?token=${encodeURIComponent(token)}`; + const ws = new WebSocket(authenticatedUrl); + ws.binaryType = "arraybuffer"; + + const { transport, dispatch, disconnect } = createWebSocketTransport( + ws as unknown as WebSocketLike, + { checkOpen: true }, + ); + + const connectionPromise = new Promise((resolve, reject) => { + ws.onopen = () => resolve(); + ws.onerror = () => reject(new Error("WebSocket connection failed")); + }); + + ws.onmessage = (event: MessageEvent) => { + dispatch(event.data as string | ArrayBuffer); + }; + + ws.onclose = () => { + disconnect(); + }; + + const destroy = (): void => { + ws.close(); + }; + + return { transport, waitForConnection: () => connectionPromise, destroy }; +}; diff --git a/packages/wmux-client-terminal/src/tui.tsx b/packages/wmux-client-terminal/src/tui.tsx new file mode 100644 index 0000000..0dfe806 --- /dev/null +++ b/packages/wmux-client-terminal/src/tui.tsx @@ -0,0 +1,65 @@ +import { createCliRenderer } from "@opentui/core"; +import { createRoot } from "@opentui/react"; +import { Client } from "@playfast/echoform/client"; +import type { Transport } from "@playfast/echoform/shared"; +import { connectTransport } from "./transport"; +import { TUIProvider } from "./components/FocusContext"; +import { WmuxApp } from "./components/WmuxApp"; +import { WmuxTerminal } from "./components/WmuxTerminal"; +import { WmuxFileContent } from "./components/WmuxFileContent"; +import { WmuxIframe } from "./components/WmuxIframe"; + +const tuiViewComponents = { + WmuxApp, + WmuxTerminal, + WmuxFileContent, + WmuxIframe, +}; + +export interface WmuxTUIOptions { + readonly token: string; + readonly wsUrl: string; + readonly webUrl?: string; +} + +export interface WmuxTUIHandle { + readonly destroy: () => void; +} + +const TUIRoot = ({ transport, webUrl }: { readonly transport: Transport>; readonly webUrl?: string }) => ( + + + +); + +export const renderWmuxTUI = async (options: WmuxTUIOptions): Promise => { + let connection: ReturnType | null = null; + + const renderer = await createCliRenderer({ + exitOnCtrlC: false, + onDestroy: () => { + connection?.destroy(); + }, + }); + + // Ensure cleanup on crashes + const onError = (error: unknown): void => { + console.error(error); + renderer.destroy(); + process.exit(1); + }; + process.on("uncaughtException", onError); + process.on("unhandledRejection", onError); + + connection = connectTransport(options.wsUrl, options.token); + await connection.waitForConnection(); + + const root = createRoot(renderer); + root.render(); + + const destroy = (): void => { + renderer.destroy(); + }; + + return { destroy }; +}; diff --git a/packages/wmux-client-terminal/src/types.ts b/packages/wmux-client-terminal/src/types.ts new file mode 100644 index 0000000..3bc5140 --- /dev/null +++ b/packages/wmux-client-terminal/src/types.ts @@ -0,0 +1,25 @@ +export interface FileEntry { + readonly path: string; + readonly name: string; + readonly isDir: boolean; + readonly depth: number; + readonly isExpanded: boolean; +} + +export interface TabInfo { + readonly id: string; + readonly name: string; + readonly description?: string; + readonly icon?: string; + readonly status: string; +} + +export interface CategoryInfo { + readonly name: string; + readonly color: string; + readonly icon?: string; + readonly type: string; + readonly tabs: readonly TabInfo[]; + readonly fileEntries?: readonly FileEntry[]; + readonly openFiles?: readonly { readonly path: string; readonly name: string }[]; +} diff --git a/packages/wmux-client-terminal/src/utils/ansi.ts b/packages/wmux-client-terminal/src/utils/ansi.ts new file mode 100644 index 0000000..fb27bdc --- /dev/null +++ b/packages/wmux-client-terminal/src/utils/ansi.ts @@ -0,0 +1,490 @@ +// ── Types ────────────────────────────────────────────────── + +export interface StyledSegment { + readonly text: string; + readonly fg?: string; + readonly bg?: string; + readonly bold?: boolean; + readonly italic?: boolean; + readonly underline?: boolean; +} + +export interface StyledLine { + readonly segments: readonly StyledSegment[]; +} + +// ── Color palette ────────────────────────────────────────── + +const STANDARD_COLORS: readonly string[] = [ + "#000000", "#cc0000", "#00cc00", "#cccc00", + "#0000cc", "#cc00cc", "#00cccc", "#cccccc", +]; + +const BRIGHT_COLORS: readonly string[] = [ + "#555555", "#ff5555", "#55ff55", "#ffff55", + "#5555ff", "#ff55ff", "#55ffff", "#ffffff", +]; + +const color256Palette: readonly string[] = (() => { + const p: string[] = [...STANDARD_COLORS, ...BRIGHT_COLORS]; + for (let r = 0; r < 6; r++) + for (let g = 0; g < 6; g++) + for (let b = 0; b < 6; b++) { + const h = (v: number): string => (v === 0 ? 0 : 55 + v * 40).toString(16).padStart(2, "0"); + p.push(`#${h(r)}${h(g)}${h(b)}`); + } + for (let i = 0; i < 24; i++) { + const v = (8 + i * 10).toString(16).padStart(2, "0"); + p.push(`#${v}${v}${v}`); + } + return p; +})(); + +// ── Cell & style types ───────────────────────────────────── + +interface CellStyle { + fg: string | undefined; + bg: string | undefined; + bold: boolean; + italic: boolean; + underline: boolean; +} + +interface Cell { + char: string; + style: CellStyle; +} + +const defaultStyle = (): CellStyle => ({ fg: undefined, bg: undefined, bold: false, italic: false, underline: false }); +const emptyCell = (): Cell => ({ char: " ", style: defaultStyle() }); +const cloneStyle = (s: CellStyle): CellStyle => ({ ...s }); + +// ── Terminal buffer ──────────────────────────────────────── + +const DEFAULT_COLS = 120; +const DEFAULT_ROWS = 200; // scrollback lines + +export class TerminalBuffer { + private grid: Cell[][]; + private cols: number; + private rows: number; + private cursorRow = 0; + private cursorCol = 0; + private style: CellStyle = defaultStyle(); + private savedCursor: { row: number; col: number } | null = null; + + // Parser state for incomplete escape sequences across chunks + private partial = ""; + + constructor(cols = DEFAULT_COLS, rows = DEFAULT_ROWS) { + this.cols = cols; + this.rows = rows; + this.grid = [this.newRow()]; + } + + resize(cols: number, rows: number): void { + this.cols = cols; + this.rows = rows; + // Clamp cursor + if (this.cursorCol >= cols) this.cursorCol = cols - 1; + } + + private newRow(): Cell[] { + return Array.from({ length: this.cols }, emptyCell); + } + + private ensureRow(row: number): void { + while (this.grid.length <= row) { + this.grid.push(this.newRow()); + } + } + + private writeChar(ch: string): void { + if (this.cursorCol >= this.cols) { + // Line wrap + this.cursorCol = 0; + this.cursorRow++; + } + this.ensureRow(this.cursorRow); + const row = this.grid[this.cursorRow]!; + // Extend row if needed + while (row.length <= this.cursorCol) row.push(emptyCell()); + row[this.cursorCol] = { char: ch, style: cloneStyle(this.style) }; + this.cursorCol++; + } + + // ── Escape sequence processing ─────────────────────────── + + write(data: string): void { + const input = this.partial + data; + this.partial = ""; + let i = 0; + const len = input.length; + + while (i < len) { + const ch = input[i]!; + + // ── Control characters ───────── + if (ch === "\x1b") { + // Check if we have enough data for the sequence + if (i + 1 >= len) { this.partial = input.slice(i); return; } + + const next = input[i + 1]!; + + // CSI: ESC [ + if (next === "[") { + const end = this.findCsiEnd(input, i + 2); + if (end === -1) { this.partial = input.slice(i); return; } + this.processCsi(input.slice(i + 2, end), input[end]!); + i = end + 1; + continue; + } + + // OSC: ESC ] + if (next === "]") { + const end = this.findOscEnd(input, i + 2); + if (end === -1) { this.partial = input.slice(i); return; } + i = end; // skip entire OSC + continue; + } + + // ESC ( X — character set selection (ignore) + if (next === "(") { + i += 3; // skip ESC ( X + continue; + } + + // ESC ) X — character set G1 (ignore) + if (next === ")") { + i += 3; + continue; + } + + // ESC 7 — save cursor + if (next === "7") { + this.savedCursor = { row: this.cursorRow, col: this.cursorCol }; + i += 2; + continue; + } + + // ESC 8 — restore cursor + if (next === "8") { + if (this.savedCursor) { + this.cursorRow = this.savedCursor.row; + this.cursorCol = this.savedCursor.col; + } + i += 2; + continue; + } + + // ESC = / ESC > — keypad modes (ignore) + if (next === "=" || next === ">") { + i += 2; + continue; + } + + // ESC M — reverse index (scroll down) + if (next === "M") { + if (this.cursorRow > 0) this.cursorRow--; + i += 2; + continue; + } + + // Unknown ESC sequence — skip 2 chars + i += 2; + continue; + } + + if (ch === "\r") { + this.cursorCol = 0; + i++; + continue; + } + + if (ch === "\n") { + this.cursorRow++; + this.ensureRow(this.cursorRow); + i++; + continue; + } + + if (ch === "\b") { + if (this.cursorCol > 0) this.cursorCol--; + i++; + continue; + } + + if (ch === "\t") { + const nextTab = (Math.floor(this.cursorCol / 8) + 1) * 8; + this.cursorCol = Math.min(nextTab, this.cols - 1); + i++; + continue; + } + + // Skip other C0 control characters + const code = ch.charCodeAt(0); + if (code < 0x20 && ch !== "\x1b") { + i++; + continue; + } + + // ── Printable character ──────── + this.writeChar(ch); + i++; + } + + // Trim scrollback + const maxLines = this.rows; + if (this.grid.length > maxLines * 2) { + const trim = this.grid.length - maxLines; + this.grid.splice(0, trim); + this.cursorRow -= trim; + if (this.cursorRow < 0) this.cursorRow = 0; + } + } + + private findCsiEnd(input: string, start: number): number { + // CSI params can include digits, semicolons, ?, >, !, space, etc. + // Terminated by a letter (0x40-0x7E) + for (let j = start; j < input.length; j++) { + const c = input.charCodeAt(j); + if (c >= 0x40 && c <= 0x7e) return j; + } + return -1; // incomplete + } + + private findOscEnd(input: string, start: number): number { + for (let j = start; j < input.length; j++) { + // BEL terminates + if (input[j] === "\x07") return j + 1; + // ST (ESC \) terminates + if (input[j] === "\x1b" && j + 1 < input.length && input[j + 1] === "\\") return j + 2; + } + return -1; // incomplete + } + + private processCsi(paramStr: string, command: string): void { + // Strip leading ? > for DEC private modes + const isPrivate = paramStr.startsWith("?") || paramStr.startsWith(">"); + const cleanParams = paramStr.replace(/^[?>!]/, ""); + const params = cleanParams === "" ? [] : cleanParams.split(";").map((s) => parseInt(s, 10) || 0); + + // DEC private modes — ignore (bracketed paste, cursor visibility, etc.) + if (isPrivate) return; + + switch (command) { + case "m": this.processSgr(params); break; + case "A": this.cursorRow = Math.max(0, this.cursorRow - (params[0] || 1)); break; + case "B": this.cursorRow += (params[0] || 1); this.ensureRow(this.cursorRow); break; + case "C": this.cursorCol = Math.min(this.cols - 1, this.cursorCol + (params[0] || 1)); break; + case "D": this.cursorCol = Math.max(0, this.cursorCol - (params[0] || 1)); break; + case "E": this.cursorRow += (params[0] || 1); this.cursorCol = 0; this.ensureRow(this.cursorRow); break; + case "F": this.cursorRow = Math.max(0, this.cursorRow - (params[0] || 1)); this.cursorCol = 0; break; + case "G": this.cursorCol = Math.max(0, Math.min(this.cols - 1, (params[0] || 1) - 1)); break; + case "H": case "f": this.moveCursor(params); break; + case "J": this.eraseInDisplay(params[0] || 0); break; + case "K": this.eraseInLine(params[0] || 0); break; + case "L": this.insertLines(params[0] || 1); break; + case "M": this.deleteLines(params[0] || 1); break; + case "P": this.deleteChars(params[0] || 1); break; + case "@": this.insertChars(params[0] || 1); break; + case "X": this.eraseChars(params[0] || 1); break; + case "d": this.cursorRow = Math.max(0, (params[0] || 1) - 1); this.ensureRow(this.cursorRow); break; + case "s": this.savedCursor = { row: this.cursorRow, col: this.cursorCol }; break; + case "u": if (this.savedCursor) { this.cursorRow = this.savedCursor.row; this.cursorCol = this.savedCursor.col; } break; + // SGR substrings, scroll, etc. — ignore + } + } + + private moveCursor(params: number[]): void { + const row = Math.max(0, (params[0] || 1) - 1); + const col = Math.max(0, Math.min(this.cols - 1, (params[1] || 1) - 1)); + // For a scrollback buffer, H is relative to visible area. + // We approximate by making it relative to current cursor region. + this.cursorRow = row; + this.cursorCol = col; + this.ensureRow(this.cursorRow); + } + + private eraseInDisplay(mode: number): void { + this.ensureRow(this.cursorRow); + if (mode === 0) { + // Erase from cursor to end + this.eraseInLine(0); + for (let r = this.cursorRow + 1; r < this.grid.length; r++) { + this.grid[r] = this.newRow(); + } + } else if (mode === 1) { + // Erase from start to cursor + for (let r = 0; r < this.cursorRow; r++) { + this.grid[r] = this.newRow(); + } + this.eraseInLine(1); + } else if (mode === 2 || mode === 3) { + // Erase entire display + this.grid = [this.newRow()]; + this.cursorRow = 0; + this.cursorCol = 0; + } + } + + private eraseInLine(mode: number): void { + this.ensureRow(this.cursorRow); + const row = this.grid[this.cursorRow]!; + if (mode === 0) { + // Erase from cursor to end of line + for (let c = this.cursorCol; c < row.length; c++) row[c] = emptyCell(); + } else if (mode === 1) { + // Erase from start to cursor + for (let c = 0; c <= this.cursorCol && c < row.length; c++) row[c] = emptyCell(); + } else if (mode === 2) { + // Erase entire line + this.grid[this.cursorRow] = this.newRow(); + } + } + + private insertLines(n: number): void { + for (let i = 0; i < n; i++) { + this.grid.splice(this.cursorRow, 0, this.newRow()); + } + } + + private deleteLines(n: number): void { + this.grid.splice(this.cursorRow, n); + this.ensureRow(this.cursorRow); + } + + private deleteChars(n: number): void { + this.ensureRow(this.cursorRow); + const row = this.grid[this.cursorRow]!; + row.splice(this.cursorCol, n); + while (row.length < this.cols) row.push(emptyCell()); + } + + private insertChars(n: number): void { + this.ensureRow(this.cursorRow); + const row = this.grid[this.cursorRow]!; + const blanks = Array.from({ length: n }, emptyCell); + row.splice(this.cursorCol, 0, ...blanks); + row.length = this.cols; // truncate + } + + private eraseChars(n: number): void { + this.ensureRow(this.cursorRow); + const row = this.grid[this.cursorRow]!; + for (let c = this.cursorCol; c < this.cursorCol + n && c < row.length; c++) { + row[c] = emptyCell(); + } + } + + private processSgr(params: number[]): void { + if (params.length === 0) params = [0]; + let i = 0; + while (i < params.length) { + const code = params[i]!; + if (code === 0) { Object.assign(this.style, defaultStyle()); } + else if (code === 1) { this.style.bold = true; } + else if (code === 3) { this.style.italic = true; } + else if (code === 4) { this.style.underline = true; } + else if (code === 22) { this.style.bold = false; } + else if (code === 23) { this.style.italic = false; } + else if (code === 24) { this.style.underline = false; } + else if (code >= 30 && code <= 37) { this.style.fg = STANDARD_COLORS[code - 30]; } + else if (code >= 40 && code <= 47) { this.style.bg = STANDARD_COLORS[code - 40]; } + else if (code >= 90 && code <= 97) { this.style.fg = BRIGHT_COLORS[code - 90]; } + else if (code >= 100 && code <= 107) { this.style.bg = BRIGHT_COLORS[code - 100]; } + else if (code === 39) { this.style.fg = undefined; } + else if (code === 49) { this.style.bg = undefined; } + else if (code === 38 || code === 48) { + const mode = params[i + 1]; + if (mode === 5 && i + 2 < params.length) { + const n = params[i + 2]!; + const color = n >= 0 && n < 256 ? color256Palette[n] : undefined; + if (code === 38) this.style.fg = color; else this.style.bg = color; + i += 2; + } else if (mode === 2 && i + 4 < params.length) { + const r = params[i + 2]!, g = params[i + 3]!, b = params[i + 4]!; + const color = `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`; + if (code === 38) this.style.fg = color; else this.style.bg = color; + i += 4; + } + } + i++; + } + } + + // ── Rendering to StyledLines ───────────────────────────── + + getLines(): readonly StyledLine[] { + const result: StyledLine[] = []; + // Find last non-empty row + let lastRow = this.grid.length - 1; + while (lastRow > 0 && this.isRowEmpty(this.grid[lastRow]!)) lastRow--; + + for (let r = 0; r <= lastRow; r++) { + const row = this.grid[r]!; + result.push({ segments: this.rowToSegments(row) }); + } + return result; + } + + private isRowEmpty(row: Cell[]): boolean { + return row.every((cell) => cell.char === " " && !cell.style.fg && !cell.style.bg && !cell.style.bold); + } + + private rowToSegments(row: Cell[]): StyledSegment[] { + const segments: StyledSegment[] = []; + let current = ""; + let currentStyle: CellStyle = defaultStyle(); + + // Find last non-space char to trim trailing whitespace + let lastNonSpace = -1; + for (let c = row.length - 1; c >= 0; c--) { + if (row[c]!.char !== " " || row[c]!.style.fg || row[c]!.style.bg || row[c]!.style.bold) { + lastNonSpace = c; + break; + } + } + + for (let c = 0; c <= lastNonSpace; c++) { + const cell = row[c] ?? emptyCell(); + const sameStyle = cell.style.fg === currentStyle.fg + && cell.style.bg === currentStyle.bg + && cell.style.bold === currentStyle.bold + && cell.style.italic === currentStyle.italic + && cell.style.underline === currentStyle.underline; + + if (sameStyle) { + current += cell.char; + } else { + if (current.length > 0) segments.push(makeSegment(current, currentStyle)); + current = cell.char; + currentStyle = cloneStyle(cell.style); + } + } + + if (current.length > 0) segments.push(makeSegment(current, currentStyle)); + return segments; + } +} + +const makeSegment = (text: string, style: CellStyle): StyledSegment => ({ + text, + ...(style.fg !== undefined && { fg: style.fg }), + ...(style.bg !== undefined && { bg: style.bg }), + ...(style.bold && { bold: true }), + ...(style.italic && { italic: true }), + ...(style.underline && { underline: true }), +}); + +// ── Convenience wrappers (backward compat) ───────────────── + +export const parseAnsiOutput = (raw: string): readonly StyledLine[] => { + const buf = new TerminalBuffer(); + buf.write(raw); + return buf.getLines(); +}; + +export const createDefaultState = (): { fg: undefined; bg: undefined; bold: false; italic: false; underline: false } => ({ + fg: undefined, bg: undefined, bold: false, italic: false, underline: false, +}); diff --git a/packages/wmux-client-terminal/src/utils/base64.ts b/packages/wmux-client-terminal/src/utils/base64.ts new file mode 100644 index 0000000..9731997 --- /dev/null +++ b/packages/wmux-client-terminal/src/utils/base64.ts @@ -0,0 +1,5 @@ +export const toBase64 = (data: Uint8Array): string => + Buffer.from(data).toString("base64"); + +export const fromBase64 = (data: string): Uint8Array => + new Uint8Array(Buffer.from(data, "base64")); diff --git a/packages/wmux-client-terminal/tsconfig.json b/packages/wmux-client-terminal/tsconfig.json new file mode 100644 index 0000000..14ce29b --- /dev/null +++ b/packages/wmux-client-terminal/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsxImportSource": "@opentui/react", + "types": ["bun"] + }, + "include": ["src"] +} diff --git a/packages/wmux/src/types.ts b/packages/wmux/src/types.ts index c590ed8..9a30678 100644 --- a/packages/wmux/src/types.ts +++ b/packages/wmux/src/types.ts @@ -57,5 +57,7 @@ export interface WmuxHandle { readonly url: string; readonly localUrl: string; readonly port: number; + readonly token: string; + readonly wsUrl: string; readonly stop: () => void; } diff --git a/packages/wmux/src/wmux.tsx b/packages/wmux/src/wmux.tsx index 780b3fc..e38870f 100644 --- a/packages/wmux/src/wmux.tsx +++ b/packages/wmux/src/wmux.tsx @@ -113,5 +113,5 @@ export async function wmux(config: WmuxConfig): Promise { process.on("SIGINT", () => { stop(); process.exit(0); }); process.on("SIGTERM", () => { stop(); process.exit(0); }); - return { url: fullClientUrl, localUrl: wsUrl, port: actualPort, stop }; + return { url: fullClientUrl, localUrl: wsUrl, port: actualPort, token, wsUrl, stop }; } From 33e70acd943515d533e366aed633a872a4740b4a Mon Sep 17 00:00:00 2001 From: shmuel hizmi Date: Wed, 25 Mar 2026 13:00:44 -0700 Subject: [PATCH 2/6] Add done promise to WmuxTUIHandle for consumer cleanup The `done` promise resolves when the TUI is closed (user quit via Ctrl+B q, or programmatic destroy()). This lets consumers await it and shut down the dev server cleanly. Co-Authored-By: Claude Opus 4.6 (1M context) --- demo/dev-server/server/tui.tsx | 7 ++++++- packages/wmux-client-terminal/src/tui.tsx | 8 +++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/demo/dev-server/server/tui.tsx b/demo/dev-server/server/tui.tsx index 4667b64..58fcacf 100644 --- a/demo/dev-server/server/tui.tsx +++ b/demo/dev-server/server/tui.tsx @@ -40,8 +40,13 @@ const handle = await wmux({ open: false, }); -await renderWmuxTUI({ +const tui = await renderWmuxTUI({ token: handle.token, wsUrl: handle.wsUrl, webUrl: handle.url, }); + +// Wait for user to quit the TUI (Ctrl+B q), then stop the server +await tui.done; +handle.stop(); +process.exit(0); diff --git a/packages/wmux-client-terminal/src/tui.tsx b/packages/wmux-client-terminal/src/tui.tsx index 0dfe806..fe34d52 100644 --- a/packages/wmux-client-terminal/src/tui.tsx +++ b/packages/wmux-client-terminal/src/tui.tsx @@ -23,7 +23,10 @@ export interface WmuxTUIOptions { } export interface WmuxTUIHandle { + /** Programmatically destroy the TUI */ readonly destroy: () => void; + /** Resolves when the TUI is closed (user quit or destroy() called) */ + readonly done: Promise; } const TUIRoot = ({ transport, webUrl }: { readonly transport: Transport>; readonly webUrl?: string }) => ( @@ -34,11 +37,14 @@ const TUIRoot = ({ transport, webUrl }: { readonly transport: Transport => { let connection: ReturnType | null = null; + let resolveDone: (() => void) | null = null; + const done = new Promise((resolve) => { resolveDone = resolve; }); const renderer = await createCliRenderer({ exitOnCtrlC: false, onDestroy: () => { connection?.destroy(); + resolveDone?.(); }, }); @@ -61,5 +67,5 @@ export const renderWmuxTUI = async (options: WmuxTUIOptions): Promise Date: Wed, 25 Mar 2026 13:01:44 -0700 Subject: [PATCH 3/6] tui skill --- .agents/skills/opentui/SKILL.md | 198 ++++++ .../opentui/references/animation/REFERENCE.md | 431 ++++++++++++ .../references/components/REFERENCE.md | 143 ++++ .../references/components/code-diff.md | 496 ++++++++++++++ .../references/components/containers.md | 412 ++++++++++++ .../opentui/references/components/inputs.md | 531 +++++++++++++++ .../references/components/text-display.md | 384 +++++++++++ .../opentui/references/core/REFERENCE.md | 145 +++++ .agents/skills/opentui/references/core/api.md | 506 +++++++++++++++ .../opentui/references/core/configuration.md | 166 +++++ .../skills/opentui/references/core/gotchas.md | 393 +++++++++++ .../opentui/references/core/patterns.md | 448 +++++++++++++ .../opentui/references/keyboard/REFERENCE.md | 511 +++++++++++++++ .../opentui/references/layout/REFERENCE.md | 337 ++++++++++ .../opentui/references/layout/patterns.md | 444 +++++++++++++ .../opentui/references/react/REFERENCE.md | 174 +++++ .../skills/opentui/references/react/api.md | 435 +++++++++++++ .../opentui/references/react/configuration.md | 301 +++++++++ .../opentui/references/react/gotchas.md | 443 +++++++++++++ .../opentui/references/react/patterns.md | 501 ++++++++++++++ .../opentui/references/solid/REFERENCE.md | 201 ++++++ .../skills/opentui/references/solid/api.md | 543 ++++++++++++++++ .../opentui/references/solid/configuration.md | 315 +++++++++ .../opentui/references/solid/gotchas.md | 415 ++++++++++++ .../opentui/references/solid/patterns.md | 558 ++++++++++++++++ .../opentui/references/testing/REFERENCE.md | 614 ++++++++++++++++++ .claude/skills/opentui | 1 + skills-lock.json | 10 + 28 files changed, 10056 insertions(+) create mode 100644 .agents/skills/opentui/SKILL.md create mode 100644 .agents/skills/opentui/references/animation/REFERENCE.md create mode 100644 .agents/skills/opentui/references/components/REFERENCE.md create mode 100644 .agents/skills/opentui/references/components/code-diff.md create mode 100644 .agents/skills/opentui/references/components/containers.md create mode 100644 .agents/skills/opentui/references/components/inputs.md create mode 100644 .agents/skills/opentui/references/components/text-display.md create mode 100644 .agents/skills/opentui/references/core/REFERENCE.md create mode 100644 .agents/skills/opentui/references/core/api.md create mode 100644 .agents/skills/opentui/references/core/configuration.md create mode 100644 .agents/skills/opentui/references/core/gotchas.md create mode 100644 .agents/skills/opentui/references/core/patterns.md create mode 100644 .agents/skills/opentui/references/keyboard/REFERENCE.md create mode 100644 .agents/skills/opentui/references/layout/REFERENCE.md create mode 100644 .agents/skills/opentui/references/layout/patterns.md create mode 100644 .agents/skills/opentui/references/react/REFERENCE.md create mode 100644 .agents/skills/opentui/references/react/api.md create mode 100644 .agents/skills/opentui/references/react/configuration.md create mode 100644 .agents/skills/opentui/references/react/gotchas.md create mode 100644 .agents/skills/opentui/references/react/patterns.md create mode 100644 .agents/skills/opentui/references/solid/REFERENCE.md create mode 100644 .agents/skills/opentui/references/solid/api.md create mode 100644 .agents/skills/opentui/references/solid/configuration.md create mode 100644 .agents/skills/opentui/references/solid/gotchas.md create mode 100644 .agents/skills/opentui/references/solid/patterns.md create mode 100644 .agents/skills/opentui/references/testing/REFERENCE.md create mode 120000 .claude/skills/opentui create mode 100644 skills-lock.json diff --git a/.agents/skills/opentui/SKILL.md b/.agents/skills/opentui/SKILL.md new file mode 100644 index 0000000..49dc315 --- /dev/null +++ b/.agents/skills/opentui/SKILL.md @@ -0,0 +1,198 @@ +--- +name: opentui +description: Comprehensive OpenTUI skill for building terminal user interfaces. Covers the core imperative API, React reconciler, and Solid reconciler. Use for any TUI development task including components, layout, keyboard handling, animations, and testing. +metadata: + references: core, react, solid +--- + +# OpenTUI Platform Skill + +Consolidated skill for building terminal user interfaces with OpenTUI. Use decision trees below to find the right framework and components, then load detailed references. + +## Critical Rules + +**Follow these rules in all OpenTUI code:** + +1. **Use `create-tui` for new projects.** See framework `REFERENCE.md` quick starts. +2. **`create-tui` options must come before arguments.** `bunx create-tui -t react my-app` works, `bunx create-tui my-app -t react` does NOT. +3. **Never call `process.exit()` directly.** Use `renderer.destroy()` (see `core/gotchas.md`). +4. **Text styling requires nested tags in React/Solid.** Use modifier elements, not props (see `components/text-display.md`). + +## How to Use This Skill + +### Reference File Structure + +Framework references follow a 5-file pattern. Cross-cutting concepts are single-file guides. + +Each framework in `./references//` contains: + +| File | Purpose | When to Read | +|------|---------|--------------| +| `REFERENCE.md` | Overview, when to use, quick start | **Always read first** | +| `api.md` | Runtime API, components, hooks | Writing code | +| `configuration.md` | Setup, tsconfig, bundling | Configuring a project | +| `patterns.md` | Common patterns, best practices | Implementation guidance | +| `gotchas.md` | Pitfalls, limitations, debugging | Troubleshooting | + +Cross-cutting concepts in `./references//` have `REFERENCE.md` as the entry point. + +### Reading Order + +1. Start with `REFERENCE.md` for your chosen framework +2. Then read additional files relevant to your task: + - Building components -> `api.md` + `components/.md` + - Setting up project -> `configuration.md` + - Layout/positioning -> `layout/REFERENCE.md` + - Keyboard/input handling -> `keyboard/REFERENCE.md` + - Animations -> `animation/REFERENCE.md` + - Troubleshooting -> `gotchas.md` + `testing/REFERENCE.md` + +### Example Paths + +``` +./references/react/REFERENCE.md # Start here for React +./references/react/api.md # React components and hooks +./references/solid/configuration.md # Solid project setup +./references/components/inputs.md # Input, Textarea, Select docs +./references/core/gotchas.md # Core debugging tips +``` + +### Runtime Notes + +OpenTUI runs on Bun and uses Zig for native builds. Read `./references/core/gotchas.md` for runtime requirements and build guidance. + +## Quick Decision Trees + +### "Which framework should I use?" + +``` +Which framework? +├─ I want full control, maximum performance, no framework overhead +│ └─ core/ (imperative API) +├─ I know React, want familiar component patterns +│ └─ react/ (React reconciler) +├─ I want fine-grained reactivity, optimal re-renders +│ └─ solid/ (Solid reconciler) +└─ I'm building a library/framework on top of OpenTUI + └─ core/ (imperative API) +``` + +### "I need to display content" + +``` +Display content? +├─ Plain or styled text -> components/text-display.md +├─ Container with borders/background -> components/containers.md +├─ Scrollable content area -> components/containers.md (scrollbox) +├─ ASCII art banner/title -> components/text-display.md (ascii-font) +├─ Code with syntax highlighting -> components/code-diff.md +├─ Diff viewer (unified/split) -> components/code-diff.md +├─ Line numbers with diagnostics -> components/code-diff.md +└─ Markdown content (streaming) -> components/code-diff.md (markdown) +``` + +### "I need user input" + +``` +User input? +├─ Single-line text field -> components/inputs.md (input) +├─ Multi-line text editor -> components/inputs.md (textarea) +├─ Select from a list (vertical) -> components/inputs.md (select) +├─ Tab-based selection (horizontal) -> components/inputs.md (tab-select) +└─ Custom keyboard shortcuts -> keyboard/REFERENCE.md +``` + +### "I need layout/positioning" + +``` +Layout? +├─ Flexbox-style layouts (row, column, wrap) -> layout/REFERENCE.md +├─ Absolute positioning -> layout/patterns.md +├─ Responsive to terminal size -> layout/patterns.md +├─ Centering content -> layout/patterns.md +└─ Complex nested layouts -> layout/patterns.md +``` + +### "I need animations" + +``` +Animations? +├─ Timeline-based animations -> animation/REFERENCE.md +├─ Easing functions -> animation/REFERENCE.md +├─ Property transitions -> animation/REFERENCE.md +└─ Looping animations -> animation/REFERENCE.md +``` + +### "I need to handle input" + +``` +Input handling? +├─ Keyboard events (keypress, release) -> keyboard/REFERENCE.md +├─ Focus management -> keyboard/REFERENCE.md +├─ Paste events -> keyboard/REFERENCE.md +├─ Mouse events -> components/containers.md +└─ Text selection -> components/text-display.md +``` + +### "I need to test my TUI" + +``` +Testing? +├─ Snapshot testing -> testing/REFERENCE.md +├─ Interaction testing -> testing/REFERENCE.md +├─ Test renderer setup -> testing/REFERENCE.md +└─ Debugging tests -> testing/REFERENCE.md +``` + +### "I need to debug/troubleshoot" + +``` +Troubleshooting? +├─ Runtime errors, crashes -> /gotchas.md +├─ Layout issues -> layout/REFERENCE.md + layout/patterns.md +├─ Input/focus issues -> keyboard/REFERENCE.md +└─ Repro + regression tests -> testing/REFERENCE.md +``` + +### Troubleshooting Index + +- Terminal cleanup, crashes -> `core/gotchas.md` +- Text styling not applying -> `components/text-display.md` +- Input focus/shortcuts -> `keyboard/REFERENCE.md` +- Layout misalignment -> `layout/REFERENCE.md` +- Flaky snapshots -> `testing/REFERENCE.md` + +For component naming differences and text modifiers, see `components/REFERENCE.md`. + +## Product Index + +### Frameworks +| Framework | Entry File | Description | +|-----------|------------|-------------| +| Core | `./references/core/REFERENCE.md` | Imperative API, all primitives | +| React | `./references/react/REFERENCE.md` | React reconciler for declarative TUI | +| Solid | `./references/solid/REFERENCE.md` | SolidJS reconciler for declarative TUI | + +### Cross-Cutting Concepts +| Concept | Entry File | Description | +|---------|------------|-------------| +| Layout | `./references/layout/REFERENCE.md` | Yoga/Flexbox layout system | +| Components | `./references/components/REFERENCE.md` | Component reference by category | +| Keyboard | `./references/keyboard/REFERENCE.md` | Keyboard input handling | +| Animation | `./references/animation/REFERENCE.md` | Timeline-based animations | +| Testing | `./references/testing/REFERENCE.md` | Test renderer and snapshots | + +### Component Categories +| Category | Entry File | Components | +|----------|------------|------------| +| Text & Display | `./references/components/text-display.md` | text, ascii-font, styled text | +| Containers | `./references/components/containers.md` | box, scrollbox, borders | +| Inputs | `./references/components/inputs.md` | input, textarea, select, tab-select | +| Code & Diff | `./references/components/code-diff.md` | code, line-number, diff, markdown | + +## Resources + +**Repository**: https://github.com/anomalyco/opentui +**Core Docs**: https://github.com/anomalyco/opentui/tree/main/packages/core/docs +**Examples**: https://github.com/anomalyco/opentui/tree/main/packages/core/src/examples +**Awesome List**: https://github.com/msmps/awesome-opentui diff --git a/.agents/skills/opentui/references/animation/REFERENCE.md b/.agents/skills/opentui/references/animation/REFERENCE.md new file mode 100644 index 0000000..26dd954 --- /dev/null +++ b/.agents/skills/opentui/references/animation/REFERENCE.md @@ -0,0 +1,431 @@ +# Animation System + +OpenTUI provides a timeline-based animation system for smooth property transitions. + +## Overview + +Animations in OpenTUI use: +- **Timeline**: Orchestrates multiple animations +- **Animation Engine**: Manages timelines and rendering +- **Easing Functions**: Control animation curves + +## When to Use + +Use this reference when you need timeline-driven animations, easing curves, or progressive transitions. + +## Basic Usage + +### React + +```tsx +import { useTimeline } from "@opentui/react" +import { useEffect, useState } from "react" + +function AnimatedBox() { + const [width, setWidth] = useState(0) + + const timeline = useTimeline({ + duration: 2000, + }) + + useEffect(() => { + timeline.add( + { width: 0 }, + { + width: 50, + duration: 2000, + ease: "easeOutQuad", + onUpdate: (anim) => { + setWidth(Math.round(anim.targets[0].width)) + }, + } + ) + }, []) + + return ( + + ) +} +``` + +### Solid + +```tsx +import { useTimeline } from "@opentui/solid" +import { createSignal, onMount } from "solid-js" + +function AnimatedBox() { + const [width, setWidth] = createSignal(0) + + const timeline = useTimeline({ + duration: 2000, + }) + + onMount(() => { + timeline.add( + { width: 0 }, + { + width: 50, + duration: 2000, + ease: "easeOutQuad", + onUpdate: (anim) => { + setWidth(Math.round(anim.targets[0].width)) + }, + } + ) + }) + + return ( + + ) +} +``` + +### Core + +```typescript +import { createCliRenderer, Timeline, engine } from "@opentui/core" + +const renderer = await createCliRenderer() +engine.attach(renderer) + +const timeline = new Timeline({ + duration: 2000, + autoplay: true, +}) + +timeline.add( + { x: 0 }, + { + x: 50, + duration: 2000, + ease: "easeOutQuad", + onUpdate: (anim) => { + box.setLeft(Math.round(anim.targets[0].x)) + }, + } +) + +engine.addTimeline(timeline) +``` + +## Timeline Options + +```typescript +const timeline = useTimeline({ + duration: 2000, // Total duration in ms + loop: false, // Loop the timeline + autoplay: true, // Start automatically + onComplete: () => {}, // Called when timeline completes + onPause: () => {}, // Called when timeline pauses +}) +``` + +## Timeline Methods + +```typescript +// Add animation +timeline.add(target, properties, startTime?) + +// Control playback +timeline.play() // Start/resume +timeline.pause() // Pause +timeline.restart() // Restart from beginning + +// State +timeline.progress // Current progress (0-1) +timeline.duration // Total duration +``` + +## Animation Properties + +```typescript +timeline.add( + { value: 0 }, // Target object with initial values + { + value: 100, // Final value + duration: 1000, // Animation duration in ms + ease: "linear", // Easing function + delay: 0, // Delay before starting + onUpdate: (anim) => { + // Called each frame + const current = anim.targets[0].value + }, + onComplete: () => { + // Called when this animation completes + }, + }, + 0 // Start time in timeline (optional) +) +``` + +## Easing Functions + +Available easing functions: + +### Linear + +| Name | Description | +|------|-------------| +| `linear` | Constant speed | + +### Quad (Power of 2) + +| Name | Description | +|------|-------------| +| `easeInQuad` | Slow start | +| `easeOutQuad` | Slow end | +| `easeInOutQuad` | Slow start and end | + +### Cubic (Power of 3) + +| Name | Description | +|------|-------------| +| `easeInCubic` | Slower start | +| `easeOutCubic` | Slower end | +| `easeInOutCubic` | Slower start and end | + +### Quart (Power of 4) + +| Name | Description | +|------|-------------| +| `easeInQuart` | Even slower start | +| `easeOutQuart` | Even slower end | +| `easeInOutQuart` | Even slower start and end | + +### Expo (Exponential) + +| Name | Description | +|------|-------------| +| `easeInExpo` | Exponential start | +| `easeOutExpo` | Exponential end | +| `easeInOutExpo` | Exponential start and end | + +### Back (Overshoot) + +| Name | Description | +|------|-------------| +| `easeInBack` | Pull back, then forward | +| `easeOutBack` | Overshoot, then settle | +| `easeInOutBack` | Both | + +### Elastic + +| Name | Description | +|------|-------------| +| `easeInElastic` | Elastic start | +| `easeOutElastic` | Elastic end (bouncy) | +| `easeInOutElastic` | Both | + +### Bounce + +| Name | Description | +|------|-------------| +| `easeInBounce` | Bounce at start | +| `easeOutBounce` | Bounce at end | +| `easeInOutBounce` | Both | + +## Patterns + +### Progress Bar + +```tsx +function ProgressBar({ progress }: { progress: number }) { + const [width, setWidth] = useState(0) + const maxWidth = 50 + + const timeline = useTimeline() + + useEffect(() => { + timeline.add( + { value: width }, + { + value: (progress / 100) * maxWidth, + duration: 300, + ease: "easeOutQuad", + onUpdate: (anim) => { + setWidth(Math.round(anim.targets[0].value)) + }, + } + ) + }, [progress]) + + return ( + + Progress: {progress}% + + + + + ) +} +``` + +### Fade In + +```tsx +function FadeIn({ children }) { + const [opacity, setOpacity] = useState(0) + + const timeline = useTimeline() + + useEffect(() => { + timeline.add( + { opacity: 0 }, + { + opacity: 1, + duration: 500, + ease: "easeOutQuad", + onUpdate: (anim) => { + setOpacity(anim.targets[0].opacity) + }, + } + ) + }, []) + + return ( + + {children} + + ) +} +``` + +### Looping Animation + +```tsx +function Spinner() { + const [frame, setFrame] = useState(0) + const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] + + useEffect(() => { + const interval = setInterval(() => { + setFrame(f => (f + 1) % frames.length) + }, 80) + + return () => clearInterval(interval) + }, []) + + return {frames[frame]} Loading... +} +``` + +### Staggered Animation + +```tsx +function StaggeredList({ items }) { + const [visibleCount, setVisibleCount] = useState(0) + + useEffect(() => { + let count = 0 + const interval = setInterval(() => { + count++ + setVisibleCount(count) + if (count >= items.length) { + clearInterval(interval) + } + }, 100) + + return () => clearInterval(interval) + }, [items.length]) + + return ( + + {items.slice(0, visibleCount).map((item, i) => ( + {item} + ))} + + ) +} +``` + +### Slide In + +```tsx +function SlideIn({ children, from = "left" }) { + const [offset, setOffset] = useState(from === "left" ? -20 : 20) + + const timeline = useTimeline() + + useEffect(() => { + timeline.add( + { offset: from === "left" ? -20 : 20 }, + { + offset: 0, + duration: 300, + ease: "easeOutCubic", + onUpdate: (anim) => { + setOffset(Math.round(anim.targets[0].offset)) + }, + } + ) + }, []) + + return ( + + {children} + + ) +} +``` + +## Performance Tips + +### Batch Updates + +Timeline automatically batches updates within the render loop. + +### Use Integer Values + +Round animated values for character-based positioning: + +```typescript +onUpdate: (anim) => { + setX(Math.round(anim.targets[0].x)) +} +``` + +### Clean Up Timelines + +Hooks automatically clean up, but for core: + +```typescript +// When done with timeline +engine.removeTimeline(timeline) +``` + +## Gotchas + +### Terminal Refresh Rate + +Terminal UIs typically refresh at 60 FPS max. Very fast animations may appear choppy. + +### Character Grid + +Animations are constrained to character cells. Sub-pixel positioning isn't possible. + +### Cleanup in Effects + +Always clean up intervals and timelines: + +```tsx +useEffect(() => { + const interval = setInterval(...) + return () => clearInterval(interval) +}, []) +``` + +## See Also + +- [React API](../react/api.md) - `useTimeline` hook reference +- [Solid API](../solid/api.md) - `useTimeline` hook reference +- [Core API](../core/api.md) - `AnimationEngine` and `Timeline` classes +- [Layout Patterns](../layout/patterns.md) - Animated positioning and transitions diff --git a/.agents/skills/opentui/references/components/REFERENCE.md b/.agents/skills/opentui/references/components/REFERENCE.md new file mode 100644 index 0000000..0834960 --- /dev/null +++ b/.agents/skills/opentui/references/components/REFERENCE.md @@ -0,0 +1,143 @@ +# OpenTUI Components + +Reference for all OpenTUI components, organized by category. Components are available in all three frameworks (Core, React, Solid) with slight API differences. + +## When to Use + +Use this reference when you need to find the right component category or compare naming across Core, React, and Solid. + +## Component Categories + +| Category | Components | File | +|----------|------------|------| +| Text & Display | text, ascii-font, styled text | [text-display.md](./text-display.md) | +| Containers | box, scrollbox, borders | [containers.md](./containers.md) | +| Inputs | input, textarea, select, tab-select | [inputs.md](./inputs.md) | +| Code & Diff | code, line-number, diff, markdown | [code-diff.md](./code-diff.md) | + +## Component Chooser + +``` +Need a component? +├─ Styled text or ASCII art -> text-display.md +├─ Containers, borders, scrolling -> containers.md +├─ Forms or input controls -> inputs.md +└─ Code blocks, diffs, line numbers, markdown -> code-diff.md +``` + +## Component Naming + +Components have different names across frameworks: + +| Concept | Core (Class) | React (JSX) | Solid (JSX) | +|---------|--------------|-------------|-------------| +| Text | `TextRenderable` | `` | `` | +| Box | `BoxRenderable` | `` | `` | +| ScrollBox | `ScrollBoxRenderable` | `` | `` | +| Input | `InputRenderable` | `` | `` | +| Textarea | `TextareaRenderable` | `