diff --git a/.gitignore b/.gitignore
index 5d2dc32c..5f0bc6bc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,12 @@
ssh_example_ed25519*
/tmp
+
+# MacOs Invisible file
+.DS_Store
+
+# WebAssembly files
+*.wasm
+!bindings/*.wasm
+
+# JavaScript modules
+node_modules
diff --git a/README.md b/README.md
index 8349c970..ffb42a20 100644
--- a/README.md
+++ b/README.md
@@ -761,6 +761,51 @@ the stylesheet-based Markdown renderer.
[glamour]: https://github.com/charmbracelet/glamour
+## JavaScript/WASM Bindings
+
+Lip Gloss is also available for JavaScript through WebAssembly bindings. This allows you to use Lip Gloss styling in Node.js applications and web browsers.
+
+### Installation
+
+```bash
+npm install @charmland/lipgloss
+```
+
+### Usage
+
+```javascript
+const { Table, TableData, Style, Color, normalBorder } = require("@charmland/lipgloss");
+
+// Create styled tables
+const data = new TableData(
+ ["Name", "Age", "City"],
+ ["Alice", "25", "New York"],
+ ["Bob", "30", "San Francisco"]
+);
+
+const table = new Table()
+ .data(data)
+ .border(normalBorder())
+ .styleFunc((row, col) => {
+ if (row === -1) {
+ return new Style().bold(true).foreground(Color("99"));
+ }
+ return new Style().padding(0, 1);
+ })
+ .render();
+
+console.log(table);
+```
+
+The JavaScript bindings support most Lip Gloss features including:
+- **Tables** with `TableData` for structured data management
+- **Lists** with various enumerators
+- **Trees** with nested structures
+- **Styles** with colors, borders, padding, and more
+- **Layout** functions like `joinHorizontal` and `joinVertical`
+
+For complete documentation and examples, see the [bindings directory](./bindings/).
+
## Contributing
See [contributing][contribute].
diff --git a/bindings/.gitattributes b/bindings/.gitattributes
new file mode 100644
index 00000000..e69de29b
diff --git a/bindings/README.md b/bindings/README.md
new file mode 100644
index 00000000..83a8be46
--- /dev/null
+++ b/bindings/README.md
@@ -0,0 +1,390 @@
+# LipGloss.js
+
+The lipgloss that you know and love, now in JavaScript.
+
+> [!WARNING]
+> LipGloss.js is stll experimental.
+
+## Installation
+
+```bash
+npm i @charmland/lipgloss
+```
+
+## Example
+
+```javascript
+const { Table, TableData, Style, Color, List, Tree, Leaf, Bullet, RoundedEnumerator } = require("@charmland/lipgloss");
+
+var s = new Style().foreground(Color("240")).render;
+console.log(
+ new Table()
+ .wrap(false)
+ .headers("Drink", "Description")
+ .row("Bubble Tea", s("Milky"))
+ .row("Milk Tea", s("Also milky"))
+ .row("Actual milk", s("Milky as well"))
+ .render(),
+);
+
+// TableData example - for more complex data management
+const employeeData = new TableData(
+ ["Employee ID", "Name", "Department"],
+ ["001", "John Doe", "Engineering"],
+ ["002", "Jane Smith", "Marketing"],
+ ["003", "Mike Johnson", "Sales"]
+);
+
+console.log(
+ new Table()
+ .data(employeeData)
+ .styleFunc((row, col) => {
+ if (row === -1) {
+ return new Style().foreground(Color("99")).bold(true);
+ }
+ return new Style().padding(0, 1);
+ })
+ .render()
+);
+
+// List example
+const groceries = new List("Bananas", "Barley", "Cashews", "Milk")
+ .enumerator(Bullet)
+ .itemStyle(new Style().foreground(Color("255")));
+
+console.log(groceries.render());
+
+// Tree example with Leaf nodes
+const makeupTree = new Tree()
+ .root("⁜ Makeup")
+ .child(
+ "Glossier",
+ "Fenty Beauty",
+ new Tree().child(
+ new Leaf("Gloss Bomb Universal Lip Luminizer"),
+ new Leaf("Hot Cheeks Velour Blushlighter")
+ ),
+ new Leaf("Nyx"),
+ new Leaf("Mac"),
+ "Milk"
+ )
+ .enumerator(RoundedEnumerator)
+ .enumeratorStyle(new Style().foreground(Color("63")).marginRight(1))
+ .rootStyle(new Style().foreground(Color("35")))
+ .itemStyle(new Style().foreground(Color("212")));
+
+console.log(makeupTree.render());
+```
+
+## TableData
+
+The `TableData` class provides a more structured way to manage table data compared to using individual rows. It's particularly useful when you need to:
+
+- Build tables dynamically
+- Access individual cells programmatically
+- Manage large datasets
+- Separate data logic from presentation
+
+### Basic Usage
+
+```javascript
+const { Table, TableData, Style, Color } = require("@charmland/lipgloss");
+
+// Create TableData with initial rows
+const data = new TableData(
+ ["Name", "Age", "City"],
+ ["Alice", "25", "New York"],
+ ["Bob", "30", "San Francisco"]
+);
+
+// Or build it incrementally
+const data2 = new TableData()
+ .append(["Product", "Price"])
+ .append(["Laptop", "$999"])
+ .append(["Mouse", "$25"]);
+
+// Use with Table
+const table = new Table()
+ .data(data)
+ .styleFunc((row, col) => {
+ if (row === -1) return new Style().bold(true); // Header
+ return new Style().padding(0, 1);
+ })
+ .render();
+```
+
+### TableData Methods
+
+- `new TableData(...rows)` - Create with optional initial rows
+- `.append(row)` - Add a single row (array of strings)
+- `.rows(...rows)` - Add multiple rows at once
+- `.at(row, col)` - Get value at specific position
+- `.rowCount()` - Get number of rows
+- `.columnCount()` - Get number of columns
+
+See `examples/table-data.js` for more comprehensive examples.
+
+## Environment Variables
+
+You can control debug output using environment variables:
+
+- `LIPGLOSS_DEBUG=true` - Enable all lipgloss debug output
+- `DEBUG=lipgloss` - Enable lipgloss debug output (standard debug pattern)
+- `DEBUG=*` - Enable all debug output
+
+Example:
+```bash
+node your-script.js
+```
+
+## Compability
+
+Lipgloss in JavaScript it's experimental and a lot of existing functionalities are not still ported to JavaScript.
+
+### Size (100%)
+
+| Function | Status |
+| --- | --- |
+| `Width` | ✅ |
+| `Height` | ✅ |
+| `Size` | ✅ |
+
+### Color (100%)
+
+| Function | Status |
+| --- | --- |
+| `Color` | ✅ |
+| `NoColor` | ✅ |
+| `Complete` | ✅ |
+| `LightDark` | ✅ |
+| `RGBA` | ✅ |
+
+### Borders (100%)
+
+| Function | Status |
+| --- | --- |
+| `NormalBorder` | ✅ |
+| `RoundedBorder` | ✅ |
+| `BlockBorder` | ✅ |
+| `OuterHalfBlockBorder` | ✅ |
+| `InnerHalfBlockBorder` | ✅ |
+| `ThickBorder` | ✅ |
+| `DoubleBorder` | ✅ |
+| `HiddenBorder` | ✅ |
+| `MarkdownBorder` | ✅ |
+| `ASCIIBorder` | ✅ |
+
+### Style (97.62%)
+
+| Function | Status |
+| --- | --- |
+| `Foreground` | ✅ |
+| `Background` | ✅ |
+| `Width` | ✅ |
+| `Height` | ✅ |
+| `Align` | ✅ |
+| `AlignHorizontal` | ✅ |
+| `AlignVertical` | ✅ |
+| `Padding` | ✅ |
+| `PaddingLeft` | ✅ |
+| `PaddingRight` | ✅ |
+| `PaddingTop` | ✅ |
+| `PaddingBottom` | ✅ |
+| `ColorWhitespace` | ✅ |
+| `Margin` | ✅ |
+| `MarginLeft` | ✅ |
+| `MarginRight` | ✅ |
+| `MarginTop` | ✅ |
+| `MarginBottom` | ✅ |
+| `MarginBackground` | ✅ |
+| `Border` | ✅ |
+| `BorderStyle` | ✅ |
+| `SetBorderRight` | ✅ |
+| `SetBorderLeft` | ✅ |
+| `SetBorderTop` | ✅ |
+| `SetBorderBottom` | ✅ |
+| `BorderForeground` | ✅ |
+| `BorderTopForeground` | ✅ |
+| `BorderRightForeground` | ✅ |
+| `BorderBottomForeground` | ✅ |
+| `BorderLeftForeground` | ✅ |
+| `BorderBackground` | ✅ |
+| `BorderTopBackground` | ✅ |
+| `BorderRightBackground` | ✅ |
+| `BorderBottomBackground` | ✅ |
+| `BorderLeftBackground` | ✅ |
+| `Inline` | ✅ |
+| `MaxWidth` | ✅ |
+| `MaxHeight` | ✅ |
+| `TabWidth` | ✅ |
+| `UnderlineSpaces` | ✅ |
+| `Underline` | ✅ |
+| `Reverse` | ✅ |
+| `SetString` | ✅ |
+| `Inherit` | ✅ |
+| `Faint` | ✅ |
+| `Italic` | ✅ |
+| `Strikethrough` | ✅ |
+| `StrikethroughSpaces` | ✅ |
+| `Transform` | ⏳ |
+
+### Table (100%)
+
+| Function | Status |
+| --- | --- |
+| `table.New` | ✅ |
+| `table.Rows` | ✅ |
+| `table.Headers` | ✅ |
+| `table.Render` | ✅ |
+| `table.ClearRows` | ✅ |
+| `table.BorderTop` | ✅ |
+| `table.BorderBottom` | ✅ |
+| `table.BorderLeft` | ✅ |
+| `table.BorderRight` | ✅ |
+| `table.BorderHeader` | ✅ |
+| `table.BorderColumn` | ✅ |
+| `table.BorderRow` | ✅ |
+| `table.Width` | ✅ |
+| `table.Height` | ✅ |
+| `table.Offset` | ✅ |
+| `table.String` | ✅ |
+| `table.StyleFunc` | ✅ |
+| `table.Data` | ✅ |
+| `table.Border` | ✅ |
+| `table.BorderStyle` | ✅ |
+
+### List (100%)
+
+| Function | Status |
+| --- | --- |
+| `Hidden` | ✅ |
+| `Hide` | ✅ |
+| `Offset` | ✅ |
+| `Value` | ✅ |
+| `String` | ✅ |
+| `Indenter` | ✅ |
+| `ItemStyle` | ✅ |
+| `ItemStyleFunc` | ✅ |
+| `EnumeratorStyle` | ✅ |
+| `EnumeratorStyleFunc` | ✅ |
+| `Item` | ✅ |
+| `Items` | ✅ |
+| `Enumerator` | ✅ |
+
+### Tree (95%)
+
+| Function | Status |
+| --- | --- |
+| `tree.New` | ✅ |
+| `tree.Root` | ✅ |
+| `tree.Child` | ✅ |
+| `tree.Hidden` | ✅ |
+| `tree.Hide` | ✅ |
+| `tree.SetHidden` | ✅ |
+| `tree.Offset` | ✅ |
+| `tree.Value` | ✅ |
+| `tree.SetValue` | ✅ |
+| `tree.String` | ✅ |
+| `tree.Render` | ✅ |
+| `tree.EnumeratorStyle` | ✅ |
+| `tree.EnumeratorStyleFunc` | ✅ |
+| `tree.ItemStyle` | ✅ |
+| `tree.ItemStyleFunc` | ✅ |
+| `tree.RootStyle` | ✅ |
+| `tree.Enumerator` | ✅ |
+| `tree.Indenter` | ✅ |
+| `tree.DefaultEnumerator` | ✅ |
+| `tree.RoundedEnumerator` | ✅ |
+| `tree.DefaultIndenter` | ✅ |
+| `NewLeaf` | ✅ |
+| `Leaf.Value` | ✅ |
+| `Leaf.SetValue` | ✅ |
+| `Leaf.Hidden` | ✅ |
+| `Leaf.SetHidden` | ✅ |
+| `Leaf.String` | ✅ |
+| Custom Enumerators | ⏳ |
+| Custom Indenters | ⏳ |
+
+### Join (100%)
+
+| Function | Status |
+| --- | --- |
+| `JoinHorizontal` | ✅ |
+| `JoinVertical` | ✅ |
+
+### Position (100%)
+
+| Function | Status |
+| --- | --- |
+| `Center` | ✅ |
+| `Right` | ✅ |
+| `Bottom` | ✅ |
+| `Top` | ✅ |
+| `Left` | ✅ |
+| `Place` | ✅ |
+
+### Query (50%)
+
+| Function | Status |
+| --- | --- |
+| `BackgroundColor` | ⏳ |
+| `HasDarkBackground` | ✅ |
+
+### Align (0%)
+
+| Function | Status |
+| --- | --- |
+| `EnableLegacyWindowsANSI` | ⏳ |
+
+## Contributing
+
+We'd love to have you contribute! Please see the [contributing guidelines](https://github.com/charmbracelet/lipgloss/contribute) for more information.
+
+### Testing
+
+The JavaScript bindings include a comprehensive test suite:
+
+```bash
+# From bindings directory
+npm test # Run all tests
+npm run test:simple # Basic table functionality
+npm run test:comprehensive # Complete TableData tests
+npm run examples # Showcase functionality
+
+# Or from examples directory
+cd examples && npm test
+```
+
+Tests are located in `examples/tests/` and cover:
+- Basic table functionality
+- TableData operations
+- Style functions
+- Unicode handling (with known limitations)
+
+See [contributing][contribute].
+
+[contribute]: https://github.com/charmbracelet/lipgloss/contribute
+
+## Feedback
+
+We’d love to hear your thoughts on this project. Feel free to drop us a note!
+
+- [Twitter](https://twitter.com/charmcli)
+- [The Fediverse](https://mastodon.social/@charmcli)
+- [Discord](https://charm.sh/chat)
+
+## License
+
+[MIT](https://github.com/charmbracelet/lipgloss/raw/master/LICENSE)
+
+---
+
+Part of [Charm](https://charm.land).
+
+
+
+Charm热爱开源 • Charm loves open source
+
+[docs]: https://pkg.go.dev/github.com/charmbracelet/lipgloss?tab=doc
+[wish]: https://github.com/charmbracelet/wish
+[ssh-example]: examples/ssh
diff --git a/bindings/Taskfile.yml b/bindings/Taskfile.yml
new file mode 100644
index 00000000..9f0bba0a
--- /dev/null
+++ b/bindings/Taskfile.yml
@@ -0,0 +1,18 @@
+# https://taskfile.dev
+version: "3"
+
+tasks:
+ build:
+ cmds:
+ - GOOS=js tinygo build -target wasm -tags wasm -buildmode c-shared -no-debug -opt=s -o ./src/lipgloss.wasm ./main.go
+ - cp $(tinygo env TINYGOROOT)/targets/wasm_exec.js ./src/wasmExec.js
+ - wasm-opt ./src/lipgloss.wasm -o ./src/lipgloss.wasm -O3 --intrinsic-lowering
+
+ dev:
+ cmds:
+ - GOOS=js tinygo build -target wasm -tags wasm -buildmode c-shared -o ./src/lipgloss.wasm ./main.go
+ - cp $(tinygo env TINYGOROOT)/targets/wasm_exec.js ./src/wasmExec.js
+
+ test:
+ cmds:
+ - cd ./examples && npm i && npm test
diff --git a/bindings/examples/chess.js b/bindings/examples/chess.js
new file mode 100644
index 00000000..f6e79d39
--- /dev/null
+++ b/bindings/examples/chess.js
@@ -0,0 +1,39 @@
+const {
+ Table,
+ Style,
+ Color,
+ normalBorder,
+ joinHorizontal,
+ joinVertical,
+ Right,
+ Center,
+} = require("@charmland/lipgloss");
+
+var s = new Style().foreground(Color("241")).render;
+const board = [
+ ["♜", "♞", "♝", "♛", "♚", "♝", "♞", "♜"],
+ ["♟", "♟", "♟", "♟", "♟", "♟", "♟", "♟"],
+ [" ", " ", " ", " ", " ", " ", " ", " "],
+ [" ", " ", " ", " ", " ", " ", " ", " "],
+ [" ", " ", " ", " ", " ", " ", " ", " "],
+ [" ", " ", " ", " ", " ", " ", " ", " "],
+ ["♙", "♙", "♙", "♙", "♙", "♙", "♙", "♙"],
+ ["♖", "♘", "♗", "♕", "♔", "♗", "♘", "♖"],
+];
+
+let table = new Table()
+ .border(normalBorder())
+ .borderRow(true)
+ .borderColumn(true)
+ .rows(board)
+ .styleFunc((row, col) => {
+ return new Style().padding(0, 1);
+ });
+
+let ranks = s([" A", "B", "C", "D", "E", "F", "G", "H "].join(" "));
+let files = s([" 1", "2", "3", "4", "5", "6", "7", "8 "].join("\n\n "));
+
+console.log(
+ joinVertical(Right, joinHorizontal(Center, files, table.render()), ranks) +
+ "\n",
+);
diff --git a/bindings/examples/color.js b/bindings/examples/color.js
new file mode 100644
index 00000000..478cecb8
--- /dev/null
+++ b/bindings/examples/color.js
@@ -0,0 +1,44 @@
+const {
+ lightDark,
+ Color,
+ roundedBorder,
+ joinVertical,
+ Center,
+ Style,
+ List,
+} = require("@charmland/lipgloss");
+
+// TODO: use hasDarkBackground()
+let hasDarkBG = true;
+let selectLightDark = lightDark(hasDarkBG);
+
+let frameStyle = new Style()
+ .border(roundedBorder())
+ .borderForeground(selectLightDark(Color("#C5ADF9"), Color("#864EFF")))
+ .padding(1, 3)
+ .margin(1, 3);
+
+let paragraphStyle = new Style().width(40).marginBottom(1).align(Center);
+let textStyle = new Style().foreground(
+ selectLightDark(Color("#696969"), Color("#bdbdbd")),
+);
+let keywordStyle = new Style()
+ .foreground(selectLightDark(Color("#37CD96"), Color("#22C78A")))
+ .bold(true);
+let activeButton = new Style()
+ .padding(0, 3)
+ .background(Color("#FF6AD2"))
+ .foreground(Color("#FFFCC2"));
+let inactiveButton = new Style()
+ .padding(0, 3)
+ .background(selectLightDark(Color("#988F95"), Color("#978692")))
+ .foreground(selectLightDark(Color("#FDFCE3"), Color("#FBFAE7")));
+
+let text = paragraphStyle.render(
+ textStyle.render("Are you sure you want to eat that ") +
+ keywordStyle.render("moderatly ripe") +
+ textStyle.render(" banana?"),
+);
+let buttons = activeButton.render("Yes") + " " + inactiveButton.render("No");
+let block = frameStyle.render(joinVertical(Center, text, buttons));
+console.log(block);
diff --git a/bindings/examples/duckduckgoose.js b/bindings/examples/duckduckgoose.js
new file mode 100644
index 00000000..a51349b6
--- /dev/null
+++ b/bindings/examples/duckduckgoose.js
@@ -0,0 +1,23 @@
+const {
+ List,
+ Style,
+ Color,
+} = require("../src/index.js");
+
+// Create styles matching the original Go example
+const enumStyle = new Style()
+ .foreground(Color("#00d787"))
+ .marginRight(1);
+
+const itemStyle = new Style()
+ .foreground(Color("255"));
+
+// Recreate the exact original Go example behavior
+const items = ["Duck", "Duck", "Duck", "Goose", "Duck"];
+items.forEach((item) => {
+ // Custom enumerator: "Honk →" for Goose, 7 spaces for Duck to align properly
+ const enumerator = item === "Goose" ? "Honk →" : " ";
+ const styledEnum = enumStyle.render(enumerator);
+ const styledItem = itemStyle.render(item);
+ console.log(styledEnum + " " + styledItem);
+});
\ No newline at end of file
diff --git a/bindings/examples/files.js b/bindings/examples/files.js
new file mode 100644
index 00000000..99562a5b
--- /dev/null
+++ b/bindings/examples/files.js
@@ -0,0 +1,62 @@
+const fs = require('fs');
+const path = require('path');
+const {
+ Tree,
+ Style,
+ Color,
+} = require("@charmland/lipgloss");
+
+function addBranches(root, dirPath) {
+ try {
+ const items = fs.readdirSync(dirPath, { withFileTypes: true });
+
+ for (const item of items) {
+ // Skip items that start with a dot
+ if (item.name.startsWith('.')) {
+ continue;
+ }
+
+ if (item.isDirectory()) {
+ // It's a directory
+ const treeBranch = new Tree().root(item.name);
+ root.child(treeBranch);
+
+ // Recurse into subdirectory
+ const branchPath = path.join(dirPath, item.name);
+ addBranches(treeBranch, branchPath);
+ } else {
+ // It's a file
+ root.child(item.name);
+ }
+ }
+ } catch (err) {
+ console.error(`Error reading directory ${dirPath}:`, err.message);
+ }
+}
+
+function main() {
+ const enumeratorStyle = new Style()
+ .foreground(Color("240"))
+ .paddingRight(1);
+
+ const itemStyle = new Style()
+ .foreground(Color("99"))
+ .bold(true)
+ .paddingRight(1);
+
+ // Get current working directory
+ const pwd = process.cwd();
+
+ const t = new Tree()
+ .root(pwd)
+ .enumeratorStyle(enumeratorStyle)
+ .rootStyle(itemStyle)
+ .itemStyle(itemStyle);
+
+ // Build the tree starting from current directory
+ addBranches(t, ".");
+
+ console.log(t.render());
+}
+
+main();
\ No newline at end of file
diff --git a/bindings/examples/layout.js b/bindings/examples/layout.js
new file mode 100644
index 00000000..bf4c0bdd
--- /dev/null
+++ b/bindings/examples/layout.js
@@ -0,0 +1,515 @@
+// TODO:
+// few problems found
+// 3. JoinVertical and JoinHorizontal are not decoding hidden characters.
+
+const {
+ lightDark,
+ Color,
+ roundedBorder,
+ joinVertical,
+ Center,
+ Style,
+ marginLeft,
+ normalBorder,
+ Left,
+ Right,
+ Top,
+ Bottom,
+ Width,
+ joinHorizontal,
+ Place,
+} = require("@charmland/lipgloss");
+
+const width = 96;
+const columnWidth = 30;
+const hasDarkBG = true;
+let selectLightDark = lightDark(hasDarkBG);
+
+let subtle = selectLightDark(Color("#D9DCCF"), Color("#383838"));
+let highlight = selectLightDark(Color("#874BFD"), Color("#7D56F4"));
+let special = selectLightDark(Color("#43BF6D"), Color("#73F59F"));
+
+let divider = new Style()
+ .setString("•")
+ .padding(0, 1)
+ .foreground(subtle)
+ .string();
+
+let url = new Style().foreground(special).render;
+
+// Tabs.
+
+// let activeTabBorder = lipgloss.Border{
+// Top: "─",
+// Bottom: " ",
+// Left: "│",
+// Right: "│",
+// TopLeft: "╭",
+// TopRight: "╮",
+// BottomLeft: "┘",
+// BottomRight: "└",
+// }
+let activeTabBorder = normalBorder();
+
+// tabBorder = lipgloss.Border{
+// Top: "─",
+// Bottom: "─",
+// Left: "│",
+// Right: "│",
+// TopLeft: "╭",
+// TopRight: "╮",
+// BottomLeft: "┴",
+// BottomRight: "┴",
+// }
+let tabBorder = normalBorder();
+
+// tab = lipgloss.NewStyle().
+// Border(tabBorder, true).
+// BorderForeground(highlight).
+// Padding(0, 1)
+
+// activeTab = tab.Border(activeTabBorder, true)
+
+let tab = new Style()
+ .border(tabBorder, true)
+ .borderForeground(highlight)
+ .padding(0, 1).render;
+
+let activeTab = new Style()
+ .border(tabBorder, true)
+ .borderForeground(highlight)
+ .padding(0, 1)
+ .border(activeTabBorder, true).render;
+
+let tabGap = new Style()
+ .border(activeTabBorder, true)
+ .borderForeground(highlight)
+ .padding(0, 1)
+ .borderTop(false)
+ .borderLeft(false)
+ .borderRight(false);
+
+// Title.
+let titleStyle = new Style()
+ .marginLeft(1)
+ .marginRight(5)
+ .padding(0, 1)
+ .italic(true)
+ .foreground(Color("#FFF7DB"))
+ .setString("Lip Gloss");
+
+let descStyle = new Style().marginTop(1);
+
+let infoStyle = new Style()
+ .borderStyle(normalBorder())
+ .borderTop(true)
+ .borderForeground(subtle);
+
+let dialogBoxStyle = new Style()
+ .border(roundedBorder())
+ .borderForeground(Color("#874BFD"))
+ .padding(1, 0)
+ .borderTop(true)
+ .borderLeft(true)
+ .borderRight(true)
+ .borderBottom(true);
+
+let buttonStyle = new Style()
+ .foreground(Color("#FFF7DB"))
+ .background(Color("#888B7E"))
+ .padding(0, 3)
+ .marginTop(1);
+
+let activeButtonStyle = new Style()
+ .padding(0, 3)
+ .marginTop(1)
+ .foreground(Color("#FFF7DB"))
+ .background(Color("#F25D94"))
+ .marginRight(2)
+ .underline(true);
+
+// List.
+let list = new Style()
+ .border(normalBorder(), false, true, false, false)
+ .borderForeground(subtle)
+ .marginRight(1)
+ .height(8)
+ .width(width / 3).render;
+
+let listHeader = new Style()
+ .borderStyle(normalBorder())
+ .borderBottom(true)
+ .borderForeground(subtle)
+ .marginRight(2).render;
+
+let listItem = new Style().paddingLeft(2).render;
+
+let checkMark = new Style()
+ .setString("✓")
+ .foreground(special)
+ .paddingRight(1)
+ .string();
+
+function listDone(s) {
+ return (
+ checkMark +
+ new Style()
+ .strikethrough(true)
+ .foreground(selectLightDark(Color("#969B86"), Color("#696969")))
+ .render(s)
+ );
+}
+
+// Paragraphs/History.
+let historyStyle = new Style()
+ .align(Left)
+ .foreground(Color("#FAFAFA"))
+ .background(highlight)
+ .margin(1, 3, 0, 0)
+ .padding(1, 2)
+ .height(19)
+ .width(columnWidth);
+
+// Status Bar.
+
+let statusNugget = new Style().foreground(Color("#FFFDF5")).padding(0, 1);
+
+let statusBarStyle = new Style()
+ .foreground(selectLightDark(Color("#343433"), Color("#C1C6B2")))
+ .background(selectLightDark(Color("#D9DCCF"), Color("#353533")));
+
+let statusStyle = new Style()
+ .inherit(statusBarStyle)
+ .foreground(Color("#FFFDF5"))
+ .background(Color("#FF5F87"))
+ .padding(0, 1)
+ .marginRight(1);
+
+let encodingStyle = statusNugget.background(Color("#A550DF")).align(Right);
+
+let statusText = new Style().inherit(statusBarStyle);
+
+let fishCakeStyle = new Style()
+ .foreground(Color("#FFFDF5"))
+ .padding(0, 1)
+ .background(Color("#6124DF"));
+
+// Page.
+let docStyle = new Style().padding(1, 2, 1, 2);
+
+let physicalWidth = process.stdout.columns;
+let doc = "";
+
+// Tabs.
+{
+ let row = joinHorizontal(
+ Top,
+ activeTab("Lip Gloss"),
+ tab("Blush"),
+ tab("Eye Shadow"),
+ tab("Mascara"),
+ tab("Foundation"),
+ );
+ // console.log("oi", row.length);
+ // let repeat = Math.max(0, width - Width(row) - 2);
+ let repeat = 27;
+ let gap = tabGap.render(" ".repeat(repeat));
+ row = joinHorizontal(Bottom, row, gap);
+ doc += row + "\n\n";
+}
+
+// row := lipgloss.JoinHorizontal(
+// lipgloss.Top,
+// activeTab.Render("Lip Gloss"),
+// tab.Render("Blush"),
+// tab.Render("Eye Shadow"),
+// tab.Render("Mascara"),
+// tab.Render("Foundation"),
+// )
+// gap := tabGap.Render(strings.Repeat(" ", max(0, width-lipgloss.Width(row)-2)))
+// row = lipgloss.JoinHorizontal(lipgloss.Bottom, row, gap)
+// doc.WriteString(row + "\n\n")
+
+// Title.
+{
+ let colors = colorGrid(1, 5);
+ let title = "";
+
+ for (let i = 0; i < colors.length; i++) {
+ const offset = 2;
+ const v = colors[i];
+ let c = Color(v[0]);
+ title += titleStyle
+ .marginLeft(i * offset)
+ .background(c)
+ .render();
+ if (i < colors.length - 1) {
+ title += "\n";
+ }
+ }
+
+ let desc = joinVertical(
+ Left,
+ descStyle.render("Style Definitions for Nice Terminal Layouts"),
+ infoStyle.render(
+ "From Charm" + divider + url("https://github.com/charmbracelet/lipgloss"),
+ ),
+ );
+
+ let row = joinHorizontal(Top, title, desc);
+ doc += row + "\n\n";
+}
+
+// Dialog.
+{
+ let okButton = new Style().inherit(activeButtonStyle).render("Yes");
+ let cancelButton = new Style().inherit(buttonStyle).render("Maybe");
+
+ let grad = applyGradient(
+ "Are you sure you want to eat marmalade?",
+ "#EDFF82",
+ "#F25D94",
+ );
+
+ let question = new Style().width(50).align(Center).render(grad);
+
+ let buttons = joinHorizontal(Top, okButton, cancelButton);
+ let ui = joinVertical(Center, question, buttons);
+
+ let dialog = Place(
+ width,
+ 9,
+ Center,
+ Center,
+ dialogBoxStyle.render(ui),
+ // withWhitespaceChars("猫咪"),
+ // withWhitespaceStyle(new Style().foreground(subtle)),
+ );
+
+ doc += dialog + "\n\n";
+}
+
+// // Color grid.
+let colors = () => {
+ const colors = colorGrid(14, 8);
+
+ let b = "";
+ for (let i = 0; i < colors.length; i++) {
+ const x = colors[i];
+ for (let j = 0; j < x.length; j++) {
+ const y = x[j];
+ const s = new Style().setString(" ").background(Color(y));
+ b += s.render();
+ }
+ b += "\n";
+ }
+
+ return b;
+};
+
+// doc += colors();
+
+// let lists = joinHorizontal(
+// Top,
+// list(
+// joinVertical(
+// Left,
+// listHeader("Citrus Fruits to Try"),
+// listDone("Grapefruit"),
+// listDone("Yuzu"),
+// listItem("Citron"),
+// listItem("Kumquat"),
+// listItem("Pomelo"),
+// ),
+// ),
+// list(
+// joinVertical(
+// Left,
+// listHeader("Actual Lip Gloss Vendors"),
+// listItem("Glossier"),
+// listItem("Claire‘s Boutique"),
+// listDone("Nyx"),
+// listItem("Mac"),
+// listDone("Milk"),
+// ),
+// ),
+// );
+
+doc += joinHorizontal(Top, new Style().marginLeft(1).render(colors()));
+
+// // Marmalade history.
+// {
+// const (
+// historyA = "The Romans learned from the Greeks that quinces slowly cooked with honey would “set” when cool. The Apicius gives a recipe for preserving whole quinces, stems and leaves attached, in a bath of honey diluted with defrutum: Roman marmalade. Preserves of quince and lemon appear (along with rose, apple, plum and pear) in the Book of ceremonies of the Byzantine Emperor Constantine VII Porphyrogennetos."
+// historyB = "Medieval quince preserves, which went by the French name cotignac, produced in a clear version and a fruit pulp version, began to lose their medieval seasoning of spices in the 16th century. In the 17th century, La Varenne provided recipes for both thick and clear cotignac."
+// historyC = "In 1524, Henry VIII, King of England, received a “box of marmalade” from Mr. Hull of Exeter. This was probably marmelada, a solid quince paste from Portugal, still made and sold in southern Europe today. It became a favourite treat of Anne Boleyn and her ladies in waiting."
+// )
+
+// doc.WriteString(lipgloss.JoinHorizontal(
+// lipgloss.Top,
+// historyStyle.Align(lipgloss.Right).Render(historyA),
+// historyStyle.Align(lipgloss.Center).Render(historyB),
+// historyStyle.MarginRight(0).Render(historyC),
+// ))
+
+// doc.WriteString("\n\n")
+// }
+
+// Status bar.
+// {
+// let w = lipgloss.Width;
+
+// let lightDarkState = "Light";
+// if (hasDarkBG) {
+// lightDarkState = "Dark";
+// }
+
+// let statusKey = statusStyle.render("STATUS");
+// let encoding = encodingStyle.render("UTF-8");
+// let fishCake = fishCakeStyle.render("🍥 Fish Cake");
+// let statusVal = statusText.
+// width(width - w(statusKey) - w(encoding) - w(fishCake)).
+// render("Ravishingly " + lightDarkState + "!")
+
+// let bar = lipgloss.joinHorizontal(Top,
+// statusKey,
+// statusVal,
+// encoding,
+// fishCake,
+// )
+
+// doc += statusBarStyle.width(with)
+// doc.WriteString(statusBarStyle.Width(width).Render(bar))
+// }
+
+if (physicalWidth > 0) {
+ docStyle = docStyle.maxWidth(physicalWidth);
+}
+
+// Okay, let's print it. We use a special Lipgloss writer to downsample
+// colors to the terminal's color palette. And, if output's not a TTY, we
+// will remove color entirely.
+console.log("%s", docStyle.render(doc));
+
+/**
+ * Creates a grid of hex color values by blending between four corner colors
+ * @param {number} xSteps - Number of horizontal color steps
+ * @param {number} ySteps - Number of vertical color steps
+ * @returns {string[][]} 2D array of hex color strings
+ */
+function colorGrid(xSteps, ySteps) {
+ // Define the four corner colors in hex
+ const x0y0 = hexToRgb("#F25D94");
+ const x1y0 = hexToRgb("#EDFF82");
+ const x0y1 = hexToRgb("#643AFF");
+ const x1y1 = hexToRgb("#14F9D5");
+
+ // Create the left edge colors (x0)
+ const x0 = [];
+ for (let i = 0; i < ySteps; i++) {
+ x0.push(blendLuv(x0y0, x0y1, i / ySteps));
+ }
+
+ // Create the right edge colors (x1)
+ const x1 = [];
+ for (let i = 0; i < ySteps; i++) {
+ x1.push(blendLuv(x1y0, x1y1, i / ySteps));
+ }
+
+ // Create the color grid
+ const grid = [];
+ for (let x = 0; x < ySteps; x++) {
+ const y0 = x0[x];
+ const row = [];
+ for (let y = 0; y < xSteps; y++) {
+ row.push(rgbToHex(blendLuv(y0, x1[x], y / xSteps)));
+ }
+ grid.push(row);
+ }
+
+ return grid;
+}
+
+/**
+ * Converts hex color string to RGB object
+ * @param {string} hex - Color in hex format (e.g. "#F25D94")
+ * @returns {object} RGB color object with r, g, b properties
+ */
+function hexToRgb(hex) {
+ const r = parseInt(hex.slice(1, 3), 16);
+ const g = parseInt(hex.slice(3, 5), 16);
+ const b = parseInt(hex.slice(5, 7), 16);
+ return { r, g, b };
+}
+
+// /**
+// * Converts RGB object to hex color string
+// * @param {object} rgb - RGB color object with r, g, b properties
+// * @returns {string} Color in hex format (e.g. "#F25D94")
+// */
+function rgbToHex(rgb) {
+ const toHex = (c) => {
+ const hex = Math.round(c).toString(16);
+ return hex.length === 1 ? "0" + hex : hex;
+ };
+
+ return `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`;
+}
+
+/**
+ * Blend two colors in LUV color space (approximation)
+ * Note: This is a simplified approximation as JavaScript doesn't have built-in LUV color space.
+ * For accurate LUV blending, you would need to implement full color space conversion.
+ * @param {object} color1 - First RGB color
+ * @param {object} color2 - Second RGB color
+ * @param {number} t - Blend factor (0 to 1)
+ * @returns {object} Resulting blended RGB color
+ */
+function blendLuv(color1, color2, t) {
+ // For simplicity, we'll do a simple linear interpolation in RGB space
+ // For production, consider using a color library with proper LUV support
+ return {
+ r: color1.r + (color2.r - color1.r) * t,
+ g: color1.g + (color2.g - color1.g) * t,
+ b: color1.b + (color2.b - color1.b) * t,
+ };
+}
+
+/**
+ * Applies a color gradient to a string
+ * @param {string} input - The input string to apply gradient to
+ * @param {string|object} from - Starting color (hex string or RGB object)
+ * @param {string|object} to - Ending color (hex string or RGB object)
+ * @returns {string} String with gradient applied
+ */
+function applyGradient(input, from, to) {
+ // Convert colors to RGB if they're hex strings
+ const fromRgb = typeof from === "string" ? hexToRgb(from) : from;
+ const toRgb = typeof to === "string" ? hexToRgb(to) : to;
+
+ // Split the input string into graphemes (user-perceived characters)
+ // Since we don't have the uniseg library, we'll use a simple approach
+ // Note: This won't handle complex Unicode correctly like the original Go code
+ // For production use, consider using a library like "grapheme-splitter"
+ const chars = Array.from(input);
+
+ let output = "";
+
+ // Apply the gradient
+ for (let i = 0; i < chars.length; i++) {
+ // Calculate the blend ratio
+ const t = chars.length > 1 ? i / (chars.length - 1) : 0;
+
+ // Blend the colors
+ const blendedColor = blendLuv(fromRgb, toRgb, t);
+
+ // Convert back to hex
+ const hex = rgbToHex(blendedColor);
+
+ // Apply the style and add to output
+ // Assuming baseStyle has a method similar to lipgloss.Style's Foreground() and Render()
+ output += new Style().foreground(Color(hex)).render(chars[i]);
+ }
+
+ return output;
+}
diff --git a/bindings/examples/list.js b/bindings/examples/list.js
new file mode 100644
index 00000000..8f5d315d
--- /dev/null
+++ b/bindings/examples/list.js
@@ -0,0 +1,71 @@
+const {
+ List,
+ Style,
+ Color,
+ Bullet,
+ Arabic,
+ Alphabet,
+ Dash,
+} = require("@charmland/lipgloss");
+
+// Simple bullet list
+console.log("Simple bullet list:");
+const groceries = new List(
+ "Bananas",
+ "Barley",
+ "Cashews",
+ "Milk",
+ "Eggs"
+).enumerator(Bullet);
+
+console.log(groceries.render());
+
+// Numbered list with styling
+console.log("\nNumbered list with styling:");
+const purple = Color("99");
+const gray = Color("245");
+
+let itemStyle = new Style().foreground(gray).padding(0, 1);
+let enumeratorStyle = new Style().foreground(purple).bold(true);
+
+const tasks = new List()
+ .item("Write documentation")
+ .item("Add tests")
+ .item("Review code")
+ .item("Deploy to production")
+ .enumerator(Arabic)
+ .itemStyle(itemStyle)
+ .enumeratorStyle(enumeratorStyle);
+
+console.log(tasks.render());
+
+// Nested list
+console.log("\nNested list:");
+const subList = new List(
+ "Almond Milk",
+ "Coconut Milk",
+ "Full Fat Milk"
+).enumerator(Dash);
+
+const nestedGroceries = new List(
+ "Bananas",
+ "Barley",
+ "Cashews",
+ subList,
+ "Eggs",
+ "Fish Cake"
+).enumerator(Bullet);
+
+console.log(nestedGroceries.render());
+
+// Alphabetical list
+console.log("\nAlphabetical list:");
+const alphabet = new List(
+ "Apple",
+ "Banana",
+ "Cherry",
+ "Date",
+ "Elderberry"
+).enumerator(Alphabet);
+
+console.log(alphabet.render());
\ No newline at end of file
diff --git a/bindings/examples/makeup.js b/bindings/examples/makeup.js
new file mode 100644
index 00000000..24881a44
--- /dev/null
+++ b/bindings/examples/makeup.js
@@ -0,0 +1,38 @@
+const {
+ Tree,
+ Style,
+ Color,
+ RoundedEnumerator,
+} = require("@charmland/lipgloss");
+
+// Create styles matching the Go example
+const enumeratorStyle = new Style()
+ .foreground(Color("63"))
+ .marginRight(1);
+
+const rootStyle = new Style()
+ .foreground(Color("35"));
+
+const itemStyle = new Style()
+ .foreground(Color("212"));
+
+// Create the tree structure
+const makeupTree = new Tree()
+ .root("⁜ Makeup")
+ .child(
+ "Glossier",
+ "Fenty Beauty",
+ new Tree().child(
+ "Gloss Bomb Universal Lip Luminizer",
+ "Hot Cheeks Velour Blushlighter"
+ ),
+ "Nyx",
+ "Mac",
+ "Milk"
+ )
+ .enumerator(RoundedEnumerator)
+ .enumeratorStyle(enumeratorStyle)
+ .rootStyle(rootStyle)
+ .itemStyle(itemStyle);
+
+console.log(makeupTree.render());
\ No newline at end of file
diff --git a/bindings/examples/package-lock.json b/bindings/examples/package-lock.json
new file mode 100644
index 00000000..80039640
--- /dev/null
+++ b/bindings/examples/package-lock.json
@@ -0,0 +1,31 @@
+{
+ "name": "wasm-test",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "wasm-test",
+ "version": "1.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "@charmland/lipgloss": "file:../"
+ }
+ },
+ "..": {
+ "name": "@charmland/lipgloss",
+ "version": "0.0.1",
+ "license": "MIT"
+ },
+ "../wasm": {
+ "name": "@charmland/lipgloss",
+ "version": "0.0.1",
+ "extraneous": true,
+ "license": "MIT"
+ },
+ "node_modules/@charmland/lipgloss": {
+ "resolved": "..",
+ "link": true
+ }
+ }
+}
diff --git a/bindings/examples/package.json b/bindings/examples/package.json
new file mode 100644
index 00000000..f8d9ffdc
--- /dev/null
+++ b/bindings/examples/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "wasm-test",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "start": "node size.js && node table.js && node color.js",
+ "examples": "node size.js && node color.js && node list.js && node table-data.js",
+ "size": "node size.js",
+ "table": "node table.js",
+ "color": "node color.js",
+ "test": "node test-runner.js",
+ "test:simple": "node tests/test-simple-table.js",
+ "test:style": "node tests/test-style-function.js",
+ "test:ascii": "node tests/test-ascii-table.js",
+ "test:comprehensive": "node tests/test-comprehensive.js",
+ "test:table-data": "node table-data.js",
+ "test:all": "npm run test && npm run test:ascii",
+ "bench": ""
+ },
+ "keywords": [],
+ "author": "",
+ "license": "MIT",
+ "type": "commonjs",
+ "dependencies": {
+ "@charmland/lipgloss": "file:../"
+ }
+}
diff --git a/bindings/examples/pokemonList.js b/bindings/examples/pokemonList.js
new file mode 100644
index 00000000..57cbff6b
--- /dev/null
+++ b/bindings/examples/pokemonList.js
@@ -0,0 +1,175 @@
+const {
+ Table,
+ Style,
+ Color,
+ normalBorder,
+ Center,
+} = require("@charmland/lipgloss");
+const readline = require("readline");
+
+// Create styles once
+const purple = Color("99");
+const gray = Color("245");
+const lightGray = Color("241");
+const headerStyle = new Style().foreground(purple).bold(true).align(Center);
+const oddRowStyle = new Style().padding(0, 1).width(16).foreground(gray);
+const evenRowStyle = new Style().padding(0, 1).width(16).foreground(lightGray);
+const borderStyle = new Style().foreground(purple);
+
+const typeColors = {
+ Grass: "#78C850",
+ Poison: "#A040A0",
+ Fire: "#F08030",
+ Flying: "#A890F0",
+ Water: "#6890F0",
+ Rock: "#B8A038",
+ Electric: "#F8D030",
+ Bug: "#A8B820",
+ Normal: "#A8A878",
+ Fighting: "#C03028",
+ Ice: "#98D8D8",
+ Psychic: "#F85888",
+ Ground: "#E0C068",
+ Ghost: "#705898",
+ Dragon: "#7038F8",
+ Dark: "#705848",
+ Steel: "#B8B8D0",
+ Fairy: "#EE99AC",
+};
+
+// Function to create a colored cell for types
+function createColorCell(typeString) {
+ const types = typeString.split("/");
+ return types.map(type => {
+ const colorMap = {
+ 'Grass': '🌿',
+ 'Poison': '☠️',
+ 'Fire': '🔥',
+ 'Flying': '🪶',
+ 'Water': '💧',
+ 'Rock': '🪨',
+ 'Electric': '⚡',
+ 'Bug': '🐛',
+ 'Normal': '⚪',
+ 'Fighting': '👊',
+ 'Ice': '❄️',
+ 'Psychic': '🔮',
+ 'Ground': '🌍',
+ 'Ghost': '👻',
+ 'Dragon': '🐉',
+ 'Dark': '🌑',
+ 'Steel': '⚙️',
+ 'Fairy': '🧚'
+ };
+ return (colorMap[type] || '●') + type;
+ }).join('/');
+}
+
+// Pokemon data
+const pokemonData = [
+ [1, "Bulbasaur", createColorCell("Grass/Poison"), "Fire, Flying"],
+ [2, "Ivysaur", createColorCell("Grass/Poison"), "Fire, Flying"],
+ [3, "Venusaur", createColorCell("Grass/Poison"), "Fire, Flying"],
+ [4, "Charmander", createColorCell("Fire"), "Water, Rock"],
+ [5, "Charmeleon", createColorCell("Fire"), "Water, Rock"],
+ [6, "Charizard", createColorCell("Fire/Flying"), "Water, Electric"],
+ [7, "Squirtle", createColorCell("Water"), "Grass, Electric"],
+ [8, "Wartortle", createColorCell("Water"), "Grass, Electric"],
+ [9, "Blastoise", createColorCell("Water"), "Grass, Electric"],
+ [10, "Caterpie", createColorCell("Bug"), "Fire, Flying"],
+ [11, "Metapod", createColorCell("Bug"), "Fire, Flying"],
+ [12, "Butterfree", createColorCell("Bug/Flying"), "Fire, Electric"],
+ [13, "Weedle", createColorCell("Bug/Poison"), "Fire, Flying"],
+ [14, "Kakuna", createColorCell("Bug/Poison"), "Fire, Flying"],
+ [15, "Beedrill", createColorCell("Bug/Poison"), "Fire, Flying"],
+ [16, "Pidgey", createColorCell("Normal/Flying"), "Electric, Ice"],
+ [17, "Pidgeotto", createColorCell("Normal/Flying"), "Electric, Ice"],
+ [18, "Pidgeot", createColorCell("Normal/Flying"), "Electric, Ice"],
+ [19, "Rattata", createColorCell("Normal"), "Fighting"],
+ [20, "Raticate", createColorCell("Normal"), "Fighting"],
+];
+
+// Pagination variables
+const rowsPerPage = 5;
+let currentPage = 0;
+const totalPages = Math.ceil(pokemonData.length / rowsPerPage);
+
+// Cache for rendered pages
+const pageCache = new Map();
+
+// Function to render a specific page on-demand
+function renderPageToCache(page) {
+ if (pageCache.has(page)) {
+ return pageCache.get(page);
+ }
+
+ console.log(`Rendering page ${page + 1}...`);
+
+ // Calculate the start and end indices for the page
+ const startIndex = page * rowsPerPage;
+ const endIndex = Math.min((page + 1) * rowsPerPage, pokemonData.length);
+ const currentRows = pokemonData.slice(startIndex, endIndex);
+
+ // Create the table for this specific page
+ const table = new Table()
+ .border(normalBorder())
+ .borderStyle(borderStyle)
+ .headers("#", "NAME", "TYPE", "WEAKNESS")
+ .styleFunc((row, col) => {
+ if (row === -1) {
+ return headerStyle;
+ }
+ // Calculate absolute row for consistent styling
+ const absoluteRow = startIndex + row;
+ return absoluteRow % 2 === 0 ? evenRowStyle : oddRowStyle;
+ })
+ .rows(currentRows);
+
+ // Render and cache the result
+ const renderedTable = table.render();
+ pageCache.set(page, renderedTable);
+
+ return renderedTable;
+}
+
+// Function to display a page (from cache or render on-demand)
+function renderTable(page) {
+ const renderedTable = renderPageToCache(page);
+
+ console.clear();
+ console.log(renderedTable);
+ console.log(`\nPage ${page + 1} of ${totalPages} | Use ↑ and ↓ arrow keys to navigate | Press 'q' to quit`);
+}
+
+// Pre-load the first page for instant startup
+renderPageToCache(0);
+
+// Set up readline for arrow key navigation
+readline.emitKeypressEvents(process.stdin);
+if (process.stdin.isTTY) {
+ process.stdin.setRawMode(true);
+}
+
+// Initial render
+renderTable(currentPage);
+
+// Handle keypress events
+process.stdin.on("keypress", (str, key) => {
+ if (key.name === "q" || (key.ctrl && key.name === "c")) {
+ process.exit();
+ }
+
+ if (key.name === "down") {
+ if (currentPage < totalPages - 1) {
+ currentPage++;
+ renderTable(currentPage);
+ }
+ } else if (key.name === "up") {
+ if (currentPage > 0) {
+ currentPage--;
+ renderTable(currentPage);
+ }
+ }
+});
+
+console.log("Use ↑ and ↓ arrow keys to navigate through the Pokémon table pages. Press 'q' to quit.");
diff --git a/bindings/examples/size.js b/bindings/examples/size.js
new file mode 100644
index 00000000..2dc78295
--- /dev/null
+++ b/bindings/examples/size.js
@@ -0,0 +1,3 @@
+const { Size } = require("@charmland/lipgloss");
+
+console.log(Size("Lip Gloss is awesome!"));
diff --git a/bindings/examples/styledTree.js b/bindings/examples/styledTree.js
new file mode 100644
index 00000000..85c7033c
--- /dev/null
+++ b/bindings/examples/styledTree.js
@@ -0,0 +1,30 @@
+const {
+ Tree,
+ Style,
+ Color,
+} = require("@charmland/lipgloss");
+
+// Create styles matching the Go example
+const purple = new Style()
+ .foreground(Color("99"))
+ .marginRight(1);
+
+const pink = new Style()
+ .foreground(Color("212"))
+ .marginRight(1);
+
+// Create the tree structure
+const t = new Tree()
+ .child(
+ "Glossier",
+ "Claire's Boutique",
+ new Tree()
+ .root("Nyx")
+ .child("Lip Gloss", "Foundation")
+ .enumeratorStyle(pink),
+ "Mac",
+ "Milk"
+ )
+ .enumeratorStyle(purple);
+
+console.log(t.render());
\ No newline at end of file
diff --git a/bindings/examples/sublist.js b/bindings/examples/sublist.js
new file mode 100644
index 00000000..cca627e0
--- /dev/null
+++ b/bindings/examples/sublist.js
@@ -0,0 +1,224 @@
+const {
+ List,
+ Style,
+ Color,
+ Dash,
+ Roman,
+ Center,
+} = require("@charmland/lipgloss");
+
+// Helper function to create color grid (simplified version)
+function colorGrid(xSteps, ySteps) {
+ const colors = [
+ ["#F25D94", "#EDFF82"],
+ ["#E85A4F", "#D4E157"],
+ ["#D32F2F", "#C0CA33"],
+ ["#7B1FA2", "#8BC34A"],
+ ["#643AFF", "#14F9D5"],
+ ];
+ return colors;
+}
+
+// Styles
+const purple = new Style().foreground(Color("99")).marginRight(1);
+
+const pink = new Style().foreground(Color("212")).marginRight(1);
+
+const base = new Style().marginBottom(1).marginLeft(1);
+
+const faint = new Style().faint(true);
+
+const dim = Color("250");
+const highlight = Color("#EE6FF8");
+const special = Color("#73F59F");
+
+// Checklist functions
+const checklistEnumStyle = (items, index) => {
+ const itemIndex = items;
+ if ([1, 2, 4].includes(itemIndex)) {
+ return new Style().foreground(special).paddingRight(1);
+ }
+ return new Style().paddingRight(1);
+};
+
+const checklistEnum = (items, index) => {
+ if ([1, 2, 4].includes(index)) {
+ return "✓";
+ }
+ return "•";
+};
+
+const checklistStyle = (items, index) => {
+ // The 'items' parameter is actually the item index, 'index' is always 0
+ const itemIndex = items;
+
+ // Items with ✓ should be strikethrough: indices 1, 2, 4 (Yuzu, Citron, Pomelo) and 1, 2, 4 (Claire's, Nyx, Milk)
+ if ([1, 2, 4].includes(itemIndex)) {
+ return new Style().strikethrough(true).foreground(Color("#696969"));
+ }
+ return new Style();
+};
+
+// Helper function to create styled checklist items
+const createChecklistItem = (text, isChecked) => {
+ if (isChecked) {
+ const checkmark = new Style().foreground(special).render("✓");
+ const itemText = new Style()
+ .strikethrough(true)
+ .foreground(Color("#696969"))
+ .render(" " + text);
+ return checkmark + itemText;
+ } else {
+ return "• " + text;
+ }
+};
+
+const colors = colorGrid(1, 5);
+
+const titleStyle = new Style().italic(true).foreground(Color("#FFF7DB"));
+
+const lipglossStyleFunc = (items, index) => {
+ // The 'items' parameter is actually the item index, 'index' is always 0
+ const itemIndex = items;
+ const itemsLength = 5; // We know there are 5 items in this list
+
+ if (itemIndex === itemsLength - 1) {
+ return titleStyle
+ .padding(1, 2)
+ .margin(0, 0, 1, 0)
+ .maxWidth(20)
+ .background(Color(colors[itemIndex][0]));
+ }
+ return titleStyle
+ .padding(0, 5 - itemIndex, 0, itemIndex + 2)
+ .maxWidth(20)
+ .background(Color(colors[itemIndex][0]));
+};
+
+const history =
+ "Medieval quince preserves, which went by the French name cotignac, produced in a clear version and a fruit pulp version, began to lose their medieval seasoning of spices in the 16th century. In the 17th century, La Varenne provided recipes for both thick and clear cotignac.";
+
+// Create the complex nested list
+const l = new List()
+ .enumeratorStyle(purple)
+ .item("Lip Gloss")
+ .item("Blush")
+ .item("Eye Shadow")
+ .item("Mascara")
+ .item("Foundation")
+ .item(
+ new List()
+ .enumeratorStyle(pink)
+ .item("Citrus Fruits to Try")
+ .item(
+ new List()
+ .enumerator(Dash)
+ .enumeratorStyle(new Style().foreground(Color("0")).width(0))
+ .item(createChecklistItem("Grapefruit", false))
+ .item(createChecklistItem("Yuzu", true))
+ .item(createChecklistItem("Citron", true))
+ .item(createChecklistItem("Kumquat", false))
+ .item(createChecklistItem("Pomelo", true)),
+ )
+ .item("Actual Lip Gloss Vendors")
+ .item(
+ new List()
+ .enumerator(Dash)
+ .enumeratorStyle(new Style().foreground(Color("0")).width(0))
+ .item(createChecklistItem("Glossier", false))
+ .item(createChecklistItem("Claire's Boutique", true))
+ .item(createChecklistItem("Nyx", true))
+ .item(createChecklistItem("Mac", false))
+ .item(createChecklistItem("Milk", true))
+ .item(
+ new List()
+ .enumeratorStyle(purple)
+ .enumerator(Dash)
+ .itemStyleFunc(lipglossStyleFunc)
+ .item("Lip Gloss")
+ .item("Lip Gloss")
+ .item("Lip Gloss")
+ .item("Lip Gloss")
+ .item(
+ new List()
+ .enumeratorStyle(
+ new Style().foreground(Color(colors[4][0])).marginRight(1),
+ )
+ .item("\nStyle Definitions for Nice Terminal Layouts\n─────")
+ .item("From Charm")
+ .item("https://github.com/charmbracelet/lipgloss")
+ .item(
+ new List()
+ .enumeratorStyle(
+ new Style()
+ .foreground(Color(colors[3][0]))
+ .marginRight(1),
+ )
+ .item("Emperors: Julio-Claudian dynasty")
+ .item(
+ new Style()
+ .padding(1)
+ .render(
+ new List(
+ "Augustus",
+ "Tiberius",
+ "Caligula",
+ "Claudius",
+ "Nero",
+ )
+ .enumerator(Roman)
+ .render(),
+ ),
+ )
+ .item(
+ new Style()
+ .bold(true)
+ .foreground(Color("#FAFAFA"))
+ .background(Color("#7D56F4"))
+ .align(Center, Center)
+ .padding(1, 3)
+ .margin(0, 1, 1, 1)
+ .width(40)
+ .render(history),
+ )
+ .item("Simple table placeholder")
+ .item("Documents")
+ .item(
+ new List()
+ .enumerator((items, i) => {
+ if (i === 1) {
+ return "│\n│";
+ }
+ return " ";
+ })
+ .itemStyleFunc((items, i) => {
+ if (i === 1) {
+ return base.foreground(highlight);
+ }
+ return base.foreground(dim);
+ })
+ .enumeratorStyleFunc((items, i) => {
+ if (i === 1) {
+ return new Style().foreground(highlight);
+ }
+ return new Style().foreground(dim);
+ })
+ .item("Foo Document\n" + faint.render("1 day ago"))
+ .item("Bar Document\n" + faint.render("2 days ago"))
+ .item(
+ "Baz Document\n" + faint.render("10 minutes ago"),
+ )
+ .item("Qux Document\n" + faint.render("1 month ago")),
+ )
+ .item("EOF"),
+ )
+ .item("go get github.com/charmbracelet/lipgloss/list\n"),
+ )
+ .item("See ya later"),
+ ),
+ )
+ .item("List"),
+ )
+ .item("xoxo, Charm_™");
+
+console.log(l.render());
diff --git a/bindings/examples/table-data.js b/bindings/examples/table-data.js
new file mode 100644
index 00000000..fecc1235
--- /dev/null
+++ b/bindings/examples/table-data.js
@@ -0,0 +1,109 @@
+const {
+ Table,
+ TableData,
+ Style,
+ Color,
+ normalBorder,
+ Center,
+} = require("@charmland/lipgloss");
+
+// Example 1: Basic TableData usage
+
+const data = new TableData()
+ .append(["Name", "Age", "City"])
+ .append(["Alice", "25", "New York"])
+ .append(["Bob", "30", "San Francisco"])
+ .append(["Charlie", "35", "Chicago"]);
+
+const basicTable = new Table().data(data).border(normalBorder()).render();
+
+console.log(basicTable);
+
+// Example 2: TableData with multiple rows at once
+
+const employeeData = new TableData(
+ ["Employee ID", "Name", "Department", "Salary"],
+ ["001", "John Doe", "Engineering", "$75,000"],
+ ["002", "Jane Smith", "Marketing", "$65,000"],
+ ["003", "Mike Johnson", "Sales", "$70,000"],
+ ["004", "Sarah Wilson", "HR", "$60,000"],
+);
+
+const purple = Color("99");
+const gray = Color("245");
+const lightGray = Color("241");
+
+let headerStyle = new Style().foreground(purple).bold(true).align(Center);
+let oddRowStyle = new Style().padding(0, 1).foreground(gray);
+let evenRowStyle = new Style().padding(0, 1).foreground(lightGray);
+
+const styledTable = new Table()
+ .data(employeeData)
+ .border(normalBorder())
+ .borderStyle(new Style().foreground(purple))
+ .styleFunc((row, col) => {
+ if (row === -1) {
+ return headerStyle;
+ } else if (row % 2 === 0) {
+ return evenRowStyle;
+ } else {
+ return oddRowStyle;
+ }
+ })
+ .render();
+
+console.log(styledTable);
+
+// Example 3: Accessing individual cells
+
+console.log(
+ `Data has ${employeeData.rowCount()} rows and ${employeeData.columnCount()} columns`,
+);
+console.log(`Cell at (1, 1): "${employeeData.at(1, 1)}"`);
+console.log(`Cell at (2, 2): "${employeeData.at(2, 2)}"`);
+console.log(`Cell at (3, 3): "${employeeData.at(3, 3)}"`);
+
+// Example 4: Building data dynamically
+
+const dynamicData = new TableData().append(["Product", "Price", "Stock"]);
+
+// Simulate adding products dynamically
+const products = [
+ ["Laptop", "$999", "15"],
+ ["Mouse", "$25", "50"],
+ ["Keyboard", "$75", "30"],
+ ["Monitor", "$299", "8"],
+];
+
+products.forEach((product) => {
+ dynamicData.append(product);
+});
+
+const productTable = new Table()
+ .data(dynamicData)
+ .border(normalBorder())
+ .borderStyle(new Style().foreground(Color("cyan")))
+ .styleFunc((row, col) => {
+ if (row === -1) {
+ return new Style().foreground(Color("cyan")).bold(true).align(Center);
+ }
+ if (col === 1) {
+ // Price column
+ return new Style().foreground(Color("green")).align(Center);
+ }
+ if (col === 2) {
+ // Stock column
+ const stock = parseInt(dynamicData.at(row, col));
+ if (stock < 10) {
+ return new Style().foreground(Color("red")).align(Center);
+ } else if (stock < 20) {
+ return new Style().foreground(Color("yellow")).align(Center);
+ } else {
+ return new Style().foreground(Color("green")).align(Center);
+ }
+ }
+ return new Style();
+ })
+ .render();
+
+console.log(productTable);
diff --git a/bindings/examples/table.js b/bindings/examples/table.js
new file mode 100644
index 00000000..7f198d81
--- /dev/null
+++ b/bindings/examples/table.js
@@ -0,0 +1,50 @@
+const {
+ Table,
+ Style,
+ Color,
+ normalBorder,
+ Center,
+} = require("@charmland/lipgloss");
+
+var s = new Style().foreground(Color("240")).render;
+console.log(
+ new Table()
+ .wrap(false)
+ .headers("Drink", "Description")
+ .row("Bubble Tea", s("Milky"))
+ .row("Milk Tea", s("Also milky"))
+ .row("Actual milk", s("Milky as well"))
+ .render(),
+);
+
+const purple = Color("99");
+const gray = Color("245");
+const lightGray = Color("241");
+
+let headerStyle = new Style().foreground(purple).bold(true).align(Center);
+let oddRowStyle = new Style().padding(0, 1).width(14).foreground(gray);
+let evenRowStyle = new Style().padding(0, 1).width(14).foreground(lightGray);
+
+let rows = [
+ ["Chinese", "您好", "你好"],
+ ["Japanese", "こんにちは", "やあ"],
+ ["Arabic", "أهلين", "أهلا"],
+ ["Russian", "Здравствуйте", "Привет"],
+ ["Spanish", "Hola", "¿Qué tal?"],
+];
+const t = new Table()
+ .border(normalBorder())
+ .borderStyle(new Style().foreground(purple))
+ .styleFunc((row, col) => {
+ if (row === -1) {
+ return headerStyle;
+ } else if (row % 2 === 0) {
+ return evenRowStyle;
+ } else {
+ return oddRowStyle;
+ }
+ })
+ .headers("LANGUAGE", "FORMAL", "INFORMAL")
+ .rows(rows);
+
+console.log(t.render());
diff --git a/bindings/examples/test-checklist.cjs b/bindings/examples/test-checklist.cjs
new file mode 100644
index 00000000..32222355
--- /dev/null
+++ b/bindings/examples/test-checklist.cjs
@@ -0,0 +1,33 @@
+const {
+ List,
+ Style,
+ Color,
+} = require("@charmland/lipgloss");
+
+const checklistEnum = (items, index) => {
+ console.log(`checklistEnum called with index ${index}`);
+ if ([1, 2].includes(index)) {
+ return "✓";
+ }
+ return "•";
+};
+
+const checklistStyle = (items, index) => {
+ console.log(`checklistStyle called with index ${index}`);
+ if ([1, 2].includes(index)) {
+ return new Style()
+ .strikethrough(true)
+ .foreground(Color("#696969"));
+ }
+ return new Style();
+};
+
+const testList = new List()
+ .enumerator(checklistEnum)
+ .itemStyleFunc(checklistStyle)
+ .item("First item")
+ .item("Second item (should be checked)")
+ .item("Third item (should be checked)")
+ .item("Fourth item");
+
+console.log(testList.render());
\ No newline at end of file
diff --git a/bindings/examples/toggle.js b/bindings/examples/toggle.js
new file mode 100644
index 00000000..e5d7f4a1
--- /dev/null
+++ b/bindings/examples/toggle.js
@@ -0,0 +1,56 @@
+const {
+ Tree,
+ Style,
+ Color,
+ RoundedEnumerator,
+} = require("@charmland/lipgloss");
+
+function main() {
+ // Create styles that match the Go version exactly
+ const base = new Style().background(Color("57")).foreground(Color("225"));
+
+ const block = base.padding(1, 3).margin(1, 3).width(40);
+
+ const enumerator = new Style()
+ .background(Color("57"))
+ .foreground(Color("212"))
+ .paddingRight(1);
+
+ const toggle = new Style()
+ .background(Color("57"))
+ .foreground(Color("207"))
+ .paddingRight(1);
+
+ const dir = new Style().background(Color("57")).foreground(Color("225"));
+
+ const file = new Style().background(Color("57")).foreground(Color("225"));
+
+ // Create the tree structure exactly like the Go version
+ const t = new Tree()
+ .root(toggle.render("▼") + dir.render("~/charm"))
+ .enumerator(RoundedEnumerator)
+ .enumeratorStyle(enumerator)
+ .child(
+ toggle.render("▶") + dir.render("ayman"),
+ new Tree()
+ .root(toggle.render("▼") + dir.render("bash"))
+ .child(
+ new Tree()
+ .root(toggle.render("▼") + dir.render("tools"))
+ .child(file.render("zsh"), file.render("doom-emacs")),
+ ),
+ new Tree()
+ .root(toggle.render("▼") + dir.render("carlos"))
+ .child(
+ new Tree()
+ .root(toggle.render("▼") + dir.render("emotes"))
+ .child(file.render("chefkiss.png"), file.render("kekw.png")),
+ ),
+ toggle.render("▶") + dir.render("maas"),
+ );
+
+ // Apply the block styling to the entire tree output
+ console.log(block.render(t.render()));
+}
+
+main();
diff --git a/bindings/main.go b/bindings/main.go
new file mode 100644
index 00000000..67de3a13
--- /dev/null
+++ b/bindings/main.go
@@ -0,0 +1,326 @@
+package main
+
+import (
+ "os"
+ "unsafe"
+
+ "github.com/charmbracelet/colorprofile"
+ "github.com/charmbracelet/lipgloss/v2"
+ "github.com/charmbracelet/lipgloss/v2/compat"
+ "github.com/charmbracelet/lipgloss/v2/list"
+ _ "github.com/charmbracelet/lipgloss/v2/table"
+ "github.com/charmbracelet/lipgloss/v2/tree"
+)
+
+// Note: this function is used on wasm module initalization.
+func main() {}
+
+//go:export DetectFromEnvVars
+func wasmDetectFromEnvVars(ptrArray *uint32, count int) {
+ // Convert to a slice of pointers
+ pointerArray := unsafe.Slice(ptrArray, count*2)
+ vars := []string{}
+
+ for i := range count {
+ strPtr := uintptr(pointerArray[i*2])
+ strLen := int(pointerArray[i*2+1])
+
+ // Create a byte slice and convert to string
+ bytes := unsafe.Slice((*byte)(unsafe.Pointer(strPtr)), strLen)
+ str := string(bytes)
+ vars = append(vars, str)
+ }
+
+ compat.Profile = colorprofile.Env(vars) // truecolor
+ lipgloss.Writer = &colorprofile.Writer{
+ Forward: os.Stdout,
+ Profile: colorprofile.Env(vars),
+ }
+}
+
+var buf [1024]byte
+
+//go:export getBuffer
+func GetBuffer() *byte {
+ return &buf[0]
+}
+
+// TinyGo-specific memory management
+// Use a simple memory pool approach instead of complex tracking
+var memoryPool [][]byte
+var poolIndex int
+
+//go:export wasmMalloc
+func wasmMalloc(size int) uintptr {
+ if size <= 0 {
+ return 0
+ }
+
+ // For TinyGo, use a simpler approach with pre-allocated pools
+ // This avoids the string length overflow issues
+ data := make([]byte, size)
+
+ // Store in pool to prevent GC
+ if poolIndex >= len(memoryPool) {
+ memoryPool = append(memoryPool, make([][]byte, 100)...)
+ }
+ memoryPool[poolIndex] = data
+ poolIndex++
+
+ return uintptr(unsafe.Pointer(&data[0]))
+}
+
+//go:export wasmFree
+func wasmFree(ptr uintptr) {
+ // For TinyGo, we'll rely on periodic cleanup rather than immediate freeing
+ // This is more compatible with TinyGo's GC
+}
+
+//go:export wasmGC
+func wasmGC() {
+ // Periodic cleanup for TinyGo
+ if poolIndex > 1000 {
+ // Reset the pool periodically to prevent memory buildup
+ memoryPool = memoryPool[:0]
+ poolIndex = 0
+ }
+}
+
+//go:export getMemorySize
+func wasmGetMemorySize() int {
+ // Return a reasonable estimate for TinyGo
+ return 16 * 1024 * 1024 // 16MB
+}
+
+//go:export getAllocatedSize
+func wasmGetAllocatedSize() int {
+ return poolIndex * 1024 // Rough estimate
+}
+
+// List exports
+
+//go:export ListEnumeratorAlphabet
+func wasmListEnumeratorAlphabet() int32 {
+ return 0 // Alphabet
+}
+
+//go:export ListEnumeratorArabic
+func wasmListEnumeratorArabic() int32 {
+ return 1 // Arabic
+}
+
+//go:export ListEnumeratorBullet
+func wasmListEnumeratorBullet() int32 {
+ return 2 // Bullet
+}
+
+//go:export ListEnumeratorDash
+func wasmListEnumeratorDash() int32 {
+ return 3 // Dash
+}
+
+//go:export ListEnumeratorRoman
+func wasmListEnumeratorRoman() int32 {
+ return 4 // Roman
+}
+
+//go:export ListEnumeratorAsterisk
+func wasmListEnumeratorAsterisk() int32 {
+ return 5 // Asterisk
+}
+
+//go:export ListItem
+func wasmListItem(listPtr uintptr, strPtr uintptr, strLen int) {
+ l := (*list.List)(unsafe.Pointer(listPtr))
+ bytes := unsafe.Slice((*byte)(unsafe.Pointer(strPtr)), strLen)
+ str := string(bytes)
+ l.Item(str)
+}
+
+//go:export ListItemList
+func wasmListItemList(listPtr uintptr, itemListPtr uintptr) {
+ l := (*list.List)(unsafe.Pointer(listPtr))
+ itemList := (*list.List)(unsafe.Pointer(itemListPtr))
+ l.Item(itemList)
+}
+
+//go:export ListOffset
+func wasmListOffset(listPtr uintptr, start int, end int) {
+ l := (*list.List)(unsafe.Pointer(listPtr))
+ l.Offset(start, end)
+}
+
+//go:export ListEnumeratorStyle
+func wasmListEnumeratorStyle(listPtr uintptr, stylePtr uintptr) {
+ l := (*list.List)(unsafe.Pointer(listPtr))
+ style := (*lipgloss.Style)(unsafe.Pointer(stylePtr))
+ l.EnumeratorStyle(*style)
+}
+
+//go:export ListItemStyle
+func wasmListItemStyle(listPtr uintptr, stylePtr uintptr) {
+ l := (*list.List)(unsafe.Pointer(listPtr))
+ style := (*lipgloss.Style)(unsafe.Pointer(stylePtr))
+ l.ItemStyle(*style)
+}
+
+//go:export ListEnumerator
+func wasmListEnumerator(listPtr uintptr, enumType int32) {
+ l := (*list.List)(unsafe.Pointer(listPtr))
+ switch enumType {
+ case 0:
+ l.Enumerator(list.Alphabet)
+ case 1:
+ l.Enumerator(list.Arabic)
+ case 2:
+ l.Enumerator(list.Bullet)
+ case 3:
+ l.Enumerator(list.Dash)
+ case 4:
+ l.Enumerator(list.Roman)
+ case 5:
+ l.Enumerator(list.Asterisk)
+ }
+}
+
+// Tree exports
+
+//go:export TreeNew
+func wasmTreeNew() uintptr {
+ t := tree.New()
+ return uintptr(unsafe.Pointer(t))
+}
+
+//go:export TreeRoot
+func wasmTreeRoot(treePtr uintptr, strPtr uintptr, strLen int) {
+ t := (*tree.Tree)(unsafe.Pointer(treePtr))
+ bytes := unsafe.Slice((*byte)(unsafe.Pointer(strPtr)), strLen)
+ str := string(bytes)
+ t.Root(str)
+}
+
+//go:export TreeChild
+func wasmTreeChild(treePtr uintptr, strPtr uintptr, strLen int) {
+ t := (*tree.Tree)(unsafe.Pointer(treePtr))
+ bytes := unsafe.Slice((*byte)(unsafe.Pointer(strPtr)), strLen)
+ str := string(bytes)
+ t.Child(str)
+}
+
+//go:export TreeChildTree
+func wasmTreeChildTree(treePtr uintptr, childTreePtr uintptr) {
+ t := (*tree.Tree)(unsafe.Pointer(treePtr))
+ childTree := (*tree.Tree)(unsafe.Pointer(childTreePtr))
+ t.Child(childTree)
+}
+
+//go:export TreeHidden
+func wasmTreeHidden(treePtr uintptr) bool {
+ t := (*tree.Tree)(unsafe.Pointer(treePtr))
+ return t.Hidden()
+}
+
+//go:export TreeHide
+func wasmTreeHide(treePtr uintptr, hide bool) {
+ t := (*tree.Tree)(unsafe.Pointer(treePtr))
+ t.Hide(hide)
+}
+
+//go:export TreeOffset
+func wasmTreeOffset(treePtr uintptr, start int, end int) {
+ t := (*tree.Tree)(unsafe.Pointer(treePtr))
+ t.Offset(start, end)
+}
+
+//go:export TreeEnumeratorStyle
+func wasmTreeEnumeratorStyle(treePtr uintptr, stylePtr uintptr) {
+ t := (*tree.Tree)(unsafe.Pointer(treePtr))
+ style := (*lipgloss.Style)(unsafe.Pointer(stylePtr))
+ t.EnumeratorStyle(*style)
+}
+
+//go:export TreeItemStyle
+func wasmTreeItemStyle(treePtr uintptr, stylePtr uintptr) {
+ t := (*tree.Tree)(unsafe.Pointer(treePtr))
+ style := (*lipgloss.Style)(unsafe.Pointer(stylePtr))
+ t.ItemStyle(*style)
+}
+
+//go:export TreeRootStyle
+func wasmTreeRootStyle(treePtr uintptr, stylePtr uintptr) {
+ t := (*tree.Tree)(unsafe.Pointer(treePtr))
+ style := (*lipgloss.Style)(unsafe.Pointer(stylePtr))
+ t.RootStyle(*style)
+}
+
+//go:export TreeEnumerator
+func wasmTreeEnumerator(treePtr uintptr, enumType int32) {
+ t := (*tree.Tree)(unsafe.Pointer(treePtr))
+ switch enumType {
+ case 0:
+ t.Enumerator(tree.DefaultEnumerator)
+ case 1:
+ t.Enumerator(tree.RoundedEnumerator)
+ }
+}
+
+//go:export TreeIndenter
+func wasmTreeIndenter(treePtr uintptr, indenterType int32) {
+ t := (*tree.Tree)(unsafe.Pointer(treePtr))
+ switch indenterType {
+ case 0:
+ t.Indenter(tree.DefaultIndenter)
+ }
+}
+
+//go:export TreeNewLeaf
+func wasmTreeNewLeaf(strPtr uintptr, strLen int, hidden bool) uintptr {
+ bytes := unsafe.Slice((*byte)(unsafe.Pointer(strPtr)), strLen)
+ str := string(bytes)
+ leaf := tree.NewLeaf(str, hidden)
+ return uintptr(unsafe.Pointer(leaf))
+}
+
+//go:export TreeLeafValue
+func wasmTreeLeafValue(leafPtr uintptr) *byte {
+ leaf := (*tree.Leaf)(unsafe.Pointer(leafPtr))
+ value := leaf.Value()
+ bytes := []byte(value)
+ if len(bytes) == 0 {
+ return nil
+ }
+ return &bytes[0]
+}
+
+//go:export TreeLeafValueLength
+func wasmTreeLeafValueLength(leafPtr uintptr) int {
+ leaf := (*tree.Leaf)(unsafe.Pointer(leafPtr))
+ return len(leaf.Value())
+}
+
+//go:export TreeLeafHidden
+func wasmTreeLeafHidden(leafPtr uintptr) bool {
+ leaf := (*tree.Leaf)(unsafe.Pointer(leafPtr))
+ return leaf.Hidden()
+}
+
+//go:export TreeLeafSetHidden
+func wasmTreeLeafSetHidden(leafPtr uintptr, hidden bool) {
+ leaf := (*tree.Leaf)(unsafe.Pointer(leafPtr))
+ leaf.SetHidden(hidden)
+}
+
+//go:export TreeLeafSetValue
+func wasmTreeLeafSetValue(leafPtr uintptr, strPtr uintptr, strLen int) {
+ leaf := (*tree.Leaf)(unsafe.Pointer(leafPtr))
+ bytes := unsafe.Slice((*byte)(unsafe.Pointer(strPtr)), strLen)
+ str := string(bytes)
+ leaf.SetValue(str)
+}
+
+//go:export TreeChildLeaf
+func wasmTreeChildLeaf(treePtr uintptr, leafPtr uintptr) {
+ t := (*tree.Tree)(unsafe.Pointer(treePtr))
+ leaf := (*tree.Leaf)(unsafe.Pointer(leafPtr))
+ t.Child(leaf)
+}
diff --git a/bindings/package-lock.json b/bindings/package-lock.json
new file mode 100644
index 00000000..80b683f9
--- /dev/null
+++ b/bindings/package-lock.json
@@ -0,0 +1,13 @@
+{
+ "name": "@charmland/lipgloss",
+ "version": "2.0.0-beta.3-0e280f3",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "@charmland/lipgloss",
+ "version": "2.0.0-beta.3-0e280f3",
+ "license": "MIT"
+ }
+ }
+}
diff --git a/bindings/package.json b/bindings/package.json
new file mode 100644
index 00000000..86263a97
--- /dev/null
+++ b/bindings/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "@charmland/lipgloss",
+ "version": "2.0.0-beta.3-0e280f3",
+ "private": false,
+ "main": "./src/index.js",
+ "description": "Style definitions for nice terminal layouts 👄",
+ "scripts": {
+ "build": "task build",
+ "dev": "task dev",
+ "test": "cd examples && npm test",
+ "test:simple": "cd examples && npm run test:simple",
+ "test:style": "cd examples && npm run test:style",
+ "test:ascii": "cd examples && npm run test:ascii",
+ "test:comprehensive": "cd examples && npm run test:comprehensive",
+ "test:table-data": "cd examples && npm run test:table-data",
+ "test:all": "cd examples && npm run test:all",
+ "examples": "cd examples && npm run examples"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/charmbracelet/lipgloss.git"
+ },
+ "type": "module",
+ "publishConfig": {
+ "access": "public"
+ },
+ "files": [
+ "./src/index.js",
+ "./src/core.js",
+ "./src/helpers.js",
+ "./src/styleFunc.js",
+ "./src/wasmExec.js",
+ "./src/lipgloss.wasm"
+ ],
+ "keywords": [
+ "cli",
+ "layout",
+ "style",
+ "tui"
+ ],
+ "author": "Charm",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/charmbracelet/lipgloss/issues"
+ },
+ "homepage": "https://github.com/charmbracelet/lipgloss#readme"
+}
diff --git a/bindings/src/core.js b/bindings/src/core.js
new file mode 100644
index 00000000..0df8dddd
--- /dev/null
+++ b/bindings/src/core.js
@@ -0,0 +1,527 @@
+export function insertString(text, module) {
+ // Get the address of the writable memory.
+ let addr = module.exports.getBuffer();
+ let buffer = module.exports.memory.buffer;
+
+ // Properly encode the string as UTF-8
+ const encoder = new TextEncoder();
+ const encodedBytes = encoder.encode(text);
+
+ let mem = new Uint8Array(buffer);
+ let view = mem.subarray(addr, addr + encodedBytes.length);
+
+ // Copy the properly encoded bytes
+ view.set(encodedBytes);
+
+ // Return the address we started at.
+ return addr;
+}
+
+/**
+ * Clears a string from memory by zeroing out the buffer
+ * @param {number} addr - The starting address of the string in memory
+ * @param {number} length - The length of the string to clear
+ * @param {WebAssembly.Module} module - The WebAssembly module with memory
+ * @returns {boolean} - True if successfully cleared, false otherwise
+ */
+export function clearString(addr, length, module) {
+ try {
+ // Get the memory buffer
+ let buffer = module.exports.memory.buffer;
+ let mem = new Int8Array(buffer);
+
+ // Zero out the memory region
+ let view = mem.subarray(addr, addr + length);
+ for (let i = 0; i < length; i++) {
+ view[i] = 0;
+ }
+
+ return true;
+ } catch (error) {
+ console.error("Failed to clear string:", error);
+ return false;
+ }
+}
+
+/**
+ * @param {WebAssembly.Instance} instance - The WebAssembly instance
+ * @param {number} ptr - Pointer to the string in memory
+ * @param {number} len - Length of the string
+ * @returns {string} - The string read from memory
+ */
+export function readGoString(instance, ptr, len) {
+ try {
+ // Get the memory buffer
+ const memory = instance.exports.memory.buffer;
+
+ // Check if the pointer and length are within memory bounds
+ if (ptr + len > memory.byteLength) {
+ console.error("String access would be out of bounds:", {
+ ptr,
+ len,
+ memorySize: memory.byteLength,
+ });
+ return "";
+ }
+
+ // Read the string from memory
+ const bytes = new Uint8Array(memory, ptr, len);
+ return new TextDecoder().decode(bytes);
+ } catch (error) {
+ console.error("Error in readGoString:", error);
+ return "";
+ }
+}
+
+// Memory management for string arrays
+let memoryOffset = 4096; // Start after some safe space
+const MEMORY_CHUNK_SIZE = 8192; // 8KB chunks
+
+/**
+ * Grows WASM memory if needed to accommodate the required size
+ * @param {WebAssembly.Instance} instance - The WASM instance
+ * @param {number} requiredBytes - Number of bytes needed
+ * @returns {boolean} - True if memory is sufficient, false if growth failed
+ */
+export function ensureMemorySize(instance, requiredBytes) {
+ const memory = instance.exports.memory;
+ const currentSize = memory.buffer.byteLength;
+
+ if (currentSize >= requiredBytes) {
+ return true; // Already have enough memory
+ }
+
+ // Calculate how many pages we need to grow
+ const pageSize = 65536; // WASM page size is 64KB
+ const currentPages = currentSize / pageSize;
+ const requiredPages = Math.ceil(requiredBytes / pageSize);
+ const pagesToGrow = requiredPages - currentPages;
+
+ // Check environment variables for debug output
+ const debugMemory = process.env.LIPGLOSS_DEBUG_MEMORY === 'true' ||
+ process.env.LIPGLOSS_DEBUG === 'true' ||
+ process.env.DEBUG === 'lipgloss' ||
+ process.env.DEBUG === '*';
+
+ if (debugMemory) {
+ console.log(`Growing WASM memory: ${currentSize} -> ${requiredPages * pageSize} bytes (${pagesToGrow} pages)`);
+ }
+
+ try {
+ const previousPages = memory.grow(pagesToGrow);
+ if (debugMemory) {
+ console.log(`Memory growth successful: ${previousPages} -> ${previousPages + pagesToGrow} pages`);
+ }
+ return true;
+ } catch (error) {
+ console.error(`Failed to grow WASM memory by ${pagesToGrow} pages:`, error);
+ return false;
+ }
+}
+
+export function passStringArray(instance, strings) {
+ const memory = instance.exports.memory;
+ const wasmMalloc = instance.exports.wasmMalloc;
+ const wasmFree = instance.exports.wasmFree;
+
+ // Always use manual memory management to avoid TinyGo malloc issues
+ return passStringArrayWithBoundsCheck(instance, strings);
+}
+
+function passStringArrayWithTinyGoOptimization(instance, strings, wasmMalloc, wasmFree) {
+ const memory = instance.exports.memory;
+ const wasmGC = instance.exports.wasmGC;
+
+ // Calculate total size needed
+ const encoder = new TextEncoder();
+ let totalStringSize = 0;
+ const encodedStrings = [];
+
+ for (const str of strings) {
+ // Don't truncate strings - let memory grow instead
+ const encoded = encoder.encode(str);
+ encodedStrings.push(encoded);
+ totalStringSize += encoded.length;
+ }
+
+ // Calculate total memory needed (pointer array + string data + safety buffer)
+ const pointerArraySize = strings.length * 8;
+ const totalMemoryNeeded = pointerArraySize + totalStringSize + 4096; // 4KB safety buffer
+
+ // Try to ensure we have enough memory
+ if (!ensureMemorySize(instance, totalMemoryNeeded)) {
+ console.warn(`Failed to grow memory for ${totalMemoryNeeded} bytes, using fallback`);
+ return passStringArrayWithBoundsCheck(instance, strings);
+ }
+
+ // Allocate memory for the pointer/length array (8 bytes per string: 4 for ptr, 4 for length)
+ const pointerArrayOffset = wasmMalloc(pointerArraySize);
+
+ if (!pointerArrayOffset) {
+ console.error("TinyGo malloc failed for pointer array");
+ return passStringArrayWithBoundsCheck(instance, strings);
+ }
+
+ const allocatedPtrs = [];
+
+ try {
+ // Create a view for writing the pointer/length pairs
+ const pointerArray = new Uint32Array(
+ memory.buffer,
+ pointerArrayOffset,
+ strings.length * 2,
+ );
+
+ // Encode and store each string with careful memory management
+ for (let i = 0; i < encodedStrings.length; i++) {
+ const stringBytes = encodedStrings[i];
+
+ // Allocate memory for this string
+ const stringDataOffset = wasmMalloc(stringBytes.length);
+ if (!stringDataOffset) {
+ console.warn(`TinyGo malloc failed for string ${i}, using fallback`);
+ // Clean up and fall back
+ for (const ptr of allocatedPtrs) {
+ if (ptr) wasmFree(ptr);
+ }
+ wasmFree(pointerArrayOffset);
+ return passStringArrayWithBoundsCheck(instance, strings);
+ }
+ allocatedPtrs.push(stringDataOffset);
+
+ // Copy string data to memory with bounds checking
+ try {
+ const memoryView = new Uint8Array(memory.buffer);
+ if (stringDataOffset + stringBytes.length > memory.buffer.byteLength) {
+ throw new Error("Memory bounds exceeded");
+ }
+ memoryView.set(stringBytes, stringDataOffset);
+ } catch (e) {
+ console.warn(`Memory copy failed for string ${i}:`, e.message);
+ // Clean up and fall back
+ for (const ptr of allocatedPtrs) {
+ if (ptr) wasmFree(ptr);
+ }
+ wasmFree(pointerArrayOffset);
+ return passStringArrayWithBoundsCheck(instance, strings);
+ }
+
+ // Store pointer and length in the array
+ pointerArray[i * 2] = stringDataOffset; // String pointer
+ pointerArray[i * 2 + 1] = stringBytes.length; // String length
+ }
+
+ // Trigger periodic cleanup for TinyGo
+ if (wasmGC) {
+ wasmGC();
+ }
+
+ return pointerArrayOffset;
+ } catch (error) {
+ // Clean up on error and fall back
+ for (const ptr of allocatedPtrs) {
+ if (ptr) wasmFree(ptr);
+ }
+ wasmFree(pointerArrayOffset);
+ console.warn("TinyGo memory allocation failed, using fallback:", error.message);
+ return passStringArrayWithBoundsCheck(instance, strings);
+ }
+}
+
+function passStringArrayWithMalloc(instance, strings, malloc, free) {
+ const memory = instance.exports.memory;
+
+ // Allocate memory for the pointer/length array (8 bytes per string: 4 for ptr, 4 for length)
+ const pointerArraySize = strings.length * 8;
+ const pointerArrayOffset = malloc(pointerArraySize);
+
+ if (!pointerArrayOffset) {
+ console.error("Failed to allocate memory for pointer array");
+ return 0;
+ }
+
+ // Create a view for writing the pointer/length pairs
+ const pointerArray = new Uint32Array(
+ memory.buffer,
+ pointerArrayOffset,
+ strings.length * 2,
+ );
+
+ const allocatedPtrs = [];
+
+ try {
+ // Encode and store each string
+ for (let i = 0; i < strings.length; i++) {
+ const encoder = new TextEncoder();
+ const stringBytes = encoder.encode(strings[i]);
+
+ // Allocate memory for this string
+ const stringDataOffset = malloc(stringBytes.length);
+ if (!stringDataOffset) {
+ throw new Error(`Failed to allocate memory for string ${i}`);
+ }
+ allocatedPtrs.push(stringDataOffset);
+
+ // Copy string data to memory
+ const memoryView = new Uint8Array(memory.buffer);
+ memoryView.set(stringBytes, stringDataOffset);
+
+ // Store pointer and length in the array
+ pointerArray[i * 2] = stringDataOffset; // String pointer
+ pointerArray[i * 2 + 1] = stringBytes.length; // String length
+ }
+
+ return pointerArrayOffset;
+ } catch (error) {
+ // Clean up on error
+ for (const ptr of allocatedPtrs) {
+ if (ptr) free(ptr);
+ }
+ free(pointerArrayOffset);
+ console.error("Error in passStringArray:", error);
+ return 0;
+ }
+}
+
+function passStringArrayWithWasmMalloc(instance, strings, wasmMalloc, wasmFree) {
+ const memory = instance.exports.memory;
+ const wasmGC = instance.exports.wasmGC;
+
+ // Calculate total size needed
+ const encoder = new TextEncoder();
+ let totalStringSize = 0;
+ const encodedStrings = [];
+
+ for (const str of strings) {
+ const encoded = encoder.encode(str);
+ encodedStrings.push(encoded);
+ totalStringSize += encoded.length;
+ }
+
+ // For TinyGo, use chunked processing for large strings
+ const maxChunkSize = 64 * 1024; // 64KB chunks
+ if (totalStringSize > maxChunkSize) {
+ return passStringArrayChunked(instance, encodedStrings, wasmMalloc, wasmFree);
+ }
+
+ // Allocate memory for the pointer/length array (8 bytes per string: 4 for ptr, 4 for length)
+ const pointerArraySize = strings.length * 8;
+ const pointerArrayOffset = wasmMalloc(pointerArraySize);
+
+ if (!pointerArrayOffset) {
+ console.error("Failed to allocate memory for pointer array");
+ return 0;
+ }
+
+ const allocatedPtrs = [];
+
+ try {
+ // Create a view for writing the pointer/length pairs
+ const pointerArray = new Uint32Array(
+ memory.buffer,
+ pointerArrayOffset,
+ strings.length * 2,
+ );
+
+ // Encode and store each string
+ for (let i = 0; i < encodedStrings.length; i++) {
+ const stringBytes = encodedStrings[i];
+
+ // Allocate memory for this string
+ const stringDataOffset = wasmMalloc(stringBytes.length);
+ if (!stringDataOffset) {
+ throw new Error(`Failed to allocate memory for string ${i}`);
+ }
+ allocatedPtrs.push(stringDataOffset);
+
+ // Copy string data to memory
+ const memoryView = new Uint8Array(memory.buffer);
+ memoryView.set(stringBytes, stringDataOffset);
+
+ // Store pointer and length in the array
+ pointerArray[i * 2] = stringDataOffset; // String pointer
+ pointerArray[i * 2 + 1] = stringBytes.length; // String length
+ }
+
+ // Trigger periodic cleanup for TinyGo
+ if (wasmGC) {
+ wasmGC();
+ }
+
+ return pointerArrayOffset;
+ } catch (error) {
+ // Clean up on error
+ for (const ptr of allocatedPtrs) {
+ if (ptr) wasmFree(ptr);
+ }
+ wasmFree(pointerArrayOffset);
+ console.error("Error in passStringArrayWithWasmMalloc:", error);
+ return 0;
+ }
+}
+
+// Chunked processing for large string arrays
+function passStringArrayChunked(instance, encodedStrings, wasmMalloc, wasmFree) {
+ const memory = instance.exports.memory;
+
+ // Process strings in smaller chunks to avoid TinyGo string length limits
+ const chunkSize = 8; // Process 8 strings at a time
+ const chunks = [];
+
+ for (let i = 0; i < encodedStrings.length; i += chunkSize) {
+ chunks.push(encodedStrings.slice(i, i + chunkSize));
+ }
+
+ // Allocate memory for the main pointer array
+ const pointerArraySize = encodedStrings.length * 8;
+ const pointerArrayOffset = wasmMalloc(pointerArraySize);
+
+ if (!pointerArrayOffset) {
+ console.error("Failed to allocate memory for chunked pointer array");
+ return 0;
+ }
+
+ try {
+ const pointerArray = new Uint32Array(
+ memory.buffer,
+ pointerArrayOffset,
+ encodedStrings.length * 2,
+ );
+
+ let stringIndex = 0;
+
+ // Process each chunk
+ for (const chunk of chunks) {
+ for (const stringBytes of chunk) {
+ const stringDataOffset = wasmMalloc(stringBytes.length);
+ if (!stringDataOffset) {
+ throw new Error(`Failed to allocate memory for chunked string ${stringIndex}`);
+ }
+
+ // Copy string data to memory
+ const memoryView = new Uint8Array(memory.buffer);
+ memoryView.set(stringBytes, stringDataOffset);
+
+ // Store pointer and length in the array
+ pointerArray[stringIndex * 2] = stringDataOffset;
+ pointerArray[stringIndex * 2 + 1] = stringBytes.length;
+
+ stringIndex++;
+ }
+
+ // Small delay between chunks to let TinyGo process
+ // This is a no-op but helps with memory management
+ if (chunks.length > 1) {
+ const dummy = new Uint8Array(1);
+ }
+ }
+
+ return pointerArrayOffset;
+ } catch (error) {
+ wasmFree(pointerArrayOffset);
+ console.error("Error in passStringArrayChunked:", error);
+ return 0;
+ }
+}
+
+function passStringArrayWithBoundsCheck(instance, strings) {
+ const memory = instance.exports.memory;
+
+ // Calculate total size needed
+ const encoder = new TextEncoder();
+ let totalStringSize = 0;
+ const encodedStrings = [];
+
+ for (const str of strings) {
+ const encoded = encoder.encode(str);
+ encodedStrings.push(encoded);
+ totalStringSize += encoded.length;
+ }
+
+ // Allocate memory for the pointer/length array (8 bytes per string: 4 for ptr, 4 for length)
+ const pointerArraySize = strings.length * 8;
+ const totalSize = pointerArraySize + totalStringSize;
+ const totalMemoryNeeded = totalSize + 4096; // 4KB safety buffer
+
+ // Try to ensure we have enough memory
+ if (!ensureMemorySize(instance, totalMemoryNeeded)) {
+ console.error(`Failed to grow memory for ${totalMemoryNeeded} bytes in fallback`);
+ return 0;
+ }
+
+ const memorySize = memory.buffer.byteLength; // Get updated size after potential growth
+
+ // Use a much larger safe memory area for large ANSI strings
+ const safeMemoryStart = 1024;
+ const safeMemoryEnd = Math.min(memorySize - totalSize - 2048, memorySize * 0.8); // Use up to 80% of memory, leave 2KB buffer
+
+ if (safeMemoryEnd <= safeMemoryStart || totalSize > (memorySize * 0.8)) {
+ console.error(`Not enough memory: need ${totalSize}, available ${safeMemoryEnd - safeMemoryStart}`);
+ // Try to reset memory offset and retry once
+ memoryOffset = safeMemoryStart;
+ if (totalSize > (memorySize * 0.8)) {
+ console.error("String data too large for available memory");
+ return 0;
+ }
+ }
+
+ // Use a rotating buffer within safe bounds, but with larger chunks for ANSI strings
+ const chunkSize = Math.max(totalSize, 16384); // At least 16KB chunks
+ if (memoryOffset < safeMemoryStart || memoryOffset + totalSize > safeMemoryEnd) {
+ memoryOffset = safeMemoryStart;
+ }
+
+ const pointerArrayOffset = memoryOffset;
+
+ // Check bounds before proceeding
+ if (pointerArrayOffset + totalSize > memorySize) {
+ console.error(
+ `Memory bounds check failed: ${pointerArrayOffset + totalSize} > ${memorySize}`,
+ );
+ return 0;
+ }
+
+ try {
+ // Allocate memory for string data (after the pointer array)
+ let stringDataOffset = pointerArrayOffset + pointerArraySize;
+
+ // Create a view for writing the pointer/length pairs
+ const pointerArray = new Uint32Array(
+ memory.buffer,
+ pointerArrayOffset,
+ strings.length * 2,
+ );
+
+ const memoryView = new Uint8Array(memory.buffer);
+
+ // Store each string
+ for (let i = 0; i < encodedStrings.length; i++) {
+ const stringBytes = encodedStrings[i];
+
+ // Double-check bounds for each string
+ if (stringDataOffset + stringBytes.length > memorySize) {
+ throw new Error(`String ${i} would exceed memory bounds`);
+ }
+
+ // Copy string data to memory
+ memoryView.set(stringBytes, stringDataOffset);
+
+ // Store pointer and length in the array
+ pointerArray[i * 2] = stringDataOffset; // String pointer
+ pointerArray[i * 2 + 1] = stringBytes.length; // String length
+
+ // Update offset for next string
+ stringDataOffset += stringBytes.length;
+ }
+
+ // Update global offset for next call with larger increment to avoid conflicts
+ memoryOffset = Math.max(stringDataOffset, memoryOffset + chunkSize);
+
+ return pointerArrayOffset;
+ } catch (error) {
+ console.error("Error in passStringArrayWithBoundsCheck:", error);
+ // Reset memory offset on error
+ memoryOffset = safeMemoryStart;
+ return 0;
+ }
+}
diff --git a/bindings/src/helpers.js b/bindings/src/helpers.js
new file mode 100644
index 00000000..ddcf5ff7
--- /dev/null
+++ b/bindings/src/helpers.js
@@ -0,0 +1,122 @@
+export function whichSidesInt(...i) {
+ let top,
+ right,
+ bottom,
+ left,
+ ok = false;
+
+ switch (i.length) {
+ case 1:
+ top = i[0];
+ bottom = i[0];
+ left = i[0];
+ right = i[0];
+ ok = true;
+ break;
+ case 2:
+ top = i[0];
+ bottom = i[0];
+ left = i[1];
+ right = i[1];
+ ok = true;
+ break;
+ case 3:
+ top = i[0];
+ left = i[1];
+ right = i[1];
+ bottom = i[2];
+ ok = true;
+ break;
+ case 4:
+ top = i[0];
+ right = i[1];
+ bottom = i[2];
+ left = i[3];
+ ok = true;
+ break;
+ }
+
+ return [top, right, bottom, left, ok];
+}
+
+export function whichSidesBool(...i) {
+ let top,
+ right,
+ bottom,
+ left,
+ ok = false;
+
+ switch (i.length) {
+ case 1:
+ top = i[0];
+ bottom = i[0];
+ left = i[0];
+ right = i[0];
+ ok = true;
+ break;
+ case 2:
+ top = i[0];
+ bottom = i[0];
+ left = i[1];
+ right = i[1];
+ ok = true;
+ break;
+ case 3:
+ top = i[0];
+ left = i[1];
+ right = i[1];
+ bottom = i[2];
+ ok = true;
+ break;
+ case 4:
+ top = i[0];
+ right = i[1];
+ bottom = i[2];
+ left = i[3];
+ ok = true;
+ break;
+ }
+
+ return [top, right, bottom, left, ok];
+}
+
+export function whichSidesColor(...colors) {
+ let top,
+ right,
+ bottom,
+ left,
+ ok = false;
+
+ switch (colors.length) {
+ case 1:
+ top = colors[0];
+ bottom = colors[0];
+ left = colors[0];
+ right = colors[0];
+ ok = true;
+ break;
+ case 2:
+ top = colors[0];
+ bottom = colors[0];
+ left = colors[1];
+ right = colors[1];
+ ok = true;
+ break;
+ case 3:
+ top = colors[0];
+ left = colors[1];
+ right = colors[1];
+ bottom = colors[2];
+ ok = true;
+ break;
+ case 4:
+ top = colors[0];
+ right = colors[1];
+ bottom = colors[2];
+ left = colors[3];
+ ok = true;
+ break;
+ }
+
+ return [top, right, bottom, left, ok];
+}
diff --git a/bindings/src/index.js b/bindings/src/index.js
new file mode 100644
index 00000000..146f55e8
--- /dev/null
+++ b/bindings/src/index.js
@@ -0,0 +1,1014 @@
+import * as fs from "fs";
+import path from "node:path";
+import { fileURLToPath } from "url";
+import {
+ passStringArray,
+ readGoString,
+ insertString,
+ clearString,
+ ensureMemorySize,
+} from "./core.js";
+import { whichSidesBool, whichSidesInt, whichSidesColor } from "./helpers.js";
+import { callStyleFunction, registerStyleFunction } from "./styleFunc.js";
+import "./wasmExec.js";
+
+const go = new Go();
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const wasmBuffer = fs.readFileSync(path.join(__dirname, "./lipgloss.wasm"));
+
+// Synchronous initialization and instantiation
+const wasm = new WebAssembly.Module(wasmBuffer);
+
+go.importObject.env["callStyleFunc"] = callStyleFunction;
+
+const instance = new WebAssembly.Instance(wasm, {
+ ...go.importObject,
+});
+
+go.run(instance);
+
+// Required since wasm runtime is not tty
+process.env["TTY_FORCE"] = process.stdout.isTTY;
+let envArr = Object.entries(process.env).map(([key, value]) => {
+ return `${key}=${value}`;
+});
+let envArrPtr = passStringArray(instance, envArr);
+// Configure color profile through wasm environment variables
+instance.exports.DetectFromEnvVars(envArrPtr, envArr.length);
+
+const Top = instance.exports.PositionTop();
+const Bottom = instance.exports.PositionBottom();
+const Right = instance.exports.PositionRight();
+const Left = instance.exports.PositionLeft();
+const Center = instance.exports.PositionCenter();
+
+function Width(str) {
+ let addr = insertString(str, instance);
+ return instance.exports.Width(addr, str.length);
+}
+
+function Height(str) {
+ let addr = insertString(str, instance);
+ return instance.exports.Height(addr, str.length);
+}
+
+function join(isVertical, position, ...strings) {
+ // Early return for empty cases
+ if (strings.length === 0) return "";
+ if (strings.length === 1) return strings[0];
+
+ // Access the WebAssembly memory and necessary exports
+ const memory = instance.exports.memory;
+ const malloc = instance.exports.malloc;
+ const free = instance.exports.free;
+ let wasmJoin = instance.exports.JoinHorizontal;
+ if (isVertical) {
+ wasmJoin = instance.exports.JoinVertical;
+ }
+
+ // Initialize variables outside try block so they're available in finally
+ let ptrArrayPtr = 0;
+ let allocatedPtrs = [];
+
+ try {
+ // Allocate memory for the string pointers and lengths array
+ const stringsCount = strings.length;
+ const pointerArraySize = stringsCount * 2 * 4; // Each string needs 2 uint32 values (ptr, len) * 4 bytes each
+ ptrArrayPtr = malloc(pointerArraySize);
+
+ if (!ptrArrayPtr) {
+ throw new Error("Failed to allocate memory for pointer array");
+ }
+
+ // Create a view of the pointer array in the WASM memory
+ const ptrArray = new Uint32Array(
+ memory.buffer,
+ ptrArrayPtr,
+ stringsCount * 2,
+ );
+
+ // Process each input string
+ for (let i = 0; i < strings.length; i++) {
+ const str = strings[i];
+ if (!str.length) {
+ continue;
+ }
+
+ const strBytes = new TextEncoder().encode(str);
+ const strLen = strBytes.length;
+
+ // Allocate memory for the string data
+ const strPtr = malloc(strLen);
+ if (!strPtr) {
+ throw new Error(`Failed to allocate memory for string ${i}`);
+ }
+ allocatedPtrs.push(strPtr);
+
+ // Copy string data to WASM memory
+ const strBuffer = new Uint8Array(memory.buffer, strPtr, strLen);
+ strBuffer.set(strBytes);
+
+ // Store pointer and length in the pointer array
+ ptrArray[i * 2] = strPtr;
+ ptrArray[i * 2 + 1] = strLen;
+ }
+
+ // Call the WASM function
+ // The result is a Go string, which in WASM is returned as an integer representing
+ // a pointer to a structure containing both the string pointer and length
+ const resultGoStrPtr = wasmJoin(position, ptrArrayPtr, stringsCount);
+
+ // Extract the result string using Go's string representation
+ // We need to read the pointer and length of the string from memory
+ let result = "";
+
+ if (resultGoStrPtr) {
+ // In Go's WebAssembly ABI for strings:
+ // - First 4 bytes at resultGoStrPtr: pointer to string data
+ // - Next 4 bytes: length of string
+ const resultPtrView = new DataView(memory.buffer, resultGoStrPtr, 8);
+ const resultDataPtr = resultPtrView.getUint32(0, true); // true = little endian
+ const resultLength = resultPtrView.getUint32(4, true);
+
+ if (resultLength > 0 && resultDataPtr) {
+ const resultBytes = new Uint8Array(
+ memory.buffer,
+ resultDataPtr,
+ resultLength,
+ );
+ result = new TextDecoder().decode(resultBytes);
+ }
+ }
+
+ return result;
+ } catch (err) {
+ console.error(
+ "Error in join",
+ isVertical ? "Vertical" : "Horizontal",
+ ": ",
+ err,
+ );
+ return "";
+ } finally {
+ // Clean up allocated memory
+ // Note: We don't free the resultGoStrPtr as it's managed by Go's runtime
+ for (const ptr of allocatedPtrs) {
+ if (ptr) free(ptr);
+ }
+ if (ptrArrayPtr) free(ptrArrayPtr);
+ }
+}
+
+function Size(str) {
+ return {
+ height: Height(str),
+ width: Width(str),
+ };
+}
+
+function normalBorder() {
+ return instance.exports.BorderNormalBorder();
+}
+
+function roundedBorder() {
+ return instance.exports.BorderRoundedBorder();
+}
+
+function blockBorder() {
+ return instance.exports.BorderBlockBorder();
+}
+
+function outerHalfBlockBorder() {
+ return instance.exports.BorderOuterHalfBlockBorder();
+}
+
+function innerHalfBlockBorder() {
+ return instance.exports.BorderInnerHalfBlockBorder();
+}
+
+function thickBorder() {
+ return instance.exports.BorderThickBorder();
+}
+
+function doubleBorder() {
+ return instance.exports.BorderDoubleBorder();
+}
+
+function hiddenBorder() {
+ return instance.exports.BorderHiddenBorder();
+}
+
+function markdownBorder() {
+ return instance.exports.BorderMarkdownBorder();
+}
+
+function ASCIIBorder() {
+ return instance.exports.BorderASCIIBorder();
+}
+
+function joinHorizontal(position, ...strings) {
+ return join(false, position, ...strings);
+}
+
+function joinVertical(position, ...strings) {
+ return join(true, position, ...strings);
+}
+
+function lightDark(isDark) {
+ return (light, dark) => {
+ if (isDark) {
+ return dark;
+ }
+ return light;
+ };
+}
+
+// function Color(str) {
+// let addr = insertString(str, instance);
+// let result = instance.exports.Color(addr, str.length);
+// clearString(addr, str.length, instance);
+// return result;
+// }
+
+// wasmPlace(width, height int32, hPos, vPos *Position, str string, opts ...WhitespaceOption) *string {
+function Place(width, height, hPos, vPos, str) {
+ const memory = instance.exports.memory;
+
+ let addr = insertString(str, instance);
+ let resultGoStrPtr = instance.exports.PositionPlace(
+ width,
+ height,
+ hPos,
+ vPos,
+ addr,
+ str.length,
+ );
+
+ let result = "";
+
+ if (resultGoStrPtr) {
+ // In Go's WebAssembly ABI for strings:
+ // - First 4 bytes at resultGoStrPtr: pointer to string data
+ // - Next 4 bytes: length of string
+ const resultPtrView = new DataView(memory.buffer, resultGoStrPtr, 8);
+ const resultDataPtr = resultPtrView.getUint32(0, true); // true = little endian
+ const resultLength = resultPtrView.getUint32(4, true);
+
+ if (resultLength > 0 && resultDataPtr) {
+ const resultBytes = new Uint8Array(
+ memory.buffer,
+ resultDataPtr,
+ resultLength,
+ );
+ result = new TextDecoder().decode(resultBytes);
+ }
+ }
+
+ return result;
+}
+
+function Color(str) {
+ let addr = insertString(str, instance);
+ let result = instance.exports.Color(addr, str.length);
+ clearString(addr, str.length, instance);
+ return result;
+}
+
+const NoColor = instance.exports.NoColor();
+
+// List enumerators
+const Alphabet = instance.exports.ListEnumeratorAlphabet();
+const Arabic = instance.exports.ListEnumeratorArabic();
+const Bullet = instance.exports.ListEnumeratorBullet();
+const Dash = instance.exports.ListEnumeratorDash();
+const Roman = instance.exports.ListEnumeratorRoman();
+const Asterisk = instance.exports.ListEnumeratorAsterisk();
+
+// Tree enumerators
+const DefaultEnumerator = instance.exports.TreeEnumeratorDefault();
+const RoundedEnumerator = instance.exports.TreeEnumeratorRounded();
+
+// Tree indenters
+const DefaultIndenter = instance.exports.TreeIndenterDefault();
+
+class Style {
+ constructor() {
+ this.addr = instance.exports.StyleNewStyle();
+ this.render = this.render.bind(this);
+ }
+ render(...strs) {
+ let addr = this.addr;
+ const memory = instance.exports.memory;
+
+ if (strs.length > 0) {
+ let ptr = passStringArray(instance, strs);
+ instance.exports.StyleJoinString(addr, ptr, strs.length);
+ }
+
+ // Estimate memory needed for rendering and ensure we have enough
+ const totalInputSize = strs.reduce((sum, str) => sum + str.length, 0);
+ const estimatedOutputSize = Math.max(totalInputSize * 2, 32768); // At least 32KB for complex rendering
+ const currentMemory = memory.buffer.byteLength;
+ const memoryNeeded = currentMemory + estimatedOutputSize;
+
+ ensureMemorySize(instance, memoryNeeded);
+
+ let resultGoStrPtr = instance.exports.StyleRender(addr);
+ let result = "";
+
+ if (resultGoStrPtr) {
+ // In Go's WebAssembly ABI for strings:
+ // - First 4 bytes at resultGoStrPtr: pointer to string data
+ // - Next 4 bytes: length of string
+ const resultPtrView = new DataView(memory.buffer, resultGoStrPtr, 8);
+ const resultDataPtr = resultPtrView.getUint32(0, true); // true = little endian
+ const resultLength = resultPtrView.getUint32(4, true);
+
+ if (resultLength > 0 && resultDataPtr) {
+ const resultBytes = new Uint8Array(
+ memory.buffer,
+ resultDataPtr,
+ resultLength,
+ );
+ result = new TextDecoder().decode(resultBytes);
+ }
+ }
+
+ instance.exports.StyleClearValue(addr);
+ return result;
+ }
+ setString(...strs) {
+ let addr = this.addr;
+ let ptr = passStringArray(instance, strs);
+ instance.exports.StyleSetString(addr, ptr, strs.length);
+ return this;
+ }
+ string() {
+ return this.render();
+ }
+ bold(b) {
+ instance.exports.StyleBold(this.addr, b);
+ return this;
+ }
+ italic(b) {
+ instance.exports.StyleItalic(this.addr, b);
+ return this;
+ }
+ inherit(s) {
+ instance.exports.StyleInherit(this.addr, s.addr);
+ return this;
+ }
+ strikethrough(b) {
+ instance.exports.StyleStrikethrough(this.addr, b);
+ return this;
+ }
+ underline(b) {
+ instance.exports.StyleUnderline(this.addr, b);
+ return this;
+ }
+ blink(b) {
+ instance.exports.StyleBlink(this.addr, b);
+ return this;
+ }
+ reverse(b) {
+ instance.exports.StyleReverse(this.addr, b);
+ return this;
+ }
+ faint(b) {
+ instance.exports.StyleFaint(this.addr, b);
+ return this;
+ }
+ foreground(color) {
+ instance.exports.StyleForeground(this.addr, color);
+ return this;
+ }
+ background(color) {
+ instance.exports.StyleBackground(this.addr, color);
+ return this;
+ }
+ width(val) {
+ instance.exports.StyleWidth(this.addr, val);
+ return this;
+ }
+ height(val) {
+ instance.exports.StyleHeight(this.addr, val);
+ return this;
+ }
+ inline(b) {
+ instance.exports.StyleInline(this.addr, b);
+ return this;
+ }
+ underlineSpaces(b) {
+ instance.exports.StyleUnderlineSpaces(this.addr, b);
+ return this;
+ }
+ strikethroughSpaces(b) {
+ instance.exports.StyleStrikethroughSpaces(this.addr, b);
+ return this;
+ }
+ align(...positions) {
+ if (positions.length > 0) {
+ instance.exports.StyleAlignHorizontal(this.addr, positions[0]);
+ }
+ if (positions.length > 1) {
+ instance.exports.StyleAlignVertical(this.addr, positions[1]);
+ }
+ return this;
+ }
+ alignHorizontal(position) {
+ instance.exports.StyleAlignHorizontal(position);
+ return this;
+ }
+ alignVertical(position) {
+ instance.exports.StyleAlignVertical(position);
+ return this;
+ }
+ border(b, ...sides) {
+ instance.exports.StyleBorder(this.addr, b);
+
+ let [top, right, bottom, left, ok] = whichSidesBool(...sides);
+ if (!ok) {
+ top = true;
+ right = true;
+ bottom = true;
+ left = true;
+ }
+
+ instance.exports.SetBorderTop(this.addr, top);
+ instance.exports.SetBorderRight(this.addr, right);
+ instance.exports.SetBorderBottom(this.addr, bottom);
+ instance.exports.SetBorderLeft(this.addr, left);
+
+ return this;
+ }
+ borderStyle(b) {
+ instance.exports.StyleBorder(this.addr, b);
+ return this;
+ }
+ padding(...paddings) {
+ if (paddings.length > 0 && paddings.length <= 4) {
+ const [top, right, bottom, left, ok] = whichSidesInt(...paddings);
+ if (ok) {
+ instance.exports.StylePaddingTop(this.addr, top);
+ instance.exports.StylePaddingRight(this.addr, right);
+ instance.exports.StylePaddingBottom(this.addr, bottom);
+ instance.exports.StylePaddingLeft(this.addr, left);
+ }
+ }
+
+ return this;
+ }
+ paddingLeft(val) {
+ instance.exports.StylePaddingLeft(this.addr, val);
+ return this;
+ }
+ paddingRight(val) {
+ instance.exports.StylePaddingRight(this.addr, val);
+ return this;
+ }
+ paddingTop(val) {
+ instance.exports.StylePaddingTop(this.addr, val);
+ return this;
+ }
+ paddingBottom(val) {
+ instance.exports.StylePaddingBottom(this.addr, val);
+ return this;
+ }
+ colorWhitespace(val) {
+ instance.exports.StyleColorWhitespace(this.addr, val);
+ return this;
+ }
+ margin(...margins) {
+ if (margins.length > 0) {
+ let [top, right, bottom, left, ok] = whichSidesInt(...margins);
+ if (ok) {
+ instance.exports.StyleMarginTop(this.addr, top);
+ instance.exports.StyleMarginRight(this.addr, right);
+ instance.exports.StyleMarginBottom(this.addr, bottom);
+ instance.exports.StyleMarginLeft(this.addr, left);
+ }
+ }
+ return this;
+ }
+ marginTop(val) {
+ instance.exports.StyleMarginTop(this.addr, val);
+ return this;
+ }
+ marginRight(val) {
+ instance.exports.StyleMarginRight(this.addr, val);
+ return this;
+ }
+ marginLeft(val) {
+ instance.exports.StyleMarginLeft(this.addr, val);
+ return this;
+ }
+ marginBottom(val) {
+ instance.exports.StyleMarginBottom(this.addr, val);
+ return this;
+ }
+ marginBackground(val) {
+ instance.exports.StyleMarginBackground(this.addr, val);
+ return this;
+ }
+ borderForeground(...colors) {
+ if (colors.length > 0) {
+ let [top, right, bottom, left, ok] = whichSidesColor(...colors);
+ if (ok) {
+ instance.exports.StyleBorderTopForeground(this.addr, top);
+ instance.exports.StyleBorderRightForeground(this.addr, right);
+ instance.exports.StyleBorderBottomForeground(this.addr, bottom);
+ instance.exports.StyleBorderLeftForeground(this.addr, left);
+ }
+ }
+
+ return this;
+ }
+ borderTopForeground(color) {
+ instance.exports.StyleBorderTopForeground(this.addr, color);
+ return this;
+ }
+ borderRightForeground(color) {
+ instance.exports.StyleBorderRightForeground(this.addr, color);
+ return this;
+ }
+ borderLeftForeground(color) {
+ instance.exports.StyleBorderLeftForeground(this.addr, color);
+ return this;
+ }
+ borderBottomForeground(color) {
+ instance.exports.StyleBorderBottomForeground(this.addr, color);
+ return this;
+ }
+ borderBackground(...colors) {
+ if (colors.length > 0) {
+ let [top, right, bottom, left, ok] = whichSidesColor(...colors);
+ if (ok) {
+ instance.exports.StyleBorderTopBackground(this.addr, top);
+ instance.exports.StyleBorderRightBackground(this.addr, right);
+ instance.exports.StyleBorderBottomBackground(this.addr, bottom);
+ instance.exports.StyleBorderLeftBackground(this.addr, left);
+ }
+ }
+
+ return this;
+ }
+ borderTopBackground(color) {
+ instance.exports.StyleBorderTopBackground(this.addr, color);
+ return this;
+ }
+ borderRightBackground(color) {
+ instance.exports.StyleBorderRightBackground(this.addr, color);
+ return this;
+ }
+ borderLeftBackground(color) {
+ instance.exports.StyleBorderLeftBackground(this.addr, color);
+ return this;
+ }
+ borderBottomBackground(color) {
+ instance.exports.StyleBorderBottomBackground(this.addr, color);
+ return this;
+ }
+ borderRight(val) {
+ instance.exports.SetBorderRight(this.addr, val);
+ return this;
+ }
+ borderLeft(val) {
+ instance.exports.SetBorderLeft(this.addr, val);
+ return this;
+ }
+ borderTop(val) {
+ instance.exports.SetBorderTop(this.addr, val);
+ return this;
+ }
+ borderBottom(val) {
+ instance.exports.SetBorderBottom(this.addr, val);
+ return this;
+ }
+ maxWidth(val) {
+ instance.exports.StyleMaxWidth(this.addr, val);
+ return this;
+ }
+ maxHeight(val) {
+ instance.exports.StyleMaxHeight(this.addr, val);
+ return this;
+ }
+ tabWidth(val) {
+ instance.exports.StyleTabWidth(this.addr, val);
+ return this;
+ }
+}
+
+class TableData {
+ constructor(...rows) {
+ this.addr = instance.exports.TableDataNew();
+ if (rows.length > 0) {
+ this.rows(...rows);
+ }
+ }
+
+ append(row) {
+ if (Array.isArray(row)) {
+ let ptr = passStringArray(instance, row);
+ instance.exports.TableDataAppend(this.addr, ptr, row.length);
+ }
+ return this;
+ }
+
+ rows(...rows) {
+ for (const row of rows) {
+ this.append(row);
+ }
+ return this;
+ }
+
+ at(row, col) {
+ let ptr = instance.exports.TableDataAtPtr(this.addr, row, col);
+ let ptrLen = instance.exports.TableDataAtLength(this.addr, row, col);
+ return readGoString(instance, ptr, ptrLen);
+ }
+
+ rowCount() {
+ return instance.exports.TableDataRows(this.addr);
+ }
+
+ columnCount() {
+ return instance.exports.TableDataColumns(this.addr);
+ }
+}
+
+class Table {
+ constructor() {
+ this.addr = instance.exports.TableNew();
+ }
+ row(...rows) {
+ let ptr = passStringArray(instance, rows);
+ instance.exports.TableRow(this.addr, ptr, rows.length);
+ return this;
+ }
+ rows(rows) {
+ for (const rowIndex in rows) {
+ const row = rows[rowIndex];
+ let ptr = passStringArray(instance, row);
+ instance.exports.TableRow(this.addr, ptr, row.length);
+ }
+ return this;
+ }
+ clearRows() {
+ instance.exports.TableClearRows(this.addr);
+ return this;
+ }
+ headers(...headers) {
+ let ptr = passStringArray(instance, headers);
+ instance.exports.TableHeaders(this.addr, ptr, headers.length);
+ return this;
+ }
+ data(data) {
+ if (data instanceof TableData) {
+ instance.exports.TableSetData(this.addr, data.addr);
+ }
+ return this;
+ }
+ border(border) {
+ instance.exports.TableBorder(this.addr, border);
+ return this;
+ }
+ borderStyle(style) {
+ if (style.addr) {
+ instance.exports.TableBorderStyle(this.addr, style.addr);
+ }
+ return this;
+ }
+ styleFunc(fn) {
+ let id = registerStyleFunction(fn);
+ instance.exports.TableStyleFunc(this.addr, id);
+ return this;
+ }
+ // TODO: These JS functions can be made by a compositor function
+ wrap(b) {
+ instance.exports.TableWrap(this.addr, b);
+ return this;
+ }
+ borderLeft(b) {
+ instance.exports.TableBorderLeft(this.addr, b);
+ return this;
+ }
+ borderRight(b) {
+ instance.exports.TableBorderRight(this.addr, b);
+ return this;
+ }
+ borderHeader(b) {
+ instance.exports.TableBorderHeader(this.addr, b);
+ return this;
+ }
+ borderColumn(b) {
+ instance.exports.TableBorderColumn(this.addr, b);
+ return this;
+ }
+ borderRow(b) {
+ instance.exports.TableBorderRow(this.addr, b);
+ return this;
+ }
+ borderBottom(b) {
+ instance.exports.TableBorderBottom(this.addr, b);
+ return this;
+ }
+ borderLeft(b) {
+ instance.exports.TableBorderLeft(this.addr, b);
+ return this;
+ }
+ string() {
+ return this.render();
+ }
+ render() {
+ let ptr = instance.exports.TableRenderPtr(this.addr);
+ let ptrLen = instance.exports.TableRenderLength(this.addr);
+ return readGoString(instance, ptr, ptrLen);
+ }
+}
+
+class List {
+ constructor(...items) {
+ this.addr = instance.exports.ListNew();
+ if (items.length > 0) {
+ this.items(...items);
+ }
+ }
+ item(item) {
+ if (typeof item === 'string') {
+ let addr = insertString(item, instance);
+ const encoder = new TextEncoder();
+ const byteLength = encoder.encode(item).length;
+ instance.exports.ListItem(this.addr, addr, byteLength);
+ clearString(addr, byteLength, instance);
+ } else if (item instanceof List) {
+ instance.exports.ListItemList(this.addr, item.addr);
+ }
+ return this;
+ }
+ items(...items) {
+ for (const item of items) {
+ this.item(item);
+ }
+ return this;
+ }
+ hidden() {
+ return instance.exports.ListHidden(this.addr);
+ }
+ hide(hide) {
+ instance.exports.ListHide(this.addr, hide);
+ return this;
+ }
+ offset(start, end) {
+ instance.exports.ListOffset(this.addr, start, end);
+ return this;
+ }
+ enumeratorStyle(style) {
+ if (style.addr) {
+ instance.exports.ListEnumeratorStyle(this.addr, style.addr);
+ }
+ return this;
+ }
+ enumeratorStyleFunc(fn) {
+ let id = registerStyleFunction(fn);
+ instance.exports.ListEnumeratorStyleFunc(this.addr, id);
+ return this;
+ }
+ itemStyle(style) {
+ if (style.addr) {
+ instance.exports.ListItemStyle(this.addr, style.addr);
+ }
+ return this;
+ }
+ itemStyleFunc(fn) {
+ let id = registerStyleFunction(fn);
+ instance.exports.ListStyleFunc(this.addr, id);
+ return this;
+ }
+ enumerator(type) {
+ instance.exports.ListEnumerator(this.addr, type);
+ return this;
+ }
+ string() {
+ return this.render();
+ }
+ render() {
+ let ptr = instance.exports.ListRenderPtr(this.addr);
+ let ptrLen = instance.exports.ListRenderLength(this.addr);
+ return readGoString(instance, ptr, ptrLen);
+ }
+}
+
+class Tree {
+ constructor() {
+ this.addr = instance.exports.TreeNew();
+ }
+ root(value) {
+ if (typeof value === 'string') {
+ let addr = insertString(value, instance);
+ const encoder = new TextEncoder();
+ const byteLength = encoder.encode(value).length;
+ instance.exports.TreeRoot(this.addr, addr, byteLength);
+ clearString(addr, byteLength, instance);
+ }
+ return this;
+ }
+ child(...children) {
+ for (const child of children) {
+ if (typeof child === 'string') {
+ let addr = insertString(child, instance);
+ const encoder = new TextEncoder();
+ const byteLength = encoder.encode(child).length;
+ instance.exports.TreeChild(this.addr, addr, byteLength);
+ clearString(addr, byteLength, instance);
+ } else if (child instanceof Tree) {
+ instance.exports.TreeChildTree(this.addr, child.addr);
+ } else if (child instanceof Leaf) {
+ instance.exports.TreeChildLeaf(this.addr, child.addr);
+ }
+ }
+ return this;
+ }
+ hidden() {
+ return instance.exports.TreeHidden(this.addr);
+ }
+ hide(hide) {
+ instance.exports.TreeHide(this.addr, hide);
+ return this;
+ }
+ offset(start, end) {
+ instance.exports.TreeOffset(this.addr, start, end);
+ return this;
+ }
+ enumeratorStyle(style) {
+ if (style.addr) {
+ instance.exports.TreeEnumeratorStyle(this.addr, style.addr);
+ }
+ return this;
+ }
+ enumeratorStyleFunc(fn) {
+ let id = registerStyleFunction(fn);
+ instance.exports.TreeEnumeratorStyleFunc(this.addr, id);
+ return this;
+ }
+ itemStyle(style) {
+ if (style.addr) {
+ instance.exports.TreeItemStyle(this.addr, style.addr);
+ }
+ return this;
+ }
+ itemStyleFunc(fn) {
+ let id = registerStyleFunction(fn);
+ instance.exports.TreeStyleFunc(this.addr, id);
+ return this;
+ }
+ rootStyle(style) {
+ if (style.addr) {
+ instance.exports.TreeRootStyle(this.addr, style.addr);
+ }
+ return this;
+ }
+ enumerator(type) {
+ instance.exports.TreeEnumerator(this.addr, type);
+ return this;
+ }
+ indenter(type) {
+ instance.exports.TreeIndenter(this.addr, type);
+ return this;
+ }
+ string() {
+ return this.render();
+ }
+ render() {
+ let ptr = instance.exports.TreeRenderPtr(this.addr);
+ let ptrLen = instance.exports.TreeRenderLength(this.addr);
+ return readGoString(instance, ptr, ptrLen);
+ }
+}
+
+class Leaf {
+ constructor(value, hidden = false) {
+ if (typeof value === 'string') {
+ let addr = insertString(value, instance);
+ const encoder = new TextEncoder();
+ const byteLength = encoder.encode(value).length;
+ this.addr = instance.exports.TreeNewLeaf(addr, byteLength, hidden);
+ clearString(addr, byteLength, instance);
+ } else {
+ // Convert non-string values to string
+ const strValue = String(value);
+ let addr = insertString(strValue, instance);
+ const encoder = new TextEncoder();
+ const byteLength = encoder.encode(strValue).length;
+ this.addr = instance.exports.TreeNewLeaf(addr, byteLength, hidden);
+ clearString(addr, byteLength, instance);
+ }
+ }
+ value() {
+ let ptr = instance.exports.TreeLeafValue(this.addr);
+ let ptrLen = instance.exports.TreeLeafValueLength(this.addr);
+ return readGoString(instance, ptr, ptrLen);
+ }
+ hidden() {
+ return instance.exports.TreeLeafHidden(this.addr);
+ }
+ setHidden(hidden) {
+ instance.exports.TreeLeafSetHidden(this.addr, hidden);
+ return this;
+ }
+ setValue(value) {
+ const strValue = String(value);
+ let addr = insertString(strValue, instance);
+ const encoder = new TextEncoder();
+ const byteLength = encoder.encode(strValue).length;
+ instance.exports.TreeLeafSetValue(this.addr, addr, byteLength);
+ clearString(addr, byteLength, instance);
+ return this;
+ }
+ string() {
+ return this.value();
+ }
+}
+
+function joinStyled(strings, bgColor, fgColor) {
+ const memory = instance.exports.memory;
+ let ptr = passStringArray(instance, strings);
+
+ let resultGoStrPtr = instance.exports.StyleJoinStyled(ptr, strings.length, bgColor || 0, fgColor || 0);
+ let result = "";
+
+ if (resultGoStrPtr) {
+ // In Go's WebAssembly ABI for strings:
+ // - First 4 bytes at resultGoStrPtr: pointer to string data
+ // - Next 4 bytes: length of string
+ const resultPtrView = new DataView(memory.buffer, resultGoStrPtr, 8);
+ const resultDataPtr = resultPtrView.getUint32(0, true); // true = little endian
+ const resultLength = resultPtrView.getUint32(4, true);
+
+ if (resultLength > 0 && resultDataPtr) {
+ const resultBytes = new Uint8Array(
+ memory.buffer,
+ resultDataPtr,
+ resultLength,
+ );
+ result = new TextDecoder().decode(resultBytes);
+ }
+ }
+
+ return result;
+}
+
+function triggerGC() {
+ try {
+ if (instance && instance.exports.wasmGC) {
+ instance.exports.wasmGC();
+ }
+ } catch (e) {
+ // Ignore GC errors
+ }
+}
+
+export {
+ Place,
+ Table,
+ TableData,
+ List,
+ Tree,
+ Leaf,
+ Style,
+ Color,
+ NoColor,
+ Size,
+ Width,
+ Height,
+ Left,
+ Right,
+ Center,
+ Top,
+ Bottom,
+ joinVertical,
+ joinHorizontal,
+ normalBorder,
+ roundedBorder,
+ blockBorder,
+ outerHalfBlockBorder,
+ innerHalfBlockBorder,
+ thickBorder,
+ doubleBorder,
+ hiddenBorder,
+ markdownBorder,
+ ASCIIBorder,
+ lightDark,
+ joinStyled,
+ triggerGC,
+ Alphabet,
+ Arabic,
+ Bullet,
+ Dash,
+ Roman,
+ Asterisk,
+ DefaultEnumerator,
+ RoundedEnumerator,
+ DefaultIndenter,
+};
diff --git a/bindings/src/lipgloss.wasm b/bindings/src/lipgloss.wasm
new file mode 100644
index 00000000..187ef145
Binary files /dev/null and b/bindings/src/lipgloss.wasm differ
diff --git a/bindings/src/styleFunc.js b/bindings/src/styleFunc.js
new file mode 100644
index 00000000..d910df54
--- /dev/null
+++ b/bindings/src/styleFunc.js
@@ -0,0 +1,30 @@
+// Callback registry
+const styleFunction = {};
+let nextFunctionId = 1;
+
+// Register a style callback
+export function registerStyleFunction(callback) {
+ const id = nextFunctionId++;
+ styleFunction[id] = callback;
+ return id;
+}
+
+// Call style function from javascript
+export function callStyleFunction(functionId, row, col) {
+ if (!styleFunction[functionId]) {
+ console.error(`Style function with ID ${functionId} not found!`);
+ return 0; // Return null pointer
+ }
+
+ try {
+ const style = styleFunction[functionId](row, col);
+ if (!style || !style.addr) {
+ console.error(`Style function returned invalid style:`, style);
+ return 0;
+ }
+ return style.addr;
+ } catch (error) {
+ console.error(`Error in style function:`, error);
+ return 0;
+ }
+}
diff --git a/bindings/src/wasmExec.js b/bindings/src/wasmExec.js
new file mode 100644
index 00000000..53ea75fd
--- /dev/null
+++ b/bindings/src/wasmExec.js
@@ -0,0 +1,553 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+//
+// This file has been modified for use by the TinyGo compiler.
+
+(() => {
+ // Map multiple JavaScript environments to a single common API,
+ // preferring web standards over Node.js API.
+ //
+ // Environments considered:
+ // - Browsers
+ // - Node.js
+ // - Electron
+ // - Parcel
+
+ if (typeof global !== "undefined") {
+ // global already exists
+ } else if (typeof window !== "undefined") {
+ window.global = window;
+ } else if (typeof self !== "undefined") {
+ self.global = self;
+ } else {
+ throw new Error("cannot export Go (neither global, window nor self is defined)");
+ }
+
+ if (!global.require && typeof require !== "undefined") {
+ global.require = require;
+ }
+
+ if (!global.fs && global.require) {
+ global.fs = require("node:fs");
+ }
+
+ const enosys = () => {
+ const err = new Error("not implemented");
+ err.code = "ENOSYS";
+ return err;
+ };
+
+ if (!global.fs) {
+ let outputBuf = "";
+ global.fs = {
+ constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
+ writeSync(fd, buf) {
+ outputBuf += decoder.decode(buf);
+ const nl = outputBuf.lastIndexOf("\n");
+ if (nl != -1) {
+ console.log(outputBuf.substr(0, nl));
+ outputBuf = outputBuf.substr(nl + 1);
+ }
+ return buf.length;
+ },
+ write(fd, buf, offset, length, position, callback) {
+ if (offset !== 0 || length !== buf.length || position !== null) {
+ callback(enosys());
+ return;
+ }
+ const n = this.writeSync(fd, buf);
+ callback(null, n);
+ },
+ chmod(path, mode, callback) { callback(enosys()); },
+ chown(path, uid, gid, callback) { callback(enosys()); },
+ close(fd, callback) { callback(enosys()); },
+ fchmod(fd, mode, callback) { callback(enosys()); },
+ fchown(fd, uid, gid, callback) { callback(enosys()); },
+ fstat(fd, callback) { callback(enosys()); },
+ fsync(fd, callback) { callback(null); },
+ ftruncate(fd, length, callback) { callback(enosys()); },
+ lchown(path, uid, gid, callback) { callback(enosys()); },
+ link(path, link, callback) { callback(enosys()); },
+ lstat(path, callback) { callback(enosys()); },
+ mkdir(path, perm, callback) { callback(enosys()); },
+ open(path, flags, mode, callback) { callback(enosys()); },
+ read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
+ readdir(path, callback) { callback(enosys()); },
+ readlink(path, callback) { callback(enosys()); },
+ rename(from, to, callback) { callback(enosys()); },
+ rmdir(path, callback) { callback(enosys()); },
+ stat(path, callback) { callback(enosys()); },
+ symlink(path, link, callback) { callback(enosys()); },
+ truncate(path, length, callback) { callback(enosys()); },
+ unlink(path, callback) { callback(enosys()); },
+ utimes(path, atime, mtime, callback) { callback(enosys()); },
+ };
+ }
+
+ if (!global.process) {
+ global.process = {
+ getuid() { return -1; },
+ getgid() { return -1; },
+ geteuid() { return -1; },
+ getegid() { return -1; },
+ getgroups() { throw enosys(); },
+ pid: -1,
+ ppid: -1,
+ umask() { throw enosys(); },
+ cwd() { throw enosys(); },
+ chdir() { throw enosys(); },
+ }
+ }
+
+ if (!global.crypto) {
+ const nodeCrypto = require("node:crypto");
+ global.crypto = {
+ getRandomValues(b) {
+ nodeCrypto.randomFillSync(b);
+ },
+ };
+ }
+
+ if (!global.performance) {
+ global.performance = {
+ now() {
+ const [sec, nsec] = process.hrtime();
+ return sec * 1000 + nsec / 1000000;
+ },
+ };
+ }
+
+ if (!global.TextEncoder) {
+ global.TextEncoder = require("node:util").TextEncoder;
+ }
+
+ if (!global.TextDecoder) {
+ global.TextDecoder = require("node:util").TextDecoder;
+ }
+
+ // End of polyfills for common API.
+
+ const encoder = new TextEncoder("utf-8");
+ const decoder = new TextDecoder("utf-8");
+ let reinterpretBuf = new DataView(new ArrayBuffer(8));
+ var logLine = [];
+ const wasmExit = {}; // thrown to exit via proc_exit (not an error)
+
+ global.Go = class {
+ constructor() {
+ this._callbackTimeouts = new Map();
+ this._nextCallbackTimeoutID = 1;
+
+ const mem = () => {
+ // The buffer may change when requesting more memory.
+ return new DataView(this._inst.exports.memory.buffer);
+ }
+
+ const unboxValue = (v_ref) => {
+ reinterpretBuf.setBigInt64(0, v_ref, true);
+ const f = reinterpretBuf.getFloat64(0, true);
+ if (f === 0) {
+ return undefined;
+ }
+ if (!isNaN(f)) {
+ return f;
+ }
+
+ const id = v_ref & 0xffffffffn;
+ return this._values[id];
+ }
+
+
+ const loadValue = (addr) => {
+ let v_ref = mem().getBigUint64(addr, true);
+ return unboxValue(v_ref);
+ }
+
+ const boxValue = (v) => {
+ const nanHead = 0x7FF80000n;
+
+ if (typeof v === "number") {
+ if (isNaN(v)) {
+ return nanHead << 32n;
+ }
+ if (v === 0) {
+ return (nanHead << 32n) | 1n;
+ }
+ reinterpretBuf.setFloat64(0, v, true);
+ return reinterpretBuf.getBigInt64(0, true);
+ }
+
+ switch (v) {
+ case undefined:
+ return 0n;
+ case null:
+ return (nanHead << 32n) | 2n;
+ case true:
+ return (nanHead << 32n) | 3n;
+ case false:
+ return (nanHead << 32n) | 4n;
+ }
+
+ let id = this._ids.get(v);
+ if (id === undefined) {
+ id = this._idPool.pop();
+ if (id === undefined) {
+ id = BigInt(this._values.length);
+ }
+ this._values[id] = v;
+ this._goRefCounts[id] = 0;
+ this._ids.set(v, id);
+ }
+ this._goRefCounts[id]++;
+ let typeFlag = 1n;
+ switch (typeof v) {
+ case "string":
+ typeFlag = 2n;
+ break;
+ case "symbol":
+ typeFlag = 3n;
+ break;
+ case "function":
+ typeFlag = 4n;
+ break;
+ }
+ return id | ((nanHead | typeFlag) << 32n);
+ }
+
+ const storeValue = (addr, v) => {
+ let v_ref = boxValue(v);
+ mem().setBigUint64(addr, v_ref, true);
+ }
+
+ const loadSlice = (array, len, cap) => {
+ return new Uint8Array(this._inst.exports.memory.buffer, array, len);
+ }
+
+ const loadSliceOfValues = (array, len, cap) => {
+ const a = new Array(len);
+ for (let i = 0; i < len; i++) {
+ a[i] = loadValue(array + i * 8);
+ }
+ return a;
+ }
+
+ const loadString = (ptr, len) => {
+ return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len));
+ }
+
+ const timeOrigin = Date.now() - performance.now();
+ this.importObject = {
+ wasi_snapshot_preview1: {
+ // https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write
+ fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) {
+ let nwritten = 0;
+ if (fd == 1) {
+ for (let iovs_i=0; iovs_i 0, // dummy
+ fd_fdstat_get: () => 0, // dummy
+ fd_seek: () => 0, // dummy
+ proc_exit: (code) => {
+ this.exited = true;
+ this.exitCode = code;
+ this._resolveExitPromise();
+ throw wasmExit;
+ },
+ random_get: (bufPtr, bufLen) => {
+ crypto.getRandomValues(loadSlice(bufPtr, bufLen));
+ return 0;
+ },
+ },
+ gojs: {
+ // func ticks() float64
+ "runtime.ticks": () => {
+ return timeOrigin + performance.now();
+ },
+
+ // func sleepTicks(timeout float64)
+ "runtime.sleepTicks": (timeout) => {
+ // Do not sleep, only reactivate scheduler after the given timeout.
+ setTimeout(() => {
+ if (this.exited) return;
+ try {
+ this._inst.exports.go_scheduler();
+ } catch (e) {
+ if (e !== wasmExit) throw e;
+ }
+ }, timeout);
+ },
+
+ // func finalizeRef(v ref)
+ "syscall/js.finalizeRef": (v_ref) => {
+ // Note: TinyGo does not support finalizers so this is only called
+ // for one specific case, by js.go:jsString. and can/might leak memory.
+ const id = v_ref & 0xffffffffn;
+ if (this._goRefCounts?.[id] !== undefined) {
+ this._goRefCounts[id]--;
+ if (this._goRefCounts[id] === 0) {
+ const v = this._values[id];
+ this._values[id] = null;
+ this._ids.delete(v);
+ this._idPool.push(id);
+ }
+ } else {
+ console.error("syscall/js.finalizeRef: unknown id", id);
+ }
+ },
+
+ // func stringVal(value string) ref
+ "syscall/js.stringVal": (value_ptr, value_len) => {
+ value_ptr >>>= 0;
+ const s = loadString(value_ptr, value_len);
+ return boxValue(s);
+ },
+
+ // func valueGet(v ref, p string) ref
+ "syscall/js.valueGet": (v_ref, p_ptr, p_len) => {
+ let prop = loadString(p_ptr, p_len);
+ let v = unboxValue(v_ref);
+ let result = Reflect.get(v, prop);
+ return boxValue(result);
+ },
+
+ // func valueSet(v ref, p string, x ref)
+ "syscall/js.valueSet": (v_ref, p_ptr, p_len, x_ref) => {
+ const v = unboxValue(v_ref);
+ const p = loadString(p_ptr, p_len);
+ const x = unboxValue(x_ref);
+ Reflect.set(v, p, x);
+ },
+
+ // func valueDelete(v ref, p string)
+ "syscall/js.valueDelete": (v_ref, p_ptr, p_len) => {
+ const v = unboxValue(v_ref);
+ const p = loadString(p_ptr, p_len);
+ Reflect.deleteProperty(v, p);
+ },
+
+ // func valueIndex(v ref, i int) ref
+ "syscall/js.valueIndex": (v_ref, i) => {
+ return boxValue(Reflect.get(unboxValue(v_ref), i));
+ },
+
+ // valueSetIndex(v ref, i int, x ref)
+ "syscall/js.valueSetIndex": (v_ref, i, x_ref) => {
+ Reflect.set(unboxValue(v_ref), i, unboxValue(x_ref));
+ },
+
+ // func valueCall(v ref, m string, args []ref) (ref, bool)
+ "syscall/js.valueCall": (ret_addr, v_ref, m_ptr, m_len, args_ptr, args_len, args_cap) => {
+ const v = unboxValue(v_ref);
+ const name = loadString(m_ptr, m_len);
+ const args = loadSliceOfValues(args_ptr, args_len, args_cap);
+ try {
+ const m = Reflect.get(v, name);
+ storeValue(ret_addr, Reflect.apply(m, v, args));
+ mem().setUint8(ret_addr + 8, 1);
+ } catch (err) {
+ storeValue(ret_addr, err);
+ mem().setUint8(ret_addr + 8, 0);
+ }
+ },
+
+ // func valueInvoke(v ref, args []ref) (ref, bool)
+ "syscall/js.valueInvoke": (ret_addr, v_ref, args_ptr, args_len, args_cap) => {
+ try {
+ const v = unboxValue(v_ref);
+ const args = loadSliceOfValues(args_ptr, args_len, args_cap);
+ storeValue(ret_addr, Reflect.apply(v, undefined, args));
+ mem().setUint8(ret_addr + 8, 1);
+ } catch (err) {
+ storeValue(ret_addr, err);
+ mem().setUint8(ret_addr + 8, 0);
+ }
+ },
+
+ // func valueNew(v ref, args []ref) (ref, bool)
+ "syscall/js.valueNew": (ret_addr, v_ref, args_ptr, args_len, args_cap) => {
+ const v = unboxValue(v_ref);
+ const args = loadSliceOfValues(args_ptr, args_len, args_cap);
+ try {
+ storeValue(ret_addr, Reflect.construct(v, args));
+ mem().setUint8(ret_addr + 8, 1);
+ } catch (err) {
+ storeValue(ret_addr, err);
+ mem().setUint8(ret_addr+ 8, 0);
+ }
+ },
+
+ // func valueLength(v ref) int
+ "syscall/js.valueLength": (v_ref) => {
+ return unboxValue(v_ref).length;
+ },
+
+ // valuePrepareString(v ref) (ref, int)
+ "syscall/js.valuePrepareString": (ret_addr, v_ref) => {
+ const s = String(unboxValue(v_ref));
+ const str = encoder.encode(s);
+ storeValue(ret_addr, str);
+ mem().setInt32(ret_addr + 8, str.length, true);
+ },
+
+ // valueLoadString(v ref, b []byte)
+ "syscall/js.valueLoadString": (v_ref, slice_ptr, slice_len, slice_cap) => {
+ const str = unboxValue(v_ref);
+ loadSlice(slice_ptr, slice_len, slice_cap).set(str);
+ },
+
+ // func valueInstanceOf(v ref, t ref) bool
+ "syscall/js.valueInstanceOf": (v_ref, t_ref) => {
+ return unboxValue(v_ref) instanceof unboxValue(t_ref);
+ },
+
+ // func copyBytesToGo(dst []byte, src ref) (int, bool)
+ "syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, src_ref) => {
+ let num_bytes_copied_addr = ret_addr;
+ let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable
+
+ const dst = loadSlice(dest_addr, dest_len);
+ const src = unboxValue(src_ref);
+ if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
+ mem().setUint8(returned_status_addr, 0); // Return "not ok" status
+ return;
+ }
+ const toCopy = src.subarray(0, dst.length);
+ dst.set(toCopy);
+ mem().setUint32(num_bytes_copied_addr, toCopy.length, true);
+ mem().setUint8(returned_status_addr, 1); // Return "ok" status
+ },
+
+ // copyBytesToJS(dst ref, src []byte) (int, bool)
+ // Originally copied from upstream Go project, then modified:
+ // https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416
+ "syscall/js.copyBytesToJS": (ret_addr, dst_ref, src_addr, src_len, src_cap) => {
+ let num_bytes_copied_addr = ret_addr;
+ let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable
+
+ const dst = unboxValue(dst_ref);
+ const src = loadSlice(src_addr, src_len);
+ if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
+ mem().setUint8(returned_status_addr, 0); // Return "not ok" status
+ return;
+ }
+ const toCopy = src.subarray(0, dst.length);
+ dst.set(toCopy);
+ mem().setUint32(num_bytes_copied_addr, toCopy.length, true);
+ mem().setUint8(returned_status_addr, 1); // Return "ok" status
+ },
+ }
+ };
+
+ // Go 1.20 uses 'env'. Go 1.21 uses 'gojs'.
+ // For compatibility, we use both as long as Go 1.20 is supported.
+ this.importObject.env = this.importObject.gojs;
+ }
+
+ async run(instance) {
+ this._inst = instance;
+ this._values = [ // JS values that Go currently has references to, indexed by reference id
+ NaN,
+ 0,
+ null,
+ true,
+ false,
+ global,
+ this,
+ ];
+ this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id
+ this._ids = new Map(); // mapping from JS values to reference ids
+ this._idPool = []; // unused ids that have been garbage collected
+ this.exited = false; // whether the Go program has exited
+ this.exitCode = 0;
+
+ if (this._inst.exports._start) {
+ let exitPromise = new Promise((resolve, reject) => {
+ this._resolveExitPromise = resolve;
+ });
+
+ // Run program, but catch the wasmExit exception that's thrown
+ // to return back here.
+ try {
+ this._inst.exports._start();
+ } catch (e) {
+ if (e !== wasmExit) throw e;
+ }
+
+ await exitPromise;
+ return this.exitCode;
+ } else {
+ this._inst.exports._initialize();
+ }
+ }
+
+ _resume() {
+ if (this.exited) {
+ throw new Error("Go program has already exited");
+ }
+ try {
+ this._inst.exports.resume();
+ } catch (e) {
+ if (e !== wasmExit) throw e;
+ }
+ if (this.exited) {
+ this._resolveExitPromise();
+ }
+ }
+
+ _makeFuncWrapper(id) {
+ const go = this;
+ return function () {
+ const event = { id: id, this: this, args: arguments };
+ go._pendingEvent = event;
+ go._resume();
+ return event.result;
+ };
+ }
+ }
+
+ if (
+ global.require &&
+ global.require.main === module &&
+ global.process &&
+ global.process.versions &&
+ !global.process.versions.electron
+ ) {
+ if (process.argv.length != 3) {
+ console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
+ process.exit(1);
+ }
+
+ const go = new Go();
+ WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then(async (result) => {
+ let exitCode = await go.run(result.instance);
+ process.exit(exitCode);
+ }).catch((err) => {
+ console.error(err);
+ process.exit(1);
+ });
+ }
+})();
diff --git a/borders.go b/borders.go
index 631a4a3c..1530e11a 100644
--- a/borders.go
+++ b/borders.go
@@ -225,37 +225,72 @@ func NormalBorder() Border {
return normalBorder
}
+//go:export BorderNormalBorder
+func wasmNormalBorder() *Border {
+ return &normalBorder
+}
+
// RoundedBorder returns a border with rounded corners.
func RoundedBorder() Border {
return roundedBorder
}
+//go:export BorderRoundedBorder
+func wasmRoundedBorder() *Border {
+ return &roundedBorder
+}
+
// BlockBorder returns a border that takes the whole block.
func BlockBorder() Border {
return blockBorder
}
+//go:export BorderBlockBorder
+func wasmBlockBorder() *Border {
+ return &blockBorder
+}
+
// OuterHalfBlockBorder returns a half-block border that sits outside the frame.
func OuterHalfBlockBorder() Border {
return outerHalfBlockBorder
}
+//go:export BorderOuterHalfBlockBorder
+func wasmOuterHalfBlockBorder() *Border {
+ return &outerHalfBlockBorder
+}
+
// InnerHalfBlockBorder returns a half-block border that sits inside the frame.
func InnerHalfBlockBorder() Border {
return innerHalfBlockBorder
}
+//go:export BorderInnerHalfBlockBorder
+func wasmInnerHalfBlockBorder() *Border {
+ return &innerHalfBlockBorder
+}
+
// ThickBorder returns a border that's thicker than the one returned by
// NormalBorder.
func ThickBorder() Border {
return thickBorder
}
+//go:export BorderThickBorder
+func wasmThickBorder() *Border {
+ return &thickBorder
+}
+
// DoubleBorder returns a border comprised of two thin strokes.
func DoubleBorder() Border {
return doubleBorder
}
+//go:export BorderDoubleBorder
+func wasmDoubleBorder() *Border {
+ return &doubleBorder
+}
+
// HiddenBorder returns a border that renders as a series of single-cell
// spaces. It's useful for cases when you want to remove a standard border but
// maintain layout positioning. This said, you can still apply a background
@@ -264,6 +299,11 @@ func HiddenBorder() Border {
return hiddenBorder
}
+//go:export BorderHiddenBorder
+func wasmHiddenBorder() *Border {
+ return &hiddenBorder
+}
+
// MarkdownBorder return a table border in markdown style.
//
// Make sure to disable top and bottom border for the best result. This will
@@ -274,6 +314,11 @@ func MarkdownBorder() Border {
return markdownBorder
}
+//go:export BorderMarkdownBorder
+func wasmMarkdownBorder() *Border {
+ return &markdownBorder
+}
+
// ASCIIBorder returns a table border with ASCII characters.
func ASCIIBorder() Border {
return asciiBorder
@@ -325,6 +370,11 @@ func (s Style) borderBlend(width, height int, colors ...color.Color) *borderBlen
return blend
}
+//go:export BorderASCIIBorder
+func wasmASCIIBorder() *Border {
+ return &asciiBorder
+}
+
func (s Style) applyBorder(str string) string {
var (
border = s.getBorderStyle()
diff --git a/color.go b/color.go
index 7443dc10..8a738c13 100644
--- a/color.go
+++ b/color.go
@@ -60,6 +60,19 @@ func (n NoColor) RGBA() (r, g, b, a uint32) {
return 0x0, 0x0, 0x0, 0xFFFF //nolint:mnd
}
+//go:export NoColor
+func wasmNoColor() *color.Color {
+ // Empty color str returns NoColor type.
+ ptr := Color("")
+ return &ptr
+}
+
+//go:export Color
+func wasmColor(str string) *color.Color {
+ ptr := Color(str)
+ return &ptr
+}
+
// Color specifies a color by hex or ANSI256 value. For example:
//
// ansiColor := lipgloss.Color("1") // The same as lipgloss.Red
diff --git a/examples/layout/main.go b/examples/layout/main.go
index df9bb971..e9517874 100644
--- a/examples/layout/main.go
+++ b/examples/layout/main.go
@@ -60,27 +60,9 @@ func main() {
// Tabs.
- activeTabBorder = lipgloss.Border{
- Top: "─",
- Bottom: " ",
- Left: "│",
- Right: "│",
- TopLeft: "╭",
- TopRight: "╮",
- BottomLeft: "┘",
- BottomRight: "└",
- }
+ activeTabBorder = lipgloss.NormalBorder()
- tabBorder = lipgloss.Border{
- Top: "─",
- Bottom: "─",
- Left: "│",
- Right: "│",
- TopLeft: "╭",
- TopRight: "╮",
- BottomLeft: "┴",
- BottomRight: "┴",
- }
+ tabBorder = lipgloss.NormalBorder()
tab = lipgloss.NewStyle().
Border(tabBorder, true).
@@ -215,6 +197,7 @@ func main() {
)
physicalWidth, _, _ := term.GetSize(os.Stdout.Fd())
+ fmt.Println(physicalWidth)
doc := strings.Builder{}
// Tabs.
@@ -227,6 +210,12 @@ func main() {
tab.Render("Mascara"),
tab.Render("Foundation"),
)
+ // out, err := json.Marshal(row)
+ // if err != nil {
+ // panic(err)
+ // }
+ // fmt.Println(string(out))
+ fmt.Print(max(0, width-lipgloss.Width(row)-2))
gap := tabGap.Render(strings.Repeat(" ", max(0, width-lipgloss.Width(row)-2)))
row = lipgloss.JoinHorizontal(lipgloss.Bottom, row, gap)
doc.WriteString(row + "\n\n")
diff --git a/join.go b/join.go
index 3e6688e2..1d737587 100644
--- a/join.go
+++ b/join.go
@@ -3,10 +3,53 @@ package lipgloss
import (
"math"
"strings"
+ "unsafe"
"github.com/charmbracelet/x/ansi"
)
+//go:export JoinVertical
+func wasmJoinVertical(pos Position, ptrArray *uint32, count int) *string {
+ // Convert to a slice of pointers
+ pointerArray := unsafe.Slice(ptrArray, count*2)
+ strs := []string{}
+
+ for i := range count {
+ strPtr := uintptr(pointerArray[i*2])
+ strLen := int(pointerArray[i*2+1])
+
+ // Create a byte slice and convert to string
+ bytes := unsafe.Slice((*byte)(unsafe.Pointer(strPtr)), strLen)
+ str := string(bytes)
+
+ strs = append(strs, str)
+ }
+
+ str := JoinVertical(pos, strs...)
+ return &str
+}
+
+//go:export JoinHorizontal
+func wasmJoinHorizontal(pos Position, ptrArray *uint32, count int) *string {
+ // Convert to a slice of pointers
+ pointerArray := unsafe.Slice(ptrArray, count*2)
+ strs := []string{}
+
+ for i := range count {
+ strPtr := uintptr(pointerArray[i*2])
+ strLen := int(pointerArray[i*2+1])
+
+ // Create a byte slice and convert to string
+ bytes := unsafe.Slice((*byte)(unsafe.Pointer(strPtr)), strLen)
+ str := string(bytes)
+
+ strs = append(strs, str)
+ }
+
+ str := JoinHorizontal(pos, strs...)
+ return &str
+}
+
// JoinHorizontal is a utility function for horizontally joining two
// potentially multi-lined strings along a vertical axis. The first argument is
// the position, with 0 being all the way at the top and 1 being all the way
diff --git a/list/list.go b/list/list.go
index 388c6e2b..628d1b90 100644
--- a/list/list.go
+++ b/list/list.go
@@ -47,6 +47,8 @@ type List struct{ tree *tree.Tree }
//
// Items can be other lists, trees, tables, rendered markdown;
// anything you want, really.
+//
+//go:export ListNew
func New(items ...any) *List {
l := &List{tree: tree.New()}
return l.Items(items...).
@@ -81,12 +83,16 @@ type Items tree.Children
type StyleFunc func(items Items, index int) lipgloss.Style
// Hidden returns whether this list is hidden.
+//
+//go:export ListHidden
func (l *List) Hidden() bool {
return l.tree.Hidden()
}
// Hide hides this list.
// If this list is hidden, it will not be shown when rendered.
+//
+//go:export ListHide
func (l *List) Hide(hide bool) *List {
l.tree.Hide(hide)
return l
diff --git a/list/wasm.go b/list/wasm.go
new file mode 100644
index 00000000..71b7c78f
--- /dev/null
+++ b/list/wasm.go
@@ -0,0 +1,46 @@
+//go:build wasm
+// +build wasm
+
+package list
+
+import "github.com/charmbracelet/lipgloss/v2"
+
+//go:export ListStyleFunc
+func (l *List) wasmStyleFunc(id int32) *List {
+ l.ItemStyleFunc(func(items Items, index int) lipgloss.Style {
+ style := lipgloss.GetStyleFromJS(id, int32(index), 0)
+ if style == nil {
+ return lipgloss.NewStyle()
+ }
+ return *style
+ })
+ return l
+}
+
+//go:export ListEnumeratorStyleFunc
+func (l *List) wasmEnumeratorStyleFunc(id int32) *List {
+ l.EnumeratorStyleFunc(func(items Items, index int) lipgloss.Style {
+ style := lipgloss.GetStyleFromJS(id, int32(index), 0)
+ if style == nil {
+ return lipgloss.NewStyle()
+ }
+ return *style
+ })
+ return l
+}
+
+//go:export ListRenderPtr
+func (l *List) wasmRenderPtr() *byte {
+ // Return a pointer to the first byte of the string
+ str := l.String()
+ if len(str) == 0 {
+ return nil
+ }
+ return &([]byte(str)[0])
+}
+
+//go:export ListRenderLength
+func (l *List) wasmRenderLength() int {
+ // Return the length of the string
+ return len(l.String())
+}
\ No newline at end of file
diff --git a/position.go b/position.go
index 508141b7..6914892e 100644
--- a/position.go
+++ b/position.go
@@ -31,12 +31,45 @@ const (
Right Position = 1.0
)
+//go:export PositionBottom
+func wasmGetPositionBottom() float64 {
+ return float64(Bottom)
+}
+
+//go:export PositionCenter
+func wasmGetPositionCenter() float64 {
+ return float64(Center)
+}
+
+//go:export PositionLeft
+func wasmGetPositionLeft() float64 {
+ return float64(Left)
+}
+
+//go:export PositionRight
+func wasmGetPositionRight() float64 {
+ return float64(Right)
+}
+
+//go:export PositionTop
+func GetPositionTop() float64 {
+ return float64(Top)
+}
+
// Place places a string or text block vertically in an unstyled box of a given
// width or height.
func Place(width, height int, hPos, vPos Position, str string, opts ...WhitespaceOption) string {
return PlaceVertical(height, vPos, PlaceHorizontal(width, hPos, str, opts...), opts...)
}
+//go:export PositionPlace
+func wasmPlace(width, height int32, hPos, vPos Position, str string, opts ...WhitespaceOption) *string {
+ opts = append(opts, WithWhitespaceChars("猫咪"))
+ opts = append(opts, WithWhitespaceStyle(NewStyle().Foreground(Color("#383838"))))
+ result := PlaceVertical(int(height), vPos, PlaceHorizontal(int(width), hPos, str, opts...), opts...)
+ return &result
+}
+
// PlaceHorizontal places a string or text block horizontally in an unstyled
// block of a given width. If the given width is shorter than the max width of
// the string (measured by its longest line) this will be a noop.
diff --git a/set.go b/set.go
index 0fdb69b7..3eee7735 100644
--- a/set.go
+++ b/set.go
@@ -1,6 +1,8 @@
package lipgloss
-import "image/color"
+import (
+ "image/color"
+)
// Set a value on the underlying rules map.
func (s *Style) set(key propKey, value any) {
@@ -182,6 +184,12 @@ func (s Style) Bold(v bool) Style {
return s
}
+//go:export StyleBold
+func (s *Style) wasmBold(v bool) *Style {
+ s.set(boldKey, v)
+ return s
+}
+
// Italic sets an italic formatting rule. In some terminal emulators this will
// render with "reverse" coloring if not italic font variant is available.
func (s Style) Italic(v bool) Style {
@@ -189,6 +197,12 @@ func (s Style) Italic(v bool) Style {
return s
}
+//go:export StyleItalic
+func (s *Style) wasmItalic(v bool) *Style {
+ s.set(italicKey, v)
+ return s
+}
+
// Underline sets an underline rule. By default, underlines will not be drawn on
// whitespace like margins and padding. To change this behavior set
// [Style.UnderlineSpaces].
@@ -197,6 +211,12 @@ func (s Style) Underline(v bool) Style {
return s
}
+//go:export StyleUnderline
+func (s *Style) wasmUnderline(v bool) *Style {
+ s.set(underlineKey, v)
+ return s
+}
+
// Strikethrough sets a strikethrough rule. By default, strikes will not be
// drawn on whitespace like margins and padding. To change this behavior set
// StrikethroughSpaces.
@@ -205,42 +225,93 @@ func (s Style) Strikethrough(v bool) Style {
return s
}
+//go:export StyleStrikethrough
+func (s *Style) wasmStrikethrough(v bool) *Style {
+ s.set(strikethroughKey, v)
+ return s
+}
+
// Reverse sets a rule for inverting foreground and background colors.
func (s Style) Reverse(v bool) Style {
s.set(reverseKey, v)
return s
}
+//go:export StyleReverse
+func (s *Style) wasmReverse(v bool) *Style {
+ s.set(reverseKey, v)
+ return s
+}
+
// Blink sets a rule for blinking foreground text.
func (s Style) Blink(v bool) Style {
s.set(blinkKey, v)
return s
}
+//go:export StyleBlink
+func (s *Style) wasmBlink(v bool) *Style {
+ s.set(blinkKey, v)
+ return s
+}
+
// Faint sets a rule for rendering the foreground color in a dimmer shade.
func (s Style) Faint(v bool) Style {
s.set(faintKey, v)
return s
}
+//go:export StyleFaint
+func (s *Style) wasmFaint(v bool) *Style {
+ s.set(faintKey, v)
+ return s
+}
+
// Foreground sets a foreground color.
//
// // Sets the foreground to blue
// s := lipgloss.NewStyle().Foreground(lipgloss.Color("#0000ff"))
//
// // Removes the foreground color
-// s.Foreground(lipgloss.NoColor)
+// s.Foreground(lipgloss.NoColor)//
func (s Style) Foreground(c color.Color) Style {
s.set(foregroundKey, c)
return s
}
+//go:export StyleForeground
+func (s *Style) wasmForeground(c *color.Color) *Style {
+ switch v := (*c).(type) {
+ case color.RGBA:
+ s.set(foregroundKey, v)
+ case color.Color:
+ s.set(foregroundKey, v)
+ default:
+ // noop
+ }
+
+ return s
+}
+
// Background sets a background color.
func (s Style) Background(c color.Color) Style {
s.set(backgroundKey, c)
return s
}
+//go:export StyleBackground
+func (s *Style) wasmBackground(c *color.Color) *Style {
+ switch v := (*c).(type) {
+ case color.RGBA:
+ s.set(backgroundKey, v)
+ case color.Color:
+ s.set(backgroundKey, v)
+ default:
+ // noop
+ }
+ return s
+}
+
// Width sets the width of the block before applying margins. This means your
// styled content will exactly equal the size set here. Text will wrap based on
// Padding and Borders set on the style.
@@ -249,6 +320,12 @@ func (s Style) Width(i int) Style {
return s
}
+//go:export StyleWidth
+func (s *Style) wasmWidth(i int32) *Style {
+ s.set(widthKey, int(i))
+ return s
+}
+
// Height sets the height of the block before applying margins. If the height of
// the text block is less than this value after applying padding (or not), the
// block will be set to this height.
@@ -257,6 +334,12 @@ func (s Style) Height(i int) Style {
return s
}
+//go:export StyleHeight
+func (s *Style) wasmHeight(i int32) *Style {
+ s.set(heightKey, int(i))
+ return s
+}
+
// Align is a shorthand method for setting horizontal and vertical alignment.
//
// With one argument, the position value is applied to the horizontal alignment.
@@ -279,12 +362,24 @@ func (s Style) AlignHorizontal(p Position) Style {
return s
}
+//go:export StyleAlignHorizontal
+func (s *Style) wasmAlignHorizontal(pos Position) *Style {
+ s.set(alignHorizontalKey, pos)
+ return s
+}
+
// AlignVertical sets a vertical text alignment rule.
func (s Style) AlignVertical(p Position) Style {
s.set(alignVerticalKey, p)
return s
}
+//go:export StyleAlignVertical
+func (s *Style) wasmAlignVertical(pos Position) *Style {
+ s.set(alignVerticalKey, pos)
+ return s
+}
+
// Padding is a shorthand method for setting padding on all sides at once.
//
// With one argument, the value is applied to all sides.
@@ -318,24 +413,48 @@ func (s Style) PaddingLeft(i int) Style {
return s
}
+//go:export StylePaddingLeft
+func (s *Style) wasmPaddingLeft(i int32) *Style {
+ s.set(paddingLeftKey, int(i))
+ return s
+}
+
// PaddingRight adds padding on the right.
func (s Style) PaddingRight(i int) Style {
s.set(paddingRightKey, i)
return s
}
+//go:export StylePaddingRight
+func (s *Style) wasmPaddingRight(i int32) *Style {
+ s.set(paddingRightKey, int(i))
+ return s
+}
+
// PaddingTop adds padding to the top of the block.
func (s Style) PaddingTop(i int) Style {
s.set(paddingTopKey, i)
return s
}
+//go:export StylePaddingTop
+func (s *Style) wasmPaddingTop(i int32) *Style {
+ s.set(paddingTopKey, int(i))
+ return s
+}
+
// PaddingBottom adds padding to the bottom of the block.
func (s Style) PaddingBottom(i int) Style {
s.set(paddingBottomKey, i)
return s
}
+//go:export StylePaddingBottom
+func (s *Style) wasmPaddingBottom(i int32) *Style {
+ s.set(paddingBottomKey, int(i))
+ return s
+}
+
// PaddingChar sets the character used for padding. This is useful for
// rendering blocks with a specific character, such as a space or a dot.
// Example of using [NBSP] as padding to prevent line breaks:
@@ -354,6 +473,8 @@ func (s Style) PaddingChar(r rune) Style {
// effects.
//
// Deprecated: Just use margins and padding.
+//
+//go:export StyleColorWhitespace
func (s Style) ColorWhitespace(v bool) Style {
s.set(colorWhitespaceKey, v)
return s
@@ -392,27 +513,53 @@ func (s Style) MarginLeft(i int) Style {
return s
}
+//go:export StyleMarginLeft
+func (s *Style) wasmMarginLeft(i int32) *Style {
+ s.set(marginLeftKey, int(i))
+ return s
+}
+
// MarginRight sets the value of the right margin.
func (s Style) MarginRight(i int) Style {
s.set(marginRightKey, i)
return s
}
+//go:export StyleMarginRight
+func (s *Style) wasmMarginRight(i int) *Style {
+ s.set(marginRightKey, int(i))
+ return s
+}
+
// MarginTop sets the value of the top margin.
func (s Style) MarginTop(i int) Style {
s.set(marginTopKey, i)
return s
}
+//go:export StyleMarginTop
+func (s *Style) wasmMarginTop(i int) *Style {
+ s.set(marginTopKey, int(i))
+ return s
+}
+
// MarginBottom sets the value of the bottom margin.
func (s Style) MarginBottom(i int) Style {
s.set(marginBottomKey, i)
return s
}
+//go:export StyleMarginBottom
+func (s *Style) wasmMarginBottom(i int32) *Style {
+ s.set(marginBottomKey, int(i))
+ return s
+}
+
// MarginBackground sets the background color of the margin. Note that this is
// also set when inheriting from a style with a background color. In that case
// the background color on that style will set the margin color on this style.
+//
+//go:export StyleMarginBackground
func (s Style) MarginBackground(c color.Color) Style {
s.set(marginBackgroundKey, c)
return s
@@ -467,6 +614,13 @@ func (s Style) Border(b Border, sides ...bool) Style {
return s
}
+//go:export StyleBorder
+func (s *Style) wasmBorder(b *Border) *Style {
+ border := *b
+ s.set(borderStyleKey, border)
+ return s
+}
+
// BorderStyle defines the Border on a style. A Border contains a series of
// definitions for the sides and corners of a border.
//
@@ -492,24 +646,48 @@ func (s Style) BorderTop(v bool) Style {
return s
}
+//go:export SetBorderTop
+func (s *Style) wasmBorderTop(v bool) *Style {
+ s.set(borderTopKey, v)
+ return s
+}
+
// BorderRight determines whether or not to draw a right border.
func (s Style) BorderRight(v bool) Style {
s.set(borderRightKey, v)
return s
}
+//go:export SetBorderRight
+func (s *Style) wasmBorderRight(v bool) *Style {
+ s.set(borderRightKey, v)
+ return s
+}
+
// BorderBottom determines whether or not to draw a bottom border.
func (s Style) BorderBottom(v bool) Style {
s.set(borderBottomKey, v)
return s
}
+//go:export SetBorderBottom
+func (s *Style) wasmBorderBottom(v bool) *Style {
+ s.set(borderBottomKey, v)
+ return s
+}
+
// BorderLeft determines whether or not to draw a left border.
func (s Style) BorderLeft(v bool) Style {
s.set(borderLeftKey, v)
return s
}
+//go:export SetBorderLeft
+func (s *Style) wasmBorderLeft(v bool) *Style {
+ s.set(borderLeftKey, v)
+ return s
+}
+
// BorderForeground is a shorthand function for setting all of the
// foreground colors of the borders at once. The arguments work as follows:
//
@@ -549,6 +727,13 @@ func (s Style) BorderTopForeground(c color.Color) Style {
return s
}
+//go:export StyleBorderTopForeground
+func (s *Style) wasmBorderTopForeground(c *color.Color) *Style {
+ color := (*c).(color.Color)
+ s.set(borderTopForegroundKey, color)
+ return s
+}
+
// BorderRightForeground sets the foreground color for the right side of the
// border.
func (s Style) BorderRightForeground(c color.Color) Style {
@@ -556,6 +741,13 @@ func (s Style) BorderRightForeground(c color.Color) Style {
return s
}
+//go:export StyleBorderRightForeground
+func (s *Style) wasmBorderRightForeground(c *color.Color) *Style {
+ color := (*c).(color.Color)
+ s.set(borderRightForegroundKey, color)
+ return s
+}
+
// BorderBottomForeground sets the foreground color for the bottom of the
// border.
func (s Style) BorderBottomForeground(c color.Color) Style {
@@ -563,6 +755,13 @@ func (s Style) BorderBottomForeground(c color.Color) Style {
return s
}
+//go:export StyleBorderBottomForeground
+func (s *Style) wasmBorderBottomForeground(c *color.Color) *Style {
+ color := (*c).(color.Color)
+ s.set(borderBottomForegroundKey, color)
+ return s
+}
+
// BorderLeftForeground sets the foreground color for the left side of the
// border.
func (s Style) BorderLeftForeground(c color.Color) Style {
@@ -618,6 +817,13 @@ func (s Style) BorderForegroundBlendOffset(v int) Style {
return s
}
+//go:export StyleBorderLeftForeground
+func (s *Style) wasmBorderLeftForeground(c *color.Color) *Style {
+ color := (*c).(color.Color)
+ s.set(borderLeftForegroundKey, color)
+ return s
+}
+
// BorderBackground is a shorthand function for setting all of the
// background colors of the borders at once. The arguments work as follows:
//
@@ -657,12 +863,26 @@ func (s Style) BorderTopBackground(c color.Color) Style {
return s
}
+//go:export StyleBorderTopBackground
+func (s *Style) wasmBorderTopBackground(c *color.Color) *Style {
+ color := (*c).(color.Color)
+ s.set(borderTopBackgroundKey, color)
+ return s
+}
+
// BorderRightBackground sets the background color of right side the border.
func (s Style) BorderRightBackground(c color.Color) Style {
s.set(borderRightBackgroundKey, c)
return s
}
+//go:export StyleBorderRightBackground
+func (s *Style) wasmBorderRightBackground(c *color.Color) *Style {
+ color := (*c).(color.Color)
+ s.set(borderRightBackgroundKey, color)
+ return s
+}
+
// BorderBottomBackground sets the background color of the bottom of the
// border.
func (s Style) BorderBottomBackground(c color.Color) Style {
@@ -670,6 +890,13 @@ func (s Style) BorderBottomBackground(c color.Color) Style {
return s
}
+//go:export StyleBorderBottomBackground
+func (s *Style) wasmBorderBottomBackground(c *color.Color) *Style {
+ color := (*c).(color.Color)
+ s.set(borderBottomBackgroundKey, color)
+ return s
+}
+
// BorderLeftBackground set the background color of the left side of the
// border.
func (s Style) BorderLeftBackground(c color.Color) Style {
@@ -677,6 +904,13 @@ func (s Style) BorderLeftBackground(c color.Color) Style {
return s
}
+//go:export StyleBorderLeftBackground
+func (s *Style) wasmBorderLeftBackground(c *color.Color) *Style {
+ color := (*c).(color.Color)
+ s.set(borderLeftBackgroundKey, color)
+ return s
+}
+
// Inline makes rendering output one line and disables the rendering of
// margins, padding and borders. This is useful when you need a style to apply
// only to font rendering and don't want it to change any physical dimensions.
@@ -690,6 +924,8 @@ func (s Style) BorderLeftBackground(c color.Color) Style {
// var userInput string = "..."
// var userStyle = text.Style{ /* ... */ }
// fmt.Println(userStyle.Inline(true).Render(userInput))
+//
+//go:export StyleInline
func (s Style) Inline(v bool) Style {
o := s // copy
o.set(inlineKey, v)
@@ -714,6 +950,12 @@ func (s Style) MaxWidth(n int) Style {
return o
}
+//go:export StyleMaxWidth
+func (s *Style) wasmMaxWidth(n int32) *Style {
+ m := s.MaxWidth(int(n))
+ return &m
+}
+
// MaxHeight applies a max height to a given style. This is useful in enforcing
// a certain height at render time, particularly with arbitrary strings and
// styles.
@@ -726,6 +968,12 @@ func (s Style) MaxHeight(n int) Style {
return o
}
+//go:export StyleMaxHeight
+func (s *Style) wasmMaxHeight(n int32) *Style {
+ m := s.MaxHeight(int(n))
+ return &m
+}
+
// NoTabConversion can be passed to [Style.TabWidth] to disable the replacement
// of tabs with spaces at render time.
const NoTabConversion = -1
@@ -743,9 +991,17 @@ func (s Style) TabWidth(n int) Style {
return s
}
+//go:export StyleTabWidth
+func (s *Style) wasmTabWidth(n int32) *Style {
+ m := s.TabWidth(int(n))
+ return &m
+}
+
// UnderlineSpaces determines whether to underline spaces between words. By
// default, this is true. Spaces can also be underlined without underlining the
// text itself.
+//
+//go:export StyleUnderlineSpaces
func (s Style) UnderlineSpaces(v bool) Style {
s.set(underlineSpacesKey, v)
return s
@@ -754,6 +1010,8 @@ func (s Style) UnderlineSpaces(v bool) Style {
// StrikethroughSpaces determines whether to apply strikethroughs to spaces
// between words. By default, this is true. Spaces can also be struck without
// underlining the text itself.
+//
+//go:export StyleStrikethroughSpaces
func (s Style) StrikethroughSpaces(v bool) Style {
s.set(strikethroughSpacesKey, v)
return s
diff --git a/size.go b/size.go
index e0384d03..fa4fd726 100644
--- a/size.go
+++ b/size.go
@@ -12,6 +12,8 @@ import (
//
// You should use this instead of len(string) or len([]rune(string) as neither
// will give you accurate results.
+//
+//go:export Width
func Width(str string) (width int) {
for l := range strings.SplitSeq(str, "\n") {
w := ansi.StringWidth(l)
@@ -26,6 +28,8 @@ func Width(str string) (width int) {
// Height returns height of a string in cells. This is done simply by
// counting \n characters. If your output has \r\n, that sequence will be
// replaced with a \n in [Style.Render].
+//
+//go:export Height
func Height(str string) int {
return strings.Count(str, "\n") + 1
}
diff --git a/style.go b/style.go
index 8b5a7df0..97118720 100644
--- a/style.go
+++ b/style.go
@@ -1,9 +1,11 @@
package lipgloss
import (
+ "fmt"
"image/color"
"strings"
"unicode"
+ "unsafe"
"github.com/charmbracelet/x/ansi"
"github.com/charmbracelet/x/cellbuf"
@@ -115,6 +117,8 @@ func NewStyle() Style {
type Style struct {
props props
value string
+ // solely used by wasm
+ wasmValue string
// we store bool props values here
attrs int
@@ -231,7 +235,143 @@ func (s Style) Inherit(i Style) Style {
return s
}
+// Note: haven't figured out yet why
+//
+//go:export StyleInherit
+func (s *Style) wasmInherit(i *Style) *Style {
+ for k := boldKey; k <= transformKey; k <<= 1 {
+ if !i.isSet(k) {
+ continue
+ }
+
+ switch k { //nolint:exhaustive
+ case marginTopKey, marginRightKey, marginBottomKey, marginLeftKey:
+ // Margins are not inherited
+ continue
+ case paddingTopKey, paddingRightKey, paddingBottomKey, paddingLeftKey:
+ // Padding is not inherited
+ continue
+ case backgroundKey:
+ // The margins also inherit the background color
+ if !s.isSet(marginBackgroundKey) && !i.isSet(marginBackgroundKey) {
+ s.set(marginBackgroundKey, i.bgColor)
+ }
+ }
+
+ if s.isSet(k) {
+ continue
+ }
+
+ s.setFrom(k, *i)
+ }
+ return s
+}
+
+//go:export StyleRender
+func (s *Style) wasmRender() *string {
+ // Return a pointer to the first byte of the string
+ result := s.Render()
+ return &result
+}
+
+//go:export StyleClearValue
+func (s *Style) wasmClearValue() *Style {
+ s.wasmValue = ""
+ return s
+}
+
+//go:export StyleSetString
+func (s *Style) wasmSetString(ptrArray *uint32, count int) *Style {
+ // Convert to a slice of pointers
+ pointerArray := unsafe.Slice(ptrArray, count*2)
+ values := []string{}
+
+ // Process each string
+ for i := range count {
+ strPtr := uintptr(pointerArray[i*2])
+ strLen := int(pointerArray[i*2+1])
+
+ // Create a byte slice and convert to string
+ bytes := unsafe.Slice((*byte)(unsafe.Pointer(strPtr)), strLen)
+ str := string(bytes)
+
+ values = append(values, str)
+ }
+
+ if len(values) > 0 {
+ s.value = joinString(values...)
+ }
+
+ return s
+}
+
+//go:export StyleJoinString
+func (s *Style) wasmJoinString(ptrArray *uint32, count int) *Style {
+ // Convert to a slice of pointers
+ pointerArray := unsafe.Slice(ptrArray, count*2)
+ values := []string{}
+
+ // Process each string
+ for i := range count {
+ strPtr := uintptr(pointerArray[i*2])
+ strLen := int(pointerArray[i*2+1])
+
+ // Create a byte slice and convert to string
+ bytes := unsafe.Slice((*byte)(unsafe.Pointer(strPtr)), strLen)
+ str := string(bytes)
+
+ values = append(values, str)
+ }
+
+ if len(values) > 0 {
+ s.value = strings.Join(values, "")
+ }
+
+ return s
+}
+
+//go:export StyleJoinStyled
+func wasmJoinStyled(ptrArray *uint32, count int, bgColor, fgColor uint32) *string {
+ // Convert to a slice of pointers
+ pointerArray := unsafe.Slice(ptrArray, count*2)
+ values := []string{}
+
+ // Process each string
+ for i := range count {
+ strPtr := uintptr(pointerArray[i*2])
+ strLen := int(pointerArray[i*2+1])
+
+ // Create a byte slice and convert to string
+ bytes := unsafe.Slice((*byte)(unsafe.Pointer(strPtr)), strLen)
+ str := string(bytes)
+
+ // Strip ANSI codes to get plain text
+ plainText := ansi.Strip(str)
+ values = append(values, plainText)
+ }
+
+ if len(values) == 0 {
+ empty := ""
+ return &empty
+ }
+
+ // Create a style with the specified background and foreground
+ style := NewStyle()
+ if bgColor != 0 {
+ style = style.Background(Color(fmt.Sprintf("%d", bgColor)))
+ }
+ if fgColor != 0 {
+ style = style.Foreground(Color(fmt.Sprintf("%d", fgColor)))
+ }
+
+ // Join all the strings and apply the style
+ joined := strings.Join(values, "")
+ result := style.Render(joined)
+ return &result
+}
+
// Render applies the defined style formatting to a given string.
+
func (s Style) Render(strs ...string) string {
if s.value != "" {
strs = append([]string{s.value}, strs...)
diff --git a/table/table.go b/table/table.go
index c0b6fc80..acd91744 100644
--- a/table/table.go
+++ b/table/table.go
@@ -3,6 +3,7 @@ package table
import (
"strings"
+ "unsafe"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/x/ansi"
@@ -76,6 +77,8 @@ type Table struct {
// attributes.
//
// By default, a table has normal border, no styling, and no rows.
+//
+//go:export TableNew
func New() *Table {
return &Table{
styleFunc: DefaultStyles,
@@ -92,6 +95,8 @@ func New() *Table {
}
// ClearRows clears the table rows.
+//
+//go:export TableClearRows
func (t *Table) ClearRows() *Table {
t.data = NewStringData()
return t
@@ -150,12 +155,60 @@ func (t *Table) Row(row ...string) *Table {
return t
}
+//go:export TableRow
+func (t *Table) wasmAddRow(ptrArray *uint32, count int) *Table {
+ // Convert to a slice of pointers
+ pointerArray := unsafe.Slice(ptrArray, count*2)
+ rows := []string{}
+
+ for i := range count {
+ strPtr := uintptr(pointerArray[i*2])
+ strLen := int(pointerArray[i*2+1])
+
+ // Create a byte slice and convert to string
+ bytes := unsafe.Slice((*byte)(unsafe.Pointer(strPtr)), strLen)
+ str := string(bytes)
+
+ rows = append(rows, str)
+ }
+
+ if len(rows) > 0 {
+ t.Rows(rows)
+ }
+
+ return t
+}
+
// Headers sets the table headers.
func (t *Table) Headers(headers ...string) *Table {
t.headers = headers
return t
}
+//go:export TableHeaders
+func (t *Table) wasmAddHeaders(ptrArray *uint32, count int) *Table {
+ // Convert to a slice of pointers
+ pointerArray := unsafe.Slice(ptrArray, count*2)
+ headers := []string{}
+
+ for i := range count {
+ strPtr := uintptr(pointerArray[i*2])
+ strLen := int(pointerArray[i*2+1])
+
+ // Create a byte slice and convert to string
+ bytes := unsafe.Slice((*byte)(unsafe.Pointer(strPtr)), strLen)
+ str := string(bytes)
+
+ headers = append(headers, str)
+ }
+
+ if len(headers) > 0 {
+ t.headers = headers
+ }
+
+ return t
+}
+
// GetHeaders returns the table headers.
func (t *Table) GetHeaders() []string {
return t.headers
@@ -167,43 +220,64 @@ func (t *Table) Border(border lipgloss.Border) *Table {
return t
}
+//go:export TableBorder
+func (t *Table) wasmBorder(borderPtr *lipgloss.Border) *Table {
+ border := *borderPtr
+ t.border = border
+ return t
+}
+
// BorderTop sets the top border.
+//
+//go:export TableBorderTop
func (t *Table) BorderTop(v bool) *Table {
t.borderTop = v
return t
}
// BorderBottom sets the bottom border.
+//
+//go:export BorderBottom
func (t *Table) BorderBottom(v bool) *Table {
t.borderBottom = v
return t
}
// BorderLeft sets the left border.
+//
+//go:export TableBorderLeft
func (t *Table) BorderLeft(v bool) *Table {
t.borderLeft = v
return t
}
// BorderRight sets the right border.
+//
+//go:export TableBorderRight
func (t *Table) BorderRight(v bool) *Table {
t.borderRight = v
return t
}
// BorderHeader sets the header separator border.
+//
+//go:export TableBorderHeader
func (t *Table) BorderHeader(v bool) *Table {
t.borderHeader = v
return t
}
// BorderColumn sets the column border separator.
+//
+//go:export TableBorderColumn
func (t *Table) BorderColumn(v bool) *Table {
t.borderColumn = v
return t
}
// BorderRow sets the row border separator.
+//
+//go:export TableBorderRow
func (t *Table) BorderRow(v bool) *Table {
t.borderRow = v
return t
@@ -250,15 +324,26 @@ func (t *Table) GetBorderRow() bool {
return t.borderRow
}
+//go:export TableBorderStyle
+func (t *Table) wasmBorderStyle(stylePtr *lipgloss.Style) *Table {
+ style := *stylePtr
+ t.borderStyle = style
+ return t
+}
+
// Width sets the table width, this auto-sizes the columns to fit the width by
// either expanding or contracting the widths of each column as a best effort
// approach.
+//
+//go:export TableWidth
func (t *Table) Width(w int) *Table {
t.width = w
return t
}
// Height sets the table height.
+//
+//go:export TableHeight
func (t *Table) Height(h int) *Table {
t.height = h
t.useManualHeight = true
@@ -271,6 +356,8 @@ func (t *Table) GetHeight() int {
}
// YOffset sets the table rendering offset.
+//
+//go:export TableOffset
func (t *Table) YOffset(o int) *Table {
t.yOffset = o
return t
@@ -302,6 +389,8 @@ func (t *Table) VisibleRows() int {
// Wrap dictates whether or not the table content should wrap.
//
// This only applies to data cells. Headers are never wrapped.
+//
+//go:export TableWrap
func (t *Table) Wrap(w bool) *Table {
t.wrap = w
return t
@@ -374,6 +463,18 @@ func (t *Table) Render() string {
return t.String()
}
+//go:export TableRenderPtr
+func (t *Table) wasmRenderPtr() *byte {
+ // Return a pointer to the first byte of the string
+ return &([]byte(t.String())[0])
+}
+
+//go:export TableRenderLength
+func (t *Table) wasmRenderLenght() int {
+ // Return the length of the string
+ return len(t.String())
+}
+
// constructTopBorder constructs the top border for the table given it's current
// border configuration and data.
func (t *Table) constructTopBorder() string {
diff --git a/table/wasm.go b/table/wasm.go
new file mode 100644
index 00000000..2a2b04d1
--- /dev/null
+++ b/table/wasm.go
@@ -0,0 +1,88 @@
+//go:build wasm
+// +build wasm
+
+package table
+
+import (
+ "unsafe"
+
+ "github.com/charmbracelet/lipgloss/v2"
+)
+
+//go:export TableStyleFunc
+func (t *Table) wasmStyleFunc(id int32) *Table {
+ t.styleFunc = func(row, col int) lipgloss.Style {
+ style := lipgloss.GetStyleFromJS(id, int32(row), int32(col))
+ if style == nil {
+ return lipgloss.NewStyle()
+ }
+ return *style
+ }
+ return t
+}
+
+//go:export TableDataNew
+func wasmNewStringData() *StringData {
+ return NewStringData()
+}
+
+//go:export TableDataAppend
+func (d *StringData) wasmAppend(ptrArray *uint32, count int) *StringData {
+ // Convert to a slice of pointers
+ pointerArray := unsafe.Slice(ptrArray, count*2)
+ row := []string{}
+
+ for i := range count {
+ ptr := pointerArray[i*2]
+ length := pointerArray[i*2+1]
+
+ if length > 0 {
+ // Convert pointer to byte slice
+ bytes := unsafe.Slice((*byte)(unsafe.Pointer(uintptr(ptr))), length)
+ row = append(row, string(bytes))
+ } else {
+ row = append(row, "")
+ }
+ }
+
+ d.Append(row)
+ return d
+}
+
+//go:export TableDataAtPtr
+func (d *StringData) wasmAtPtr(row, col int32) uintptr {
+ value := d.At(int(row), int(col))
+ if value == "" {
+ return 0
+ }
+
+ // Convert string to bytes and return pointer
+ bytes := []byte(value)
+ if len(bytes) == 0 {
+ return 0
+ }
+
+ return uintptr(unsafe.Pointer(&bytes[0]))
+}
+
+//go:export TableDataAtLength
+func (d *StringData) wasmAtLength(row, col int32) int {
+ value := d.At(int(row), int(col))
+ return len(value)
+}
+
+//go:export TableDataRows
+func (d *StringData) wasmRows() int32 {
+ return int32(d.Rows())
+}
+
+//go:export TableDataColumns
+func (d *StringData) wasmColumns() int32 {
+ return int32(d.Columns())
+}
+
+//go:export TableSetData
+func (t *Table) wasmSetData(data *StringData) *Table {
+ t.data = data
+ return t
+}
diff --git a/tree/wasm.go b/tree/wasm.go
new file mode 100644
index 00000000..d00c0f75
--- /dev/null
+++ b/tree/wasm.go
@@ -0,0 +1,61 @@
+//go:build wasm
+// +build wasm
+
+package tree
+
+import "github.com/charmbracelet/lipgloss/v2"
+
+//go:export TreeStyleFunc
+func (t *Tree) wasmStyleFunc(id int32) *Tree {
+ t.ItemStyleFunc(func(children Children, index int) lipgloss.Style {
+ style := lipgloss.GetStyleFromJS(id, int32(index), 0)
+ if style == nil {
+ return lipgloss.NewStyle()
+ }
+ return *style
+ })
+ return t
+}
+
+//go:export TreeEnumeratorStyleFunc
+func (t *Tree) wasmEnumeratorStyleFunc(id int32) *Tree {
+ t.EnumeratorStyleFunc(func(children Children, index int) lipgloss.Style {
+ style := lipgloss.GetStyleFromJS(id, int32(index), 0)
+ if style == nil {
+ return lipgloss.NewStyle()
+ }
+ return *style
+ })
+ return t
+}
+
+//go:export TreeRenderPtr
+func (t *Tree) wasmRenderPtr() *byte {
+ // Return a pointer to the first byte of the string
+ str := t.String()
+ if len(str) == 0 {
+ return nil
+ }
+ return &([]byte(str)[0])
+}
+
+//go:export TreeRenderLength
+func (t *Tree) wasmRenderLength() int {
+ // Return the length of the string
+ return len(t.String())
+}
+
+//go:export TreeEnumeratorDefault
+func wasmTreeEnumeratorDefault() int32 {
+ return 0 // DefaultEnumerator
+}
+
+//go:export TreeEnumeratorRounded
+func wasmTreeEnumeratorRounded() int32 {
+ return 1 // RoundedEnumerator
+}
+
+//go:export TreeIndenterDefault
+func wasmTreeIndenterDefault() int32 {
+ return 0 // DefaultIndenter
+}
\ No newline at end of file
diff --git a/wasm.go b/wasm.go
new file mode 100644
index 00000000..da4a4f4c
--- /dev/null
+++ b/wasm.go
@@ -0,0 +1,35 @@
+//go:build wasm
+// +build wasm
+
+package lipgloss
+
+import (
+ "unsafe"
+)
+
+// We need Style as uintpr instead of *Style because
+// it is used as part of callStyleFunc, it isn't allowed
+// to send custom struct go pointers (not u)
+//
+//go:export StyleNewStyle
+func wasmNewStyle() *Style {
+ return &Style{}
+}
+
+// Import the JavaScript function we want to call
+//
+//go:wasmimport env callStyleFunc
+func WasmStyleFunc(funcId, row, col int32) uintptr
+
+func GetStyleFromJS(funcId, row, col int32) *Style {
+ // Call the JavaScript function to get the pointer
+ ptr := WasmStyleFunc(funcId, row, col)
+
+ // Cast the pointer to our Style struct
+ if ptr == 0 {
+ return nil // Handle null pointers
+ }
+
+ // Convert the raw pointer to a Go struct pointer
+ return (*Style)(unsafe.Pointer(ptr))
+}