diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a48b16b..a0c4fbf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -77,7 +77,6 @@ tests/ ├── ansi.test.ts # ANSI integration tests └── *.test.ts # Additional test suites scratchpad/ # Development playground -docs/ # Module documentation ``` ## Development Workflow @@ -174,7 +173,8 @@ This creates a markdown file in `.changeset/`. ## Getting Help -- Check the [documentation](./docs/) for module guides +- Review inline TSDoc comments in `src/` for API documentation +- Check the [API Reference](https://effect-boxes.lloydrichards.dev/api/box) for module guides - Review [AGENTS.md](./AGENTS.md) for detailed coding patterns (useful for AI assistants) - Open an issue for bugs or feature requests diff --git a/README.md b/README.md index cb5b067..1ac99a3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # Effect Boxes +[![npm version](https://img.shields.io/npm/v/effect-boxes)](https://www.npmjs.com/package/effect-boxes) +[![License: BSD-3-Clause](https://img.shields.io/npm/l/effect-boxes)](./LICENSE) +[![TypeScript](https://img.shields.io/badge/TypeScript-6.0+-blue)](https://www.typescriptlang.org/) +[![Effect](https://img.shields.io/badge/power%20up-Effect-black)](https://effect.website) + A functional layout system for terminal applications built with Effect.js. Create TUIs with composable boxes, ANSI styling, and reactive components. @@ -8,20 +13,18 @@ Create TUIs with composable boxes, ANSI styling, and reactive components. ## What is Effect Boxes? Effect Boxes is a TypeScript library inspired by Haskell's -`Text.PrettyPrint.Boxes`, providing a flex-style layout system for terminal -applications within the Effect ecosystem. It started from the original box model -and function naming, then grew into its own implementation with Effect -integration, annotations, ANSI styling, and reactive rendering support. Think of -it as a CSS flexbox system, but built specifically for functional composition of -elements in terminal UIs, ASCII art, and structured text output. +`Text.PrettyPrint.Boxes`, providing a flex-style layout system for terminal applications within the Effect ecosystem. Think of it as CSS flexbox, but built for functional composition of elements in terminal UIs, ASCII art, and structured text output. -## Key Features +## Features -- **Flex-y Layout System**: Horizontal and vertical composition with alignment +- **Flex-y Layout System** — Horizontal and vertical composition with alignment control -- **Text Flow**: Automatic paragraph wrapping and column layout -- **ANSI Color Support**: Rich terminal styling with colors and text attributes -- **Reactive Components**: Create dynamic UIs with efficient partial updates +- **Text Flow** — Automatic paragraph wrapping and column layout +- **ANSI Color Support** — Rich terminal styling with colors and text attributes +- **Reactive Components** — Dynamic UIs with efficient partial updates +- **Effect Integration** — Pipeable, Equal, Hash, and dual-function APIs +- **Unicode Aware** — Correct width calculations for full-width characters and + emoji ## Installation @@ -31,111 +34,56 @@ npm install effect-boxes bun add effect-boxes # or pnpm add effect-boxes -# or -yarn add effect-boxes ``` -## Quick Start +## Quick Example ```typescript import { Box, Ansi } from "effect-boxes"; -// Alternative import patterns: -// import * as Box from "effect-boxes/Box"; -// import * as Ansi from "effect-boxes/Ansi"; -// Create a simple box with colored text and positioning const myBox = Box.hsep( [ - Box.text("Hello").pipe(Box.annotate(Ansi.green)), + Box.text("Hello").pipe(Box.annotate(Ansi.green)), Box.text("\nEffect").pipe(Box.annotate(Ansi.bold)), Box.text("Boxes!").pipe(Box.annotate(Ansi.blue)), - ], + ], 1, Box.left ); -// Render to string (with ANSI colors) console.log(Box.renderPrettySync(myBox)); -/** - * Hello Boxes! - * Effect - */ ``` -## Example: Creating a Table +## Documentation -```typescript -import { pipe } from "effect"; -import { Box } from "effect-boxes"; - -// Create a simple table layout -const createTable = (headers: string[], rows: string[][]) => { - const headerRow = Box.punctuateH( - headers.map((h) => Box.text(h).pipe(Box.alignHoriz(Box.center1, 12))), - Box.top, - Box.text(" | ") - ); - - const separator = Box.text("-".repeat(headerRow.cols)); - - const dataRows = rows.map((row) => - Box.punctuateH( - row.map((cell) => Box.text(cell).pipe(Box.alignHoriz(Box.left, 12))), - Box.top, - Box.text(" | ") - ) - ); - - return Box.vcat([headerRow, separator, ...dataRows], Box.left); -}; - -const table = createTable( - ["Name", "Age", "City"], - [ - ["Alice", "30", "New York"], - ["Bob", "25", "London"], - ["Charlie", "35", "Tokyo"], - ] -); +Full documentation, guides, and API reference are available at +[effect-boxes.lloydrichards.dev](https://effect-boxes.lloydrichards.dev). -console.log(Box.renderPlainSync(table)); -/** -* Name | Age | City -* ------------------------------------------ -* Alice | 30 | New York -* Bob | 25 | London -* Charlie | 35 | Tokyo -*/ -``` +For library-specific details (modules, usage guides, common patterns), see the +[package README](./packages/effect-boxes/README.md). -## Documentation +## Repository Structure + +This project is organized as a monorepo: -| Module | Description | -| -------------- | ------------------------------------------------------------------------- | -| **Box** | Core box creation and composition (`hcat`, `vcat`, `text`, `align`, etc.) | -| **Annotation** | Attach metadata/annotations to boxes for styling and semantics | -| **ANSI** | Terminal styling with colors and text attributes | -| **Cmd** | Terminal control commands (cursor movement, screen clearing) | -| **Reactive** | Position tracking for interactive terminal interfaces | -| **Width** | Unicode and ANSI-aware text width calculations | - -### Guides - -- [Box Module](./docs/using-box.md) - Core layout primitives and composition -- [Layout Module](./docs/using-layout.md) - Higher-level Flex, Container, and Grid layouts -- [Annotation Module](./docs/using-annotation.md) - Adding metadata to boxes -- [ANSI Module](./docs/using-ansi.md) - Terminal colors and styling -- [Cmd Module](./docs/using-cmd.md) - Terminal control sequences -- [Reactive Module](./docs/using-reactive.md) - Building interactive UIs -- [Common Patterns](./docs/common-patterns.md) - Reusable patterns and examples +| Path | Description | +| --- | --- | +| [`packages/effect-boxes`](./packages/effect-boxes) | Core library (published to npm) | +| [`apps/docs`](./apps/docs) | Documentation website | +| [`apps/scratchpad`](./apps/scratchpad) | Development playground | ## Contributing -Interested in contributing? See [CONTRIBUTING.md](./CONTRIBUTING.md) for -development setup, commands, and release process. +See [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup and workflow. + +```bash +bun install # Install dependencies +turbo run build # Build all packages +turbo run test # Run all tests +``` ## License BSD-3-Clause -This library is inspired by Haskell's `Text.PrettyPrint.Boxes` by Brent Yorgey. +Inspired by Haskell's `Text.PrettyPrint.Boxes` by Brent Yorgey. diff --git a/apps/scratchpad/01-progress-bar.ts b/apps/scratchpad/01-progress-bar.ts index 1acf483..fd9dfcc 100644 --- a/apps/scratchpad/01-progress-bar.ts +++ b/apps/scratchpad/01-progress-bar.ts @@ -7,22 +7,26 @@ import { Ref, Schedule, Stream, + Terminal, } from "effect"; import * as Ansi from "effect-boxes/Ansi"; import * as Box from "effect-boxes/Box"; import * as Cmd from "effect-boxes/Cmd"; +import { Container, Flex } from "effect-boxes/Layout"; import * as Reactive from "effect-boxes/Reactive"; const display = (msg: string) => Effect.sync(() => process.stdout.write(msg)); -const StatusBar = (status: string, counter: number, time: string) => - pipe( +const StatusBar = (status: string, counter: number, time: string, width: number) => + Flex.row( [ - Box.text(`Status: ${status}`), - Box.text(`Counter: ${counter}`), - Box.text(`⏰ ${time}`), + Flex.fixed(Box.text(`Status: ${status}`)), + Flex.fixed(Box.text(" | ")), + Flex.fixed(Box.text(`Counter: ${counter}`)), + Flex.fixed(Box.text(" | ")), + Flex.fixed(Box.text(`⏰ ${time}`)), ], - Box.punctuateH(Box.left, Box.text(" | ")) + width ); const ProgressBar = (progress: number, total: number, width: number) => { @@ -53,11 +57,16 @@ const formatTime = (timestamp: number): string => { export const main = Effect.gen(function* () { // Clear screen and hide cursor for cleaner output - yield* display(Box.renderPrettySync(Cmd.clearScreen)); - yield* display(Box.renderPrettySync(Cmd.cursorHide)); + yield* display(Box.renderPrettySync(Box.combine(Cmd.clearScreen,Cmd.cursorHide))); + + const terminal = yield* Terminal.Terminal const Complete = 1000; - const ProgressBarWidth = 69; + const ContainerWidth = (yield* terminal.columns)-2 + // For reactive updates, pre-compute the progress bar width matching Container's inner math + // Container padding [1,2] => innerWidth = 85 - 2(border) - 2*2(padding) = 79 + // percentBox = 5 + 2(border) = 7, gap = 2 + const ProgressBarInnerWidth = ContainerWidth - 2 - 4 - 7 - 2; const counterRef = yield* Ref.make(0); @@ -68,40 +77,59 @@ export const main = Effect.gen(function* () { const timeStr = formatTime(timestamp); const status = counter >= Complete ? "completed" : "running"; - const top = Box.hcat( - [ - ProgressBar(counter, Complete, ProgressBarWidth).pipe( - Reactive.makeReactive("progress-bar"), - Box.border("single") - ), - Box.text(`${percentage.toString().padStart(3)}%`).pipe( + const top = Container.make( + { width: ContainerWidth, padding: [1, 2] }, + (ctx) => { + const percentBox = Box.text( + `${percentage.toString().padStart(3)}%` + ).pipe( Box.alignHoriz(Box.right, 5), Box.annotate(percentage === 100 ? Ansi.green : Ansi.blue), Reactive.makeReactive("percentage"), - Box.border("single"), - Box.annotate(Ansi.green) - ), - ], - Box.center1 - ).pipe(Box.pad(1), Box.border("single")); - - const bottom = StatusBar(status, counter, timeStr).pipe( - Box.alignHoriz(Box.center1, 79), - Reactive.makeReactive("status-bar"), - Box.border("single") - ); + Box.border("rounded", { + annotation: Ansi.green, + sides:{left:false}}), + ); + // progress bar fills remaining space after percentage box + const barWidth = ctx.innerWidth - Box.cols(percentBox) - 2; + return Flex.row( + [ + Flex.fixed( + ProgressBar(counter, Complete, barWidth).pipe( + Reactive.makeReactive("progress-bar"), + Box.border("single") + ) + ), + Flex.fixed(percentBox), + ], + ctx.innerWidth, + { align: Box.center1 } + ); + } + ).pipe(Box.border("rounded", { sides: { bottom: false } })); + + const bottom = Container.make( + { width: ContainerWidth }, + (ctx) => + StatusBar(status, counter, timeStr, ctx.innerWidth).pipe( + Box.alignHoriz(Box.center1, ctx.innerWidth), + Reactive.makeReactive("status-bar") + ) + ).pipe(Box.border("single")); - const footer = Box.punctuateH( + const footer = Flex.row( [ - Box.text("Press"), - Box.text("Ctrl+C").pipe(Box.annotate(Ansi.blue)), - Box.text("to stop..."), + Flex.fixed(Box.text("Press")), + Flex.fixed(Box.text(" ")), + Flex.fixed(Box.text("Ctrl+C").pipe(Box.annotate(Ansi.blue))), + Flex.fixed(Box.text(" ")), + Flex.fixed(Box.text("to stop...")), ], - Box.left, - Box.text(" ") + ContainerWidth ); - return Box.punctuateV([top, bottom, footer], Box.top, Box.char(" ")); + const layout = Box.vcat([top, bottom], Box.left); + return Box.punctuateV([layout, footer], Box.top, Box.char(" ")); }; // Display the initial layout @@ -137,7 +165,7 @@ export const main = Effect.gen(function* () { Reactive.cursorToReactive("progress-bar"), Option.map((cursorCmd) => [ cursorCmd, - ProgressBar(counter, Complete, ProgressBarWidth), + ProgressBar(counter, Complete, ProgressBarInnerWidth), ]) ), pipe( @@ -157,9 +185,9 @@ export const main = Effect.gen(function* () { Reactive.cursorToReactive("status-bar"), Option.map((cursorCmd) => [ cursorCmd, - StatusBar(status, counter, timeStr).pipe( - Box.truncate(79, Box.center1), - Box.alignHoriz(Box.center1, 79) + StatusBar(status, counter, timeStr, ContainerWidth - 2).pipe( + Box.truncate(ContainerWidth - 2, Box.center1), + Box.alignHoriz(Box.center1, ContainerWidth - 2) ), ]) ), diff --git a/docs/common-patterns.md b/docs/common-patterns.md deleted file mode 100644 index f4062df..0000000 --- a/docs/common-patterns.md +++ /dev/null @@ -1,355 +0,0 @@ -# Common Patterns - -This document covers common patterns, Effect.js integration details, and -reusable components that apply across multiple modules in Effect Boxes. - -## Effect.js Integration - -Effect Boxes integrates with the Effect.js ecosystem through several key -interfaces and patterns. - -### Pipeable Interface - -All box operations support both data-first and data-last styles: - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; - -// Data-first style -const box1 = Box.moveRight(Box.text("Hello"), 5); - -// Data-last style with pipe -const box2 = pipe(Box.text("Hello"), Box.moveRight(5)); - -// Method chaining with .pipe() -const box3 = Box.text("Hello").pipe(Box.moveRight(5)); -``` - -### Equal Interface - -Boxes implement Effect's `Equal` interface for structural equality: - -```typescript -import { Equal } from "effect"; -import * as Box from "effect-boxes/Box"; - -const box1 = Box.text("hello"); -const box2 = Box.text("hello"); -const box3 = Box.text("world"); - -console.log(Equal.equals(box1, box2)); // true -console.log(Equal.equals(box1, box3)); // false -``` - -### Hash Interface - -Boxes implement Effect's `Hash` interface for efficient collection operations: - -```typescript -// Boxes can be used as Set keys or Map keys -const boxSet = new Set([box1, box2, box3]); -console.log(boxSet.size); // 2 (box1 and box2 are equal) -``` - -### Dual Functions - -Most functions support both data-first and data-last parameter ordering: - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; - -// Data-first: function(data, param) -const box1 = Box.alignHoriz(Box.text("Hello"), Box.center1, 20); - -// Data-last: pipe(data, function(param)) -const box2 = pipe(Box.text("Hello"), Box.alignHoriz(Box.center1, 20)); -``` - -## Reusable UI Patterns - -### Creating a Border - -```typescript -import { pipe, Array } from "effect"; -import * as Box from "effect-boxes/Box"; - -const Border = (self: Box.Box) => { - const middleBorder = pipe( - Array.makeBy(self.rows, () => Box.char("│")), - Box.vcat(Box.left) - ); - - const topBorder = pipe( - [Box.char("┌"), Box.text("─".repeat(self.cols)), Box.char("┐")], - Box.hcat(Box.top) - ); - - const bottomBorder = pipe( - [Box.char("└"), Box.text("─".repeat(self.cols)), Box.char("┘")], - Box.hcat(Box.top) - ); - - const middleSection = pipe( - [middleBorder, self, middleBorder], - Box.hcat(Box.top) - ); - - return pipe([topBorder, middleSection, bottomBorder], Box.vcat(Box.left)); -}; - -const bordered = Border(Box.text("Hello\nWorld")); -console.log(Box.renderPlainSync(bordered)); -/* -┌─────┐ -│Hello│ -│World│ -└─────┘ -*/ -``` - -### Adding Padding - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; - -const Padding = (width: number) => (self: Box.Box) => - pipe( - self, - Box.moveUp(width), - Box.moveDown(width), - Box.moveLeft(width), - Box.moveRight(width) - ); - -const padded = Padding(2)(Box.text("Hello")); -// Visualize with dots to show padding (rendered output has spaces) -// ......... -// ......... -// ..Hello.. -// ......... -// ......... -``` - -### Creating a Table - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; - -const createTable = (headers: string[], rows: string[][]) => { - const headerRow = Box.punctuateH( - headers.map((h) => Box.text(h).pipe(Box.alignHoriz(Box.center1, 12))), - Box.top, - Box.text(" | ") - ); - - const separator = Box.text("-".repeat(headerRow.cols)); - - const dataRows = rows.map((row) => - Box.punctuateH( - row.map((cell) => Box.text(cell).pipe(Box.alignHoriz(Box.left, 12))), - Box.top, - Box.text(" | ") - ) - ); - - return Box.vcat([headerRow, separator, ...dataRows], Box.left); -}; - -const table = createTable( - ["Name", "Age", "City"], - [ - ["Alice", "30", "New York"], - ["Bob", "25", "London"], - ["Charlie", "35", "Tokyo"], - ] -); -``` - -### Status Indicators - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Ansi from "effect-boxes/Ansi"; - -const createStatusBox = (status: "success" | "error" | "warning" | "info") => { - const styles = { - success: Ansi.combine(Ansi.green, Ansi.bold), - error: Ansi.combine(Ansi.red, Ansi.bold), - warning: Ansi.combine(Ansi.yellow, Ansi.bold), - info: Ansi.combine(Ansi.blue, Ansi.bold), - }; - - const icons = { - success: "✓", - error: "✗", - warning: "⚠", - info: "ℹ", - }; - - return pipe( - Box.text(`${icons[status]} ${status.toUpperCase()}`), - Box.annotate(styles[status]) - ); -}; - -const statusBox = createStatusBox("success"); -console.log(Box.renderPrettySync(statusBox)); -// Renders: "✓ SUCCESS" in bold green -``` - -### Progress Bar - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Ansi from "effect-boxes/Ansi"; - -const progressBar = (progress: number, total: number, width: number) => { - const ratio = Math.min(Math.max(progress / total, 0), 1); - const filledLength = Math.round(ratio * width); - const emptyLength = width - filledLength; - - // Dynamic color based on progress - const r = Math.round(255 * (1 - ratio)); - const g = Math.round(255 * ratio); - const progressColor = - progress === total ? Ansi.green : Ansi.colorRGB(r, g, 0); - - const filledBar = Box.text("█".repeat(filledLength)).pipe( - Box.annotate(progressColor) - ); - const emptyBar = Box.text("░".repeat(emptyLength)); - - return pipe(filledBar, Box.hAppend(emptyBar)); -}; - -const bar = progressBar(75, 100, 50); -console.log(Box.renderPrettySync(bar)); -// Renders a progress bar that's 75% complete with color gradient -``` - -## Module Integration Patterns - -### Box + ANSI - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Ansi from "effect-boxes/Ansi"; - -// Add ANSI color to a box -const coloredBox = Box.annotate(Box.text("Error!"), Ansi.red); - -// Combine multiple styles -const styledBox = pipe( - Box.text("Important"), - Box.annotate(Ansi.combine(Ansi.bold, Ansi.underlined, Ansi.red)) -); -``` - -### Box + Cmd - -```typescript -import { pipe } from "effect"; -import * as Ansi from "effect-boxes/Ansi"; -import * as Box from "effect-boxes/Box"; -import * as Cmd from "effect-boxes/Cmd"; - -// Combine boxes and commands for complex layouts -const complexLayout = pipe( - Cmd.clearScreen, - Box.combine( - Box.text("Title").pipe( - Box.annotate(Ansi.bold), - Box.alignHoriz(Box.center1, 80) - ) - ), - Box.combine(Cmd.cursorTo(0, 5)), - Box.combine(Box.text("Content goes here")) -); -``` - -### Box + Reactive + Cmd - -```typescript -import { pipe, Option } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Reactive from "effect-boxes/Reactive"; -import * as Cmd from "effect-boxes/Cmd"; - -// Create a layout with reactive elements -const layout = Box.vcat( - [ - Box.text("Header"), - Reactive.makeReactive(Box.text("Click me!"), "button"), - Box.text("Footer"), - ], - Box.left -); - -// Get positions of reactive elements -const positions = Reactive.getPositions(layout); - -// Move cursor to the button -const moveToButton = Reactive.cursorToReactive(positions, "button"); - -// Render the layout and then position cursor at the button -const combined = pipe( - layout, - Box.combine(Option.getOrElse(moveToButton, () => Box.nullBox)) -); -console.log(Box.renderPrettySync(combined)); -``` - -### Box + Layout - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Ansi from "effect-boxes/Ansi"; -import { Flex, Container, Grid } from "effect-boxes/Layout"; - -// Use Flex to build a status bar with items pushed apart -const statusBar = Flex.row( - [ - Flex.fixed(Box.text("app v1.0").pipe(Box.annotate(Ansi.bold))), - Flex.spacer(), - Flex.fixed(Box.text("connected").pipe(Box.annotate(Ansi.green))), - ], - 80 -); - -// Use Container for dimension-aware layouts with padding and borders -const panel = Container.make({ width: 60, padding: 1 }, (ctx) => - pipe( - Flex.row( - [Flex.fixed(Box.text("Name:")), Flex.grow(Box.text("Alice"))], - ctx.innerWidth - ), - Box.border("rounded") - ) -); - -// Use Grid to arrange items in columns -const grid = Grid.auto( - ["Dashboard", "Settings", "Profile", "Logout"].map((label) => - pipe(Box.text(label), Box.pad(0, 2), Box.border("rounded")) - ), - 80, - { minColWidth: 20 } -); -``` - -## See Also - -- [Box Module](./using-box.md) - Core box creation and composition -- [Layout Module](./using-layout.md) - Higher-level Flex, Container, and Grid layouts -- [Annotation Module](./using-annotation.md) - Text annotation system -- [ANSI Module](./using-ansi.md) - Terminal rendering with ANSI codes -- [Cmd Module](./using-cmd.md) - Terminal control commands -- [Reactive Module](./using-reactive.md) - Position tracking for interactive elements diff --git a/docs/using-annotation.md b/docs/using-annotation.md deleted file mode 100644 index 99a44ab..0000000 --- a/docs/using-annotation.md +++ /dev/null @@ -1,238 +0,0 @@ -# Annotation Module - -The Annotation module provides a system for attaching metadata to boxes. This -enables features like ANSI styling, interactive elements, and custom data -associations. - -## Core Concepts - -An `Annotation` is a wrapper around arbitrary data of type `A`. Annotations -can be attached to boxes using the `Box.annotate` function, and they can -influence rendering behavior or store application-specific data. - -## Basic Usage - -### Creating Annotations - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Annotation from "effect-boxes/Annotation"; - -// Create a simple annotation with string data -const myAnnotation = Annotation.createAnnotation("metadata"); - -// Create an annotation with complex data -interface UserData { - id: number; - name: string; -} - -const userData: UserData = { id: 123, name: "Alice" }; -const userAnnotation = Annotation.createAnnotation(userData); -``` - -### Attaching Annotations to Boxes - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Annotation from "effect-boxes/Annotation"; - -const myAnnotation = Annotation.createAnnotation("metadata"); - -// Attach an annotation to a box -const annotatedBox = Box.annotate(Box.text("Hello"), myAnnotation); - -// Attach an annotation with pipe -const annotatedBox2 = pipe(Box.text("Hello"), Box.annotate(myAnnotation)); -``` - -### Working with Annotated Boxes - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Annotation from "effect-boxes/Annotation"; - -const myAnnotation = Annotation.createAnnotation("metadata"); -const annotatedBox = Box.annotate(Box.text("Hello"), myAnnotation); - -// Remove annotation from a box -const plainBox = Box.unAnnotate(annotatedBox); - -// Transform the annotation data -const updatedBox = Box.reAnnotate(annotatedBox, (data: string) => - data.toUpperCase() -); - -// Create multiple boxes with different annotations -const boxes = Box.alterAnnotation(annotatedBox, (data: string) => [ - data + "1", - data + "2", - data + "3", -]); -// Result: Array of 3 boxes with annotations "metadata1", "metadata2", "metadata3" -``` - -## Annotation Utilities - -### Type Guards - -```typescript -import * as Annotation from "effect-boxes/Annotation"; - -// Check if a value is an Annotation -const isAnnotation = Annotation.isAnnotation(value); - -// Check if a value is an Annotation with specific data type -const isUserAnnotation = Annotation.isAnnotationWithData( - value, - (data): data is UserData => - typeof data === "object" && data !== null && "id" in data && "name" in data -); -``` - -### Data Extraction and Manipulation - -```typescript -import * as Annotation from "effect-boxes/Annotation"; - -const myAnnotation = Annotation.createAnnotation("metadata"); - -// Extract data from an annotation - -// Map annotation data to create a new annotation -const uppercaseAnnotation = Annotation.mapAnnotationData( - myAnnotation, - (data: string) => data.toUpperCase() -); - -// Combine two annotations -const combinedAnnotation = Annotation.combineAnnotations( - Annotation.createAnnotation({ count: 5 }), - Annotation.createAnnotation({ count: 10 }), - (a, b) => ({ count: a.count + b.count }) -); -// Result: Annotation with data { count: 15 } - -// Filter annotation based on a predicate -const filteredAnnotation = Annotation.filterAnnotation( - Annotation.createAnnotation(42), - (num) => num > 10 -); -// Result: Original annotation (condition is true) -``` - -### Batch Operations - -```typescript -import * as Annotation from "effect-boxes/Annotation"; - -// Create multiple annotations from data array -const dataArray = ["one", "two", "three"]; -const annotations = Annotation.createAnnotations(dataArray); - -// Extract data from multiple annotations -const extractedData = Annotation.extractAnnotationData(annotations); -// Result: ["one", "two", "three"] -``` - -## Common Use Cases - -### Custom Metadata - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Annotation from "effect-boxes/Annotation"; - -// Attach application-specific data to UI elements -interface ButtonData { - id: string; - action: string; - enabled: boolean; -} - -const buttonAnnotation = Annotation.createAnnotation({ - id: "submit-btn", - action: "submit-form", - enabled: true, -}); - -const buttonBox = pipe(Box.text(" Submit "), Box.annotate(buttonAnnotation)); -``` - -### Styling with ANSI - -The ANSI module uses annotations to apply terminal styling: - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Ansi from "effect-boxes/Ansi"; - -// The ANSI module creates specialized annotations -const coloredBox = pipe(Box.text("Error!"), Box.annotate(Ansi.red)); - -// Combine multiple styles -const styledBox = pipe( - Box.text("Important"), - Box.annotate(Ansi.combine(Ansi.bold, Ansi.underlined, Ansi.red)) -); - -// Use hex colors for foreground and background -const hexColoredBox = pipe( - Box.text("Custom Color"), - Box.annotate(Ansi.colorHex("#ff6600")) -); - -const hexBgBox = pipe( - Box.text("Highlighted"), - Box.annotate(Ansi.combine(Ansi.colorHex("#ffffff"), Ansi.bgColorHex("#333333"))) -); -``` - -## Advanced Usage - -### Type-Level Utilities - -```typescript -import * as Annotation from "effect-boxes/Annotation"; - -// Extract the data type from an Annotation type -import { type AnnotationData } from "effect-boxes/Annotation"; - -interface UserData { - id: number; - name: string; -} - -type MyAnnotation = Annotation.Annotation; -type ExtractedType = AnnotationData; -// ExtractedType is equivalent to UserData -``` - -### Nested Annotations - -When boxes with annotations are nested, the innermost annotation takes -precedence during rendering: - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Ansi from "effect-boxes/Ansi"; - -const nestedBox = pipe( - Box.text("Outer").pipe(Box.annotate(Ansi.red)), - Box.hAppend(Box.text("Inner").pipe(Box.annotate(Ansi.blue))) -); -// "Outer" will be red, "Inner" will be blue -``` - -## See Also - -- [Box Module](./using-box.md) - Core box creation and composition -- [ANSI Module](./using-ansi.md) - Terminal styling with ANSI codes (uses annotations) -- [Common Patterns](./common-patterns.md) - Reusable patterns and Effect.js - integration diff --git a/docs/using-ansi.md b/docs/using-ansi.md deleted file mode 100644 index 5053376..0000000 --- a/docs/using-ansi.md +++ /dev/null @@ -1,166 +0,0 @@ -# ANSI Module - -The ANSI module provides terminal styling capabilities for boxes, allowing you -to add colors, text formatting, and other terminal effects to your layouts. - -![ANSI Colors](../media/ansi-colors.png) - -## Core Concepts - -The ANSI module uses the Annotation system to attach styling information to -boxes. When rendering with `{ style: "pretty" }`, these annotations are -converted to ANSI escape sequences that most modern terminals understand. - -## Basic Usage - -### Text Colors - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Ansi from "effect-boxes/Ansi"; - -// Create a red text box -const redText = pipe(Box.text("Error!"), Box.annotate(Ansi.red)); - -// Create a green text box -const greenText = pipe(Box.text("Success!"), Box.annotate(Ansi.green)); - -// Print with ANSI colors enabled -console.log(Box.renderPrettySync(redText)); -``` - -### Background Colors - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Ansi from "effect-boxes/Ansi"; - -// Text with background color -const highlighted = pipe(Box.text("Warning"), Box.annotate(Ansi.bgYellow)); - -// Combine foreground and background colors -const colorCombo = pipe( - Box.text("Important"), - Box.annotate(Ansi.combine(Ansi.white, Ansi.bgRed)) -); -``` - -### Text Formatting - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Ansi from "effect-boxes/Ansi"; - -// Bold text -const boldText = pipe(Box.text("Bold"), Box.annotate(Ansi.bold)); - -// Underlined text -const underlinedText = pipe( - Box.text("Underlined"), - Box.annotate(Ansi.underlined) -); - -// Italic text -const italicText = pipe(Box.text("Italic"), Box.annotate(Ansi.italic)); - -// Combine multiple text attributes -const formattedText = pipe( - Box.text("Important"), - Box.annotate(Ansi.combine(Ansi.bold, Ansi.underlined)) -); -``` - -## Advanced Color Support - -### 256-Color Mode - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Ansi from "effect-boxes/Ansi"; - -// Use 256-color palette (0-255) -const color256Text = pipe( - Box.text("256 Colors"), - Box.annotate(Ansi.color256(39)) // Deep purple -); - -const bg256Text = pipe( - Box.text("256 Colors"), - Box.annotate(Ansi.bgColor256(39)) // Deep purple background -); -``` - -### True Color (RGB) - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Ansi from "effect-boxes/Ansi"; - -// Use true color (RGB) -const rgbText = pipe( - Box.text("RGB Color"), - Box.annotate(Ansi.colorRGB(255, 100, 50)) // Custom orange -); - -const bgRgbText = pipe( - Box.text("RGB Background"), - Box.annotate(Ansi.bgColorRGB(50, 100, 255)) // Custom blue background -); -``` - -## Combining Styles - -The `combine` function merges multiple ANSI styles, resolving conflicts with a -last-wins strategy: - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Ansi from "effect-boxes/Ansi"; - -// Combine multiple styles -const multiStyled = pipe( - Box.text("Styled Text"), - Box.annotate( - Ansi.combine( - Ansi.bold, - Ansi.underlined, - Ansi.colorRGB(255, 100, 50), - Ansi.bgBlue - ) - ) -); -``` - -## Rendering with ANSI - -To render boxes with ANSI styling, use the sync render functions: - -```typescript -import { pipe, Effect } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Ansi from "effect-boxes/Ansi"; -import { AnsiRendererLive } from "effect-boxes/Renderer"; - -const styledBox = pipe(Box.text("Hello!"), Box.annotate(Ansi.bold)); - -// Render with ANSI colors enabled (synchronous) -const rendered = Box.renderPrettySync(styledBox); -console.log(rendered); - -// Print using Effect (asynchronous) -const program = Box.printBox(styledBox).pipe(Effect.provide(AnsiRendererLive)); -Effect.runPromise(program); -``` - -## See Also - -- [Box Module](./using-box.md) - Core box creation and composition -- [Annotation Module](./using-annotation.md) - The underlying annotation system -- [Common Patterns](./common-patterns.md) - For practical examples and reusable - patterns diff --git a/docs/using-box.md b/docs/using-box.md deleted file mode 100644 index 88a99ea..0000000 --- a/docs/using-box.md +++ /dev/null @@ -1,369 +0,0 @@ -# Box Module - -The Box module is the core of the Effect Boxes library, providing the fundamental -data structures and operations for creating and composing rectangular text -layouts. - -## Core Concepts - -A **Box** is a rectangular container with explicit dimensions (rows and columns) -that can hold text or other boxes. Each box knows its size and can be combined -with others to create complex layouts through functional composition. - -## Box Creation - -### Basic Box Creation - -```typescript -import * as Box from "effect-boxes/Box"; - -// Create a box from text (automatically handles multi-line strings) -const textBox = Box.text("Hello\nWorld"); -// Result: 2 rows, 5 columns - -// Create an empty box with specific dimensions -const emptyBox = Box.emptyBox(3, 10); -// Result: 3 rows, 10 columns - -// Create a single-character box -const charBox = Box.char("*"); -// Result: 1 row, 1 column - -// Create a single-line box (strips newlines, joins into single line) -const lineBox = Box.line("Hello\nWorld"); -// Result: 1 row, 10 columns ("HelloWorld") -``` - -### Text Flow and Paragraphs - -```typescript -import * as Box from "effect-boxes/Box"; - -// Flow text into a paragraph with automatic wrapping -const paragraph = Box.para( - "This is a long text that will be automatically flowed into multiple lines based on the specified width.", - Box.left, // Alignment - 30 // Width -); - -// Create newspaper-style columns (returns Box[]) -const columnBoxes = Box.columns( - "Very long article text here that will be split into multiple columns...", - Box.left, // Alignment - 20, // Column width - 10 // Column height -); -// Combine the columns horizontally -const columnsLayout = Box.hcat(columnBoxes, Box.top); -``` - -## Box Composition - -### Horizontal Composition - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; - -// Combine boxes horizontally with alignment -const row = Box.hcat( - [Box.text("Left"), Box.text("Center"), Box.text("Right")], - Box.center1 // Vertical alignment within the row -); - -// Horizontal composition with a separator -const rowWithSeparator = Box.punctuateH( - [Box.text("Name"), Box.text("Age"), Box.text("City")], - Box.top, // Vertical alignment - Box.text(" | ") // Separator -); - -// Horizontal composition with spacing -const spacedRow = Box.hsep( - [Box.text("A"), Box.text("B"), Box.text("C")], - 3, // Number of spaces between boxes - Box.top // Vertical alignment -); -``` - -### Vertical Composition - -```typescript -import * as Box from "effect-boxes/Box"; - -// Combine boxes vertically with alignment -const column = Box.vcat( - [Box.text("Top"), Box.text("Middle"), Box.text("Bottom")], - Box.left // Horizontal alignment within the column -); - -// Vertical composition with a separator -const columnWithSeparator = Box.punctuateV( - [Box.text("Header"), Box.text("Content"), Box.text("Footer")], - Box.left, // Horizontal alignment - Box.text("~~~") // Separator -); - -// Vertical composition with spacing -const spacedColumn = Box.vsep( - [Box.text("First"), Box.text("Second"), Box.text("Third")], - 2, // Number of empty rows between boxes - Box.left // Horizontal alignment -); -``` - -### Combining Individual Boxes - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; - -// Append boxes horizontally -const combined1 = Box.hAppend(Box.text("Hello"), Box.text("World")); - -// Append boxes horizontally with pipe -const combined2 = pipe(Box.text("Hello"), Box.hAppend(Box.text("World"))); - -// Append boxes horizontally with a space -const combinedWithSpace = Box.hcatWithSpace( - Box.text("Hello"), - Box.text("World") -); - -// Append boxes vertically -const stacked = Box.vAppend(Box.text("Top"), Box.text("Bottom")); - -// Append boxes vertically with a space -const stackedWithSpace = Box.vcatWithSpace(Box.text("Top"), Box.text("Bottom")); -``` - -## Alignment and Positioning - -### Alignment - -```typescript -import * as Box from "effect-boxes/Box"; - -// Align a box horizontally within a width -const rightAligned = Box.alignHoriz(Box.text("Hello"), Box.right, 20); -// Result: " Hello" - -// Align a box vertically within a height -const bottomAligned = Box.alignVert(Box.text("Hello"), Box.bottom, 5); -// Result: 4 empty rows, then "Hello" - -// Align a box both horizontally and vertically -const centered = Box.align( - Box.text("Center me!"), - Box.center1, // Horizontal alignment - Box.center1, // Vertical alignment - 5, // Height - 20 // Width -); -``` - -### Moving Boxes - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; - -// Move a box right by adding space to the left -const movedRight = Box.moveRight(Box.text("Hello"), 5); -// Result: " Hello" - -// Move a box down by adding empty rows above -const movedDown = Box.moveDown(Box.text("Hello"), 3); -// Result: 3 empty rows, then "Hello" - -// Move a box left by adding space to the right -const movedLeft = Box.moveLeft(Box.text("Hello"), 5); -// Result: "Hello " - -// Move a box up by adding empty rows below -const movedUp = Box.moveUp(Box.text("Hello"), 2); -// Result: "Hello", then 2 empty rows - -// Combine movements with pipe -const positioned = pipe(Box.text("Hello"), Box.moveRight(5), Box.moveDown(2)); -// Result: 2 empty rows, then " Hello" -``` - -## Truncation - -Truncate each line of a box to a maximum width, inserting an ellipsis (`…`) -where content was removed. The `position` parameter controls which part of the -text is preserved. - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; - -const long = Box.text("This is a very long piece of text"); - -// Truncate from end (default) — keeps the beginning -const end = pipe(long, Box.truncate(15, Box.left)); -console.log(Box.renderPlainSync(end)); -// "This is a very…" - -// Truncate from start — keeps the end -const start = pipe(long, Box.truncate(15, Box.right)); -console.log(Box.renderPlainSync(start)); -// "…piece of text" - -// Truncate from middle — keeps both ends -const middle = pipe(long, Box.truncate(15, Box.center1)); -console.log(Box.renderPlainSync(middle)); -// "This is…of text" -``` - -If the box is already within the target width, it is returned unchanged. - -## Borders and Padding - -### Borders - -Add a border around a box using Unicode box-drawing characters. The default -style is `"single"`. - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; - -// Default single border -const bordered = pipe(Box.text("Hello"), Box.border()); -// ┌─────┐ -// │Hello│ -// └─────┘ - -// Double border -const doubleBordered = pipe(Box.text("Hello"), Box.border("double")); -// ╔═════╗ -// ║Hello║ -// ╚═════╝ - -// Rounded border -const rounded = pipe(Box.text("Hello"), Box.border("rounded")); -// ╭─────╮ -// │Hello│ -// ╰─────╯ -``` - -Available border styles: `"single"`, `"double"`, `"rounded"`, `"thick"`, -`"ascii"`. - -#### Colored Borders - -Use the `annotation` option to color the border characters: - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Ansi from "effect-boxes/Ansi"; - -const warning = pipe( - Box.text("Warning!"), - Box.border("rounded", { annotation: Ansi.yellow }) -); -console.log(Box.renderPrettySync(warning)); -``` - -### Padding - -Add empty space around a box's content. Supports CSS-like shorthand for -specifying padding per side. - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; - -// Uniform padding of 1 on all sides -const padded1 = pipe(Box.text("Hi"), Box.pad(1)); - -// Vertical and horizontal padding (1 row top/bottom, 2 cols left/right) -const padded2 = pipe(Box.text("Hi"), Box.pad(1, 2)); - -// Per-side padding: top, right, bottom, left (CSS order) -const padded3 = pipe(Box.text("Hi"), Box.pad(0, 2, 1, 2)); -``` - -### Combining Padding and Borders - -Padding and borders compose naturally with `pipe`: - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; - -const panel = pipe(Box.text("Hi"), Box.pad(1, 2), Box.border("rounded")); -console.log(Box.renderPlainSync(panel)); -// ╭──────╮ -// │ │ -// │ Hi │ -// │ │ -// ╰──────╯ -``` - - -## Rendering - -```typescript -import * as Box from "effect-boxes/Box"; - -const myBox = Box.text("Hello\nWorld"); - -// Render to string synchronously (plain text, no ANSI) -const plain = Box.renderPlainSync(myBox); - -// Render to string synchronously (with ANSI escape codes for styling) -const pretty = Box.renderPrettySync(myBox); - -// Render using Effect (for async/effectful contexts) -import { Effect } from "effect"; -import { PlainRendererLive } from "effect-boxes/Renderer"; - -const program = Box.render(myBox).pipe(Effect.provide(PlainRendererLive)); -Effect.runPromise(program).then(console.log); - -// Print a box to the console using Effect -const printProgram = Box.printBox(myBox).pipe(Effect.provide(PlainRendererLive)); -Effect.runPromise(printProgram); -``` - -## Working with Box Dimensions - -```typescript -import * as Box from "effect-boxes/Box"; - -// Get box dimensions -const myBox = Box.text("Hello\nWorld"); -console.log(myBox.rows); // 2 -console.log(myBox.cols); // 5 - -// Create a box with specific dimensions -const customBox = Box.emptyBox(5, 10); -console.log(customBox.rows); // 5 -console.log(customBox.cols); // 10 -``` - -## Combining with Annotations - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Ansi from "effect-boxes/Ansi"; - -// Add ANSI color to a box -const coloredBox = Box.annotate(Box.text("Error!"), Ansi.red); - -// Alternative data-last style with pipe -const coloredBox2 = pipe(Box.text("Success!"), Box.annotate(Ansi.green)); -``` - -## See Also - -- [Layout Module](./using-layout.md) - Higher-level Flex, Container, and Grid layouts -- [Annotation Module](./using-annotation.md) - For adding metadata to boxes -- [ANSI Module](./using-ansi.md) - For terminal styling with colors and effects -- [Common Patterns](./common-patterns.md) - For Effect.js integration and - reusable patterns diff --git a/docs/using-cmd.md b/docs/using-cmd.md deleted file mode 100644 index 974b742..0000000 --- a/docs/using-cmd.md +++ /dev/null @@ -1,190 +0,0 @@ -# Cmd Module - -The Cmd module provides terminal control commands for cursor positioning, screen -clearing, and other terminal operations. These commands can be composed with -boxes to create dynamic, interactive terminal interfaces. - -## Core Concepts - -The `Cmd` module creates special annotation boxes that emit ANSI escape -sequences when rendered. These commands allow precise control over terminal -behavior without affecting the layout model of boxes. - -Each command is a zero-size box (nullBox) with an annotation containing the -specific ANSI escape sequence. When rendered with `{ style: "pretty" }`, these -sequences control the terminal's behavior. - -## Basic Usage - -### Cursor Movement - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Cmd from "effect-boxes/Cmd"; - -// Move cursor up 3 lines -const moveUp = Cmd.cursorUp(3); - -// Move cursor to specific position (column, row) -const moveTo = Cmd.cursorTo(10, 5); - -// Move cursor relative to current position -const moveRelative = Cmd.cursorMove(5, -2); // 5 right, 2 up - -// Print with cursor commands -console.log(Box.renderPrettySync(moveUp)); -``` - -### Screen Clearing - -```typescript -import * as Cmd from "effect-boxes/Cmd"; - -// Clear the entire screen and move cursor to home position -const clear = Cmd.clearScreen; - -// Clear from cursor to end of screen -const clearDown = Cmd.eraseDown; - -// Clear the current line -const clearLine = Cmd.eraseLine; - -// Clear multiple lines (delete 5 lines from scroll region) -const clearLines = Cmd.eraseLines(5); - -// Clear content of previous 5 lines in place (no scrolling) -const clearInPlace = Cmd.clearLines(5); -``` - -### Cursor Visibility - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Cmd from "effect-boxes/Cmd"; - -// Hide the cursor (useful for animations) -const hideCursor = Cmd.cursorHide; - -// Show the cursor (restore visibility) -const showCursor = Cmd.cursorShow; - -// Typical pattern: hide cursor during animation, show when done -const animationSequence = pipe( - Cmd.cursorHide, - Box.combine(animationBox), - Box.combine(Cmd.cursorShow) -); -``` - -## Combining Commands with Boxes - -The real power of the Cmd module comes from combining terminal commands with -regular boxes: - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Cmd from "effect-boxes/Cmd"; - -// Create a box with text -const textBox = Box.text("Hello, world!"); - -// Combine with cursor commands -const positionedText = pipe( - Cmd.cursorTo(10, 5), // Position cursor - Box.combine(textBox), // Add the text - Box.combine(Cmd.cursorTo(0, 10)) // Move cursor away after rendering -); - -// Render with ANSI commands enabled -console.log(Box.renderPrettySync(positionedText)); -``` - -## Practical Example: Rewriting Previous Output - -`clearLines` is useful for overwriting previously printed lines without -affecting the rest of the terminal (e.g. spinners, progress bars): - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Cmd from "effect-boxes/Cmd"; - -// Print 3 lines, then clear and replace them -const initial = Box.vcat( - [ - Box.text("Downloading..."), - Box.text("Progress: 50%"), - Box.text("ETA: 10s"), - ], - Box.left -); - -// Later, clear those 3 lines and write new content -const updated = Box.vcat( - [ - Cmd.clearLines(3), - Box.text("Downloading..."), - Box.text("Progress: 100%"), - Box.text("Done!"), - ], - Box.left -); -``` - -## Practical Example: Partial Screen Updates - -```typescript -import { pipe, Effect } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Cmd from "effect-boxes/Cmd"; -import * as Ansi from "effect-boxes/Ansi"; - -// Initial setup -const setupScreen = pipe(Cmd.clearScreen, Box.combine(Cmd.cursorHide)); - -// Update just a portion of the screen -const updateCounter = (count: number) => - pipe( - Cmd.cursorTo(10, 5), - Box.combine(Box.text(`Count: ${count}`).pipe(Box.annotate(Ansi.green))) - ); - -// Cleanup when done -const cleanup = pipe(Cmd.cursorTo(0, 20), Box.combine(Cmd.cursorShow)); - -// Program that updates a counter in place -const program = Effect.gen(function* () { - // Setup screen once - yield* Effect.sync(() => - console.log(Box.renderPrettySync(setupScreen)) - ); - - // Update counter 10 times - for (let i = 0; i <= 10; i++) { - yield* Effect.sync(() => - console.log(Box.renderPrettySync(updateCounter(i))) - ); - yield* Effect.sleep("200 millis"); - } - - // Cleanup when done - yield* Effect.sync(() => - console.log(Box.renderPrettySync(cleanup)) - ); -}); - -Effect.runPromise(program); -``` - -## See Also - -- [Box Module](./using-box.md) - Core box creation and composition -- [Layout Module](./using-layout.md) - Higher-level Flex, Container, and Grid layouts -- [ANSI Module](./using-ansi.md) - Terminal styling with ANSI codes -- [Reactive Module](./using-reactive.md) - Position tracking for interactive - elements -- [Common Patterns](./common-patterns.md) - For integration examples and - reusable patterns diff --git a/docs/using-layout.md b/docs/using-layout.md deleted file mode 100644 index f3d4e2c..0000000 --- a/docs/using-layout.md +++ /dev/null @@ -1,239 +0,0 @@ -# Layout Module - -The Layout module provides higher-level layout combinators built on top of Box. -It offers container-aware composition primitives inspired by CSS Flexbox and CSS -Grid, making it straightforward to distribute space among children -proportionally, build dimension-aware containers, and arrange items in grids. - -All helpers are pure functions that return standard Box values, composable with -existing Box primitives (`border`, `annotate`, `pad`, etc). - -## Core Concepts - -The Layout module is organized into three namespaces: - -- **Flex** — flexbox-style row and column layouts that distribute space among - children proportionally -- **Container** — a dimension-aware wrapper that passes available dimensions to - a builder function -- **Grid** — fixed-column and auto-column grid layouts for arranging items in - rows and columns - -## Flex Layout - -### Child Types - -Flex layouts are built from three kinds of children: - -```typescript -import * as Box from "effect-boxes/Box"; -import { Flex } from "effect-boxes/Layout"; - -// Fixed: occupies its intrinsic width/height -const label = Flex.fixed(Box.text("Name:")); - -// Grow: stretches to fill remaining space (optional factor, default 1) -const value = Flex.grow(Box.text("value"), 2); - -// Fill: receives allocated size via builder function -const separator = Flex.fill((width) => Box.text("=".repeat(width))); - -// Spacer: pushes adjacent children apart (shorthand for fill with empty box) -const space = Flex.spacer(); -``` - -### Horizontal Layout (Row) - -Arrange children horizontally within a fixed container width: - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import { Flex } from "effect-boxes/Layout"; - -// Data-first -const row = Flex.row( - [Flex.fixed(Box.text("Name:")), Flex.spacer(), Flex.fixed(Box.text("[ok]"))], - 80 -); - -// Data-last (pipe) -const row2 = pipe( - [ - Flex.fixed(Box.text("Name:")), - Flex.grow(Box.text("value"), 2), - Flex.fixed(Box.text("[ok]")), - ], - Flex.row(80, { gap: 1 }) -); -``` - -### Vertical Layout (Column) - -Arrange children vertically within a fixed container height: - -```typescript -import * as Box from "effect-boxes/Box"; -import { Flex } from "effect-boxes/Layout"; - -const col = Flex.col( - [ - Flex.fixed(Box.text("header")), - Flex.grow(Box.text("body")), - Flex.fixed(Box.text("footer")), - ], - 24 -); -``` - -### Flex Options - -Both `Flex.row` and `Flex.col` accept an optional options object: - -- **`align`** — vertical alignment for row children or horizontal alignment for - column children (e.g. `Box.top`, `Box.center1`, `Box.bottom`) -- **`gap`** — number of columns (row) or rows (col) of space between children - -```typescript -import * as Box from "effect-boxes/Box"; -import { Flex } from "effect-boxes/Layout"; - -const row = Flex.row( - [Flex.fixed(Box.text("A")), Flex.fixed(Box.text("B"))], - 40, - { align: Box.center1, gap: 2 } -); -``` - -### Space Distribution - -Fixed children keep their intrinsic size. Grow and Fill children share remaining -space proportionally based on their factor. Remainder columns are distributed -one each to the first N grow children to avoid rounding gaps. - -If fixed children exceed the container size, grow children receive 0 space and -the result may be wider/taller than the container. - -## Container - -The Container provides available dimensions to a builder function, automatically -computing inner dimensions after padding. - -```typescript -import * as Box from "effect-boxes/Box"; -import { Container } from "effect-boxes/Layout"; - -const box = Container.make({ width: 40, padding: 1 }, (ctx) => - Box.text("inner width: " + ctx.innerWidth) -); -// ctx contains: width, height, innerWidth, innerHeight -``` - -### Container Options - -- **`width`** (required) — outer width of the container -- **`height`** — outer height of the container -- **`padding`** — uniform padding (number) or `[vertical, horizontal]` tuple - -The container enforces its width on the output and applies padding -automatically. - -## Grid Layout - -### Fixed-Column Grid - -Arrange items in a grid with a known number of columns and uniform column width: - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import { Grid } from "effect-boxes/Layout"; - -// Data-first -const grid = Grid.make(["A", "B", "C", "D"].map(Box.text), { - cols: 2, - colWidth: 10, - gap: [1, 0], -}); - -// Data-last (pipe) -const grid2 = pipe( - ["A", "B", "C", "D"].map(Box.text), - Grid.make({ cols: 2, colWidth: 10 }) -); -``` - -### Auto-Column Grid - -Let the grid calculate the number of columns from a container width: - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import { Grid } from "effect-boxes/Layout"; - -// Data-first -const grid = Grid.auto( - ["A", "B", "C", "D", "E", "F"].map(Box.text), - 80, - { minColWidth: 20 } -); - -// Data-last (pipe) -const grid2 = pipe( - ["A", "B", "C", "D", "E", "F"].map(Box.text), - Grid.auto(80, { minColWidth: 20 }) -); -``` - -### Grid Options - -**`Grid.make` options:** - -- **`cols`** — number of columns -- **`colWidth`** — uniform width of each column -- **`gap`** — `[horizontal, vertical]` spacing between cells -- **`align`** — alignment of items within their cells -- **`stretch`** — whether to stretch items to fill their cell width - -**`Grid.auto` options:** - -- **`minColWidth`** — minimum column width used to calculate column count -- **`maxColWidth`** — optional maximum column width -- **`gap`** — spacing between cells -- **`align`** — alignment of items within their cells -- **`stretch`** — whether to stretch items to fill their cell width - -## Composing with Box Primitives - -Layout results are standard Box values. Compose them freely with borders, -padding, annotations, and other Box operations: - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Ansi from "effect-boxes/Ansi"; -import { Flex, Container } from "effect-boxes/Layout"; - -const panel = Container.make({ width: 60, padding: 1 }, (ctx) => - pipe( - Flex.row( - [ - Flex.fixed(Box.text("Status:")), - Flex.spacer(), - Flex.fixed(pipe(Box.text(" OK "), Box.annotate(Ansi.green))), - ], - ctx.innerWidth - ), - Box.border("rounded") - ) -); -``` - -## See Also - -- [Box Module](./using-box.md) - Core box creation and composition -- [Annotation Module](./using-annotation.md) - For adding metadata to boxes -- [ANSI Module](./using-ansi.md) - For terminal styling with colors and effects -- [Common Patterns](./common-patterns.md) - For Effect.js integration and - reusable patterns diff --git a/docs/using-reactive.md b/docs/using-reactive.md deleted file mode 100644 index 2d74d44..0000000 --- a/docs/using-reactive.md +++ /dev/null @@ -1,284 +0,0 @@ -# Reactive Module - -The Reactive module provides position tracking capabilities for boxes, enabling -interactive terminal interfaces where you can precisely locate and update -specific elements. - -## Core Concepts - -The Reactive module uses the Annotation system to attach unique identifiers to -boxes. When rendered, these identifiers allow you to track the exact position of -each box in the terminal output, making it possible to: - -1. Move the cursor to specific UI elements -2. Update individual components without redrawing the entire screen -3. Create interactive interfaces with focused elements - -## Basic Usage - -### Creating Reactive Boxes - -```typescript -import { pipe } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Reactive from "effect-boxes/Reactive"; - -// Create a box with a reactive identifier -const button = Reactive.makeReactive(Box.text("[ OK ]"), "ok-button"); - -// Create a box with a reactive identifier using pipe -const inputField = pipe( - Box.text("Enter your name: ___________"), - Reactive.makeReactive("input-field") -); - -// Create a layout with multiple reactive elements -const form = Box.vcat( - [ - Box.text("Login Form").pipe(Reactive.makeReactive("form-title")), - Box.text("Username:").pipe( - Box.hAppend( - Box.text("_________").pipe(Reactive.makeReactive("username-field")) - ) - ), - Box.text("Password:").pipe( - Box.hAppend( - Box.text("_________").pipe(Reactive.makeReactive("password-field")) - ) - ), - Box.hcat( - [ - Box.text("[ Cancel ]").pipe(Reactive.makeReactive("cancel-button")), - Box.text("[ Submit ]").pipe(Reactive.makeReactive("submit-button")), - ], - Box.center1 - ), - ], - Box.left -); -``` - -### Tracking Positions - -```typescript -import * as Box from "effect-boxes/Box"; -import * as Reactive from "effect-boxes/Reactive"; - -// Create a form with reactive elements -const form = Box.vcat( - [ - Box.text("Login Form").pipe(Reactive.makeReactive("form-title")), - Box.text("Username: _________").pipe(Reactive.makeReactive("username-field")), - ], - Box.left -); - -// Render the layout and get positions of all reactive elements -const positions = Reactive.getPositions(form); - -// positions is a HashMap -// with entries for "form-title", "username-field", etc. - -// Get the position of a specific element -const usernamePosition = positions.get("username-field"); -if (usernamePosition) { - console.log( - `Username field is at row ${usernamePosition.row}, column ${usernamePosition.col}` - ); - console.log( - `It has dimensions: ${usernamePosition.rows} rows × ${usernamePosition.cols} columns` - ); -} -``` - -### Moving Cursor to Reactive Elements - -```typescript -import { Option } from "effect"; -import * as Box from "effect-boxes/Box"; -import * as Cmd from "effect-boxes/Cmd"; -import * as Reactive from "effect-boxes/Reactive"; - -// Assuming positions from previous example -const form = Box.vcat( - [ - Box.text("[ OK ]").pipe(Reactive.makeReactive("ok-button")), - Box.text("[ Cancel ]").pipe(Reactive.makeReactive("cancel-button")), - ], - Box.left -); -const positions = Reactive.getPositions(form); - -// Get a cursor movement command to a specific reactive element -const moveToCancelButton = Reactive.cursorToReactive( - positions, - "cancel-button" -); - -// Use with Option handling -const cursorCommand = Option.getOrElse( - moveToCancelButton, - () => Cmd.cursorTo(0, 0) // Default if element not found -); - -// Render the cursor movement -console.log(Box.renderPrettySync(cursorCommand)); -``` - -## Practical Example: Interactive Form Navigation - -```typescript -import { Effect, Option, pipe } from "effect"; -import * as Ansi from "effect-boxes/Ansi"; -import * as Box from "effect-boxes/Box"; -import * as Cmd from "effect-boxes/Cmd"; -import * as Reactive from "effect-boxes/Reactive"; - -// Create a form with multiple fields -const createForm = (focusedField: string) => { - // Helper to style the currently focused field - const styleField = (fieldId: string, content: string) => - Box.text(content).pipe( - Box.annotate( - focusedField === fieldId - ? Ansi.combine(Ansi.bgBlue, Ansi.white) - : Ansi.dim - ), - Reactive.makeReactive(fieldId) - ); - - return Box.vcat( - [ - Box.text("User Registration").pipe( - Box.annotate(Ansi.bold), - Box.alignHoriz(Box.center1, 40) - ), - Box.text(""), - Box.hcat( - [ - Box.text("Name: ").pipe(Box.alignHoriz(Box.right, 12)), - styleField("name-field", "John Doe"), - ], - Box.top - ), - Box.hcat( - [ - Box.text("Email: ").pipe(Box.alignHoriz(Box.right, 12)), - styleField("email-field", "john@example.com"), - ], - Box.top - ), - Box.hcat( - [ - Box.text("Password: ").pipe(Box.alignHoriz(Box.right, 12)), - styleField("password-field", "********"), - ], - Box.top - ), - Box.text(""), - Box.hcat( - [ - styleField("cancel-button", "[ Cancel ]"), - styleField("submit-button", "[ Submit ]"), - ], - Box.center1 - ).pipe(Box.alignHoriz(Box.center1, 40)), - ], - Box.left - ); -}; - -// Simulate form navigation -const formNavigation = Effect.gen(function* () { - const fields = [ - "name-field", - "email-field", - "password-field", - "cancel-button", - "submit-button", - ]; - - // Initial render with first field focused - let currentFieldIndex = 0; - let form = createForm(fields[currentFieldIndex]); - - // Render initial form - yield* Effect.sync(() => - console.log( - Box.renderPrettySync(pipe(Cmd.clearScreen, Box.combine(form))) - ) - ); - - // Simulate tabbing through fields - for (let i = 0; i < fields.length; i++) { - yield* Effect.sleep("800 millis"); - - // Move to next field - currentFieldIndex = (currentFieldIndex + 1) % fields.length; - form = createForm(fields[currentFieldIndex]); - - // Get positions of all reactive elements - const positions = Reactive.getPositions(form); - - // Get cursor command for the focused field - const moveToCurrent = Reactive.cursorToReactive( - positions, - fields[currentFieldIndex] - ); - - // Render the updated form and move cursor to focused field - yield* Effect.sync(() => - console.log( - Box.renderPrettySync( - pipe( - Cmd.cursorTo(0, 0), - Box.combine(form), - Box.combine(Option.getOrElse(moveToCurrent, () => Box.nullBox)) - ) - ) - ) - ); - } -}); -``` - -## API Reference - -### Core Types - -```typescript -// Position tracking map -export type PositionMap = HashMap.HashMap< - string, // Reactive ID - { - readonly row: number; // 0-based row position - readonly col: number; // 0-based column position - readonly rows: number; // height of the box - readonly cols: number; // width of the box - } ->; -``` - -### Key Functions - -```typescript -// Create a reactive box -makeReactive(self: Box, id: string): Box -makeReactive(id: string): (self: Box) => Box - -// Get positions of all reactive elements -getPositions(self: Box): PositionMap - -// Get cursor command to move to a reactive element -cursorToReactive(positionMap: PositionMap, key: string): Option.Option> -cursorToReactive(key: string): (positionMap: PositionMap) => Option.Option> -``` - -## See Also - -- [Box Module](./using-box.md) - Core box creation and composition -- [Layout Module](./using-layout.md) - Higher-level Flex, Container, and Grid layouts -- [Cmd Module](./using-cmd.md) - Terminal control commands -- [ANSI Module](./using-ansi.md) - Terminal styling with ANSI codes -- [Common Patterns](./common-patterns.md) - For integration examples and - reusable patterns diff --git a/media/ansi-colors.png b/media/ansi-colors.png index b25e2bb..0b5a673 100644 Binary files a/media/ansi-colors.png and b/media/ansi-colors.png differ diff --git a/media/ansi-demo.tape b/media/ansi-demo.tape index 73654e7..58197f4 100644 --- a/media/ansi-demo.tape +++ b/media/ansi-demo.tape @@ -3,13 +3,21 @@ Set Shell "zsh" Set FontSize 16 -Set Width 1100 +Set Width 920 Set Height 720 Set WindowBar Colorful Set Padding 20 +# --- Setup: cd into scratchpad app --- +Hide +Type "cd apps/scratchpad" Enter +Sleep 300ms +Type "clear" Enter +Sleep 300ms +Show + # Run the ANSI demo -Type "bun run scratch -- --run 4" +Type "bun dev -- --run 4" Enter Sleep 2s Screenshot media/ansi-colors.png diff --git a/media/demo.gif b/media/demo.gif index 211c060..05ee23b 100644 Binary files a/media/demo.gif and b/media/demo.gif differ diff --git a/media/demo.tape b/media/demo.tape index 5d0f802..e2a22a1 100644 --- a/media/demo.tape +++ b/media/demo.tape @@ -7,15 +7,22 @@ Output media/demo.gif Set Shell "zsh" Set FontSize 16 Set Width 1000 -Set Height 620 +Set Height 350 Set TypingSpeed 60ms Set WindowBar Colorful Set Padding 20 +# --- Setup: cd into scratchpad app --- +Hide +Type "cd apps/scratchpad" Enter +Sleep 300ms +Type "clear" Enter +Sleep 300ms +Show # Run the demo Sleep 1s -Type "bun run scratch -- --run 1" +Type "bun dev -- --run 1" Sleep 300ms Enter diff --git a/media/layout-demo.tape b/media/layout-demo.tape index 11e66c5..4eddf5d 100644 --- a/media/layout-demo.tape +++ b/media/layout-demo.tape @@ -12,10 +12,17 @@ Set TypingSpeed 60ms Set WindowBar Colorful Set Padding 20 +# --- Setup: cd into scratchpad app --- +Hide +Type "cd apps/scratchpad" Enter +Sleep 300ms +Type "clear" Enter +Sleep 300ms +Show # Run the demo Sleep 1s -Type "bun run scratch --run 10" +Type "bun dev -- --run 10" Sleep 300ms Enter diff --git a/media/perlin-demo.tape b/media/perlin-demo.tape index 594f5cd..b7f1565 100644 --- a/media/perlin-demo.tape +++ b/media/perlin-demo.tape @@ -12,10 +12,17 @@ Set TypingSpeed 60ms Set WindowBar Colorful Set Padding 20 +# --- Setup: cd into scratchpad app --- +Hide +Type "cd apps/scratchpad" Enter +Sleep 300ms +Type "clear" Enter +Sleep 300ms +Show # Run the demo Sleep 1s -Type "bun run scratch --run 8" +Type "bun dev -- --run 8" Sleep 300ms Enter diff --git a/packages/effect-boxes/.gitignore b/packages/effect-boxes/.gitignore index 5236e6f..1dfe5f6 100644 --- a/packages/effect-boxes/.gitignore +++ b/packages/effect-boxes/.gitignore @@ -1,5 +1,2 @@ -# Copied during prepack for npm publish -README.md - # Generated docs docs/ diff --git a/packages/effect-boxes/README.md b/packages/effect-boxes/README.md new file mode 100644 index 0000000..222eb42 --- /dev/null +++ b/packages/effect-boxes/README.md @@ -0,0 +1,145 @@ +# Effect Boxes + +A functional layout system for terminal applications built with Effect.js. +Create TUIs with composable boxes, ANSI styling, and reactive components. + +![Effect Boxes Demo](../../media/demo.gif) + +## What is Effect Boxes? + +Effect Boxes is a TypeScript library inspired by Haskell's +`Text.PrettyPrint.Boxes`, providing a flex-style layout system for terminal +applications within the Effect ecosystem. It started from the original box model +and function naming, then grew into its own implementation with Effect +integration, annotations, ANSI styling, and reactive rendering support. Think of +it as a CSS flexbox system, but built specifically for functional composition of +elements in terminal UIs, ASCII art, and structured text output. + +## Key Features + +- **Flex-y Layout System**: Horizontal and vertical composition with alignment + control +- **Text Flow**: Automatic paragraph wrapping and column layout +- **ANSI Color Support**: Rich terminal styling with colors and text attributes +- **Reactive Components**: Create dynamic UIs with efficient partial updates + +## Installation + +```bash +npm install effect-boxes +# or +bun add effect-boxes +# or +pnpm add effect-boxes +# or +yarn add effect-boxes +``` + +## Quick Start + +```typescript +import { Box, Ansi } from "effect-boxes"; +// Alternative import patterns: +// import * as Box from "effect-boxes/Box"; +// import * as Ansi from "effect-boxes/Ansi"; + +// Create a simple box with colored text and positioning +const myBox = Box.hsep( + [ + Box.text("Hello").pipe(Box.annotate(Ansi.green)), + Box.text("\nEffect").pipe(Box.annotate(Ansi.bold)), + Box.text("Boxes!").pipe(Box.annotate(Ansi.blue)), + ], + 1, + Box.left +); + +// Render to string (with ANSI colors) +console.log(Box.renderPrettySync(myBox)); +/** + * Hello Boxes! + * Effect + */ +``` + +## Example: Creating a Table + +```typescript +import { pipe } from "effect"; +import { Box } from "effect-boxes"; + +// Create a simple table layout +const createTable = (headers: string[], rows: string[][]) => { + const headerRow = Box.punctuateH( + headers.map((h) => Box.text(h).pipe(Box.alignHoriz(Box.center1, 12))), + Box.top, + Box.text(" | ") + ); + + const separator = Box.text("-".repeat(headerRow.cols)); + + const dataRows = rows.map((row) => + Box.punctuateH( + row.map((cell) => Box.text(cell).pipe(Box.alignHoriz(Box.left, 12))), + Box.top, + Box.text(" | ") + ) + ); + + return Box.vcat([headerRow, separator, ...dataRows], Box.left); +}; + +const table = createTable( + ["Name", "Age", "City"], + [ + ["Alice", "30", "New York"], + ["Bob", "25", "London"], + ["Charlie", "35", "Tokyo"], + ] +); + +console.log(Box.renderPlainSync(table)); +/** +* Name | Age | City +* ------------------------------------------ +* Alice | 30 | New York +* Bob | 25 | London +* Charlie | 35 | Tokyo +*/ +``` + +## Documentation + +Full documentation is available at +[effect-boxes.lloydrichards.dev](https://effect-boxes.lloydrichards.dev). + +### Getting Started + +New to Effect Boxes? Start here: +[Getting Started](https://effect-boxes.lloydrichards.dev/getting-started) + +### Guides + +- [Using Box](https://effect-boxes.lloydrichards.dev/guides/using-box) - Core layout primitives and composition +- [Using Annotation](https://effect-boxes.lloydrichards.dev/guides/using-annotation) - Adding metadata to boxes +- [Using Ansi](https://effect-boxes.lloydrichards.dev/guides/using-ansi) - Terminal colors and styling +- [Using Layout](https://effect-boxes.lloydrichards.dev/guides/using-layout) - Higher-level Flex, Container, and Grid layouts +- [Using Cmd](https://effect-boxes.lloydrichards.dev/guides/using-cmd) - Terminal control sequences +- [Rendering](https://effect-boxes.lloydrichards.dev/guides/rendering) - Output and rendering strategies +- [Common Patterns](https://effect-boxes.lloydrichards.dev/guides/common-patterns) - Reusable patterns and examples + +### API Reference + +Full API documentation for each module: +[API Reference](https://effect-boxes.lloydrichards.dev/api/box) + +## Contributing + +Interested in contributing? See [CONTRIBUTING.md](./CONTRIBUTING.md) for +development setup, commands, and release process. + +## License + +BSD-3-Clause + +This library is inspired by Haskell's `Text.PrettyPrint.Boxes` by Brent Yorgey. diff --git a/packages/effect-boxes/package.json b/packages/effect-boxes/package.json index d0d273c..a2b51b1 100644 --- a/packages/effect-boxes/package.json +++ b/packages/effect-boxes/package.json @@ -73,7 +73,6 @@ }, "scripts": { "clean": "git clean -xdf dist node_modules .cache .turbo docs tsconfig.tsbuildinfo", - "prepack": "cp ../../README.md .", "build": "tsup && tsc --project tsconfig.build.json", "lint": "biome lint .", "test": "vitest run",