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). + +The Charm logo + +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)) +}